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