Skip to main content

browser_control/cli/
eval.rs

1//! `browser-control eval` — evaluate a JS expression in the active page.
2
3use anyhow::Result;
4use serde_json::Value;
5
6use crate::cli::mcp::resolve_browser;
7use crate::session::PageSession;
8
9pub async fn run(
10    browser: Option<String>,
11    expression: String,
12    target: Option<String>,
13    json: bool,
14    await_promise: bool,
15) -> Result<()> {
16    let resolved = resolve_browser(browser).await?;
17    let session =
18        PageSession::attach(&resolved.endpoint, resolved.engine, target.as_deref()).await?;
19    let value = session.evaluate(&expression, await_promise).await;
20    session.close().await;
21    let value = value?;
22    println!("{}", format_output(&value, json));
23    Ok(())
24}
25
26fn format_output(v: &Value, json: bool) -> String {
27    if json {
28        serde_json::to_string_pretty(v).unwrap_or_else(|_| v.to_string())
29    } else if let Some(s) = v.as_str() {
30        s.to_string()
31    } else {
32        serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use serde_json::json;
40
41    #[test]
42    fn eval_returns_string_unquoted_in_text_mode() {
43        let v = json!("hello");
44        assert_eq!(format_output(&v, false), "hello");
45    }
46
47    #[test]
48    fn eval_returns_number_as_json() {
49        let v = json!(42);
50        assert_eq!(format_output(&v, false), "42");
51    }
52
53    #[test]
54    fn eval_returns_json_envelope_when_json_flag() {
55        let v = json!({"a": 1});
56        let out = format_output(&v, true);
57        let parsed: Value = serde_json::from_str(&out).expect("valid JSON");
58        assert_eq!(parsed, v);
59        assert!(out.contains("\"a\""));
60    }
61
62    #[test]
63    fn eval_null_text_mode() {
64        let v = json!(null);
65        assert_eq!(format_output(&v, false), "null");
66    }
67
68    #[test]
69    fn eval_bool_text_mode() {
70        assert_eq!(format_output(&json!(true), false), "true");
71    }
72
73    // Mock CDP round-trip test mirroring src/session/attach.rs tests.
74    use crate::detect::Engine;
75    use crate::session::PageSession;
76    use futures_util::{SinkExt, StreamExt};
77    use tokio_tungstenite::tungstenite::Message;
78
79    async fn spawn_cdp_mock(targets: Vec<Value>, eval_value: Value) -> String {
80        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
81        let addr = listener.local_addr().unwrap();
82        tokio::spawn(async move {
83            let (stream, _) = listener.accept().await.unwrap();
84            let mut ws = tokio_tungstenite::accept_async(stream).await.unwrap();
85            while let Some(Ok(Message::Text(t))) = ws.next().await {
86                let req: Value = serde_json::from_str(&t).unwrap();
87                let id = req["id"].as_u64().unwrap();
88                let method = req["method"].as_str().unwrap_or("");
89                let result = match method {
90                    "Target.getTargets" => json!({"targetInfos": targets.clone()}),
91                    "Target.attachToTarget" => json!({"sessionId": "S1"}),
92                    "Runtime.evaluate" => json!({"result": {"value": eval_value.clone()}}),
93                    _ => json!({}),
94                };
95                let resp = json!({"id": id, "result": result});
96                ws.send(Message::Text(resp.to_string())).await.unwrap();
97            }
98        });
99        format!("ws://{addr}")
100    }
101
102    #[tokio::test]
103    async fn eval_mock_returns_string() {
104        let url = spawn_cdp_mock(
105            vec![json!({"targetId":"a","type":"page","url":"https://example.com/"})],
106            json!("hello"),
107        )
108        .await;
109        let s = PageSession::attach(&url, Engine::Cdp, None).await.unwrap();
110        let v = s.evaluate("'hello'", false).await.unwrap();
111        s.close().await;
112        assert_eq!(format_output(&v, false), "hello");
113    }
114
115    #[tokio::test]
116    async fn eval_mock_returns_object_json() {
117        let url = spawn_cdp_mock(
118            vec![json!({"targetId":"a","type":"page","url":"https://example.com/"})],
119            json!({"a": 1}),
120        )
121        .await;
122        let s = PageSession::attach(&url, Engine::Cdp, None).await.unwrap();
123        let v = s.evaluate("({a:1})", false).await.unwrap();
124        s.close().await;
125        let out = format_output(&v, true);
126        let parsed: Value = serde_json::from_str(&out).unwrap();
127        assert_eq!(parsed, json!({"a": 1}));
128    }
129}