Skip to main content

browser_commander/browser/
playwright_driver_page.rs

1//! Native Playwright engine: typed calls to the official driver.
2//!
3//! [`PlaywrightDriverPage`] drives the browser through the generated
4//! [`crate::playwright::protocol`] bindings over `playwright run-driver`, the
5//! way Playwright's own Python, Java and .NET clients do. It mirrors the
6//! Playwright half of `node_engine_bridge.js` call for call, so the generic
7//! Node bridge is only needed when no matching driver is installed.
8
9use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Arc;
12use std::time::Duration;
13
14use async_trait::async_trait;
15use base64::Engine as _;
16use serde_json::{json, Map, Value};
17
18use crate::core::engine::{ElementInfo, EngineAdapter, EngineError, EngineType, PdfOptions};
19use crate::playwright::protocol::*;
20use crate::playwright::{ChannelType, DriverOptions, PlaywrightDriver, ProtocolError};
21
22/// Timeout of actions and navigations, Playwright's default.
23pub const ACTION_TIMEOUT_MS: f64 = 30_000.0;
24/// Timeout of a browser launch, Playwright's default.
25pub const LAUNCH_TIMEOUT_MS: f64 = 180_000.0;
26const VERSION_PAGE_TIMEOUT_MS: f64 = 10_000.0;
27
28/// What to launch: the browser command line has already been resolved.
29#[derive(Debug, Clone, Default)]
30pub struct PlaywrightLaunch {
31    /// How to start the driver.
32    pub driver: DriverOptions,
33    /// Profile directory of the persistent context.
34    pub user_data_dir: PathBuf,
35    /// Run without a window.
36    pub headless: bool,
37    /// Delay between operations, in milliseconds.
38    pub slow_mo: u64,
39    /// Browser switches.
40    pub args: Vec<String>,
41    /// Variables added to the driver's environment for the browser process.
42    pub env: Option<HashMap<String, String>>,
43    /// Drop every Playwright default switch.
44    pub ignore_all_default_args: bool,
45    /// Playwright default switches to drop.
46    pub ignore_default_args: Vec<String>,
47    /// `light`, `dark` or `no-preference`.
48    pub color_scheme: Option<String>,
49    /// Keep Chromium's sandbox on.
50    pub sandbox: bool,
51    /// Browser channel (`chrome`, `msedge`…).
52    pub channel: Option<String>,
53    /// Browser executable.
54    pub executable_path: Option<PathBuf>,
55    /// Launch timeout; Playwright's default when unset.
56    pub timeout: Option<Duration>,
57}
58
59/// What to attach to.
60#[derive(Debug, Clone, Default)]
61pub struct PlaywrightConnect {
62    /// How to start the driver.
63    pub driver: DriverOptions,
64    /// `http://` or `ws://` DevTools endpoint.
65    pub endpoint: String,
66    /// Delay between operations, in milliseconds.
67    pub slow_mo: u64,
68    /// Connection timeout; Playwright's default when unset.
69    pub timeout: Option<Duration>,
70    /// Cookies (Playwright's shape) added to the default context.
71    pub seed_cookies: Vec<Value>,
72    /// Emulated on the picked page, best-effort.
73    pub color_scheme: Option<String>,
74}
75
76/// [`EngineAdapter`] backed by the official Playwright driver.
77pub struct PlaywrightDriverPage {
78    driver: Arc<PlaywrightDriver>,
79    browser: Browser,
80    context: BrowserContext,
81    page: Page,
82    frame: Frame,
83    launched: bool,
84}
85
86impl std::fmt::Debug for PlaywrightDriverPage {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct("PlaywrightDriverPage")
89            .field("driver", &self.driver)
90            .field("page", &self.page.guid())
91            .finish()
92    }
93}
94
95impl PlaywrightDriverPage {
96    /// Start a driver and launch a persistent context through it.
97    pub async fn launch(options: PlaywrightLaunch) -> Result<Self, ProtocolError> {
98        let driver = PlaywrightDriver::launch(options.driver.clone()).await?;
99        Self::launch_with(driver, options).await
100    }
101
102    /// Launch a persistent context through a running driver.
103    pub async fn launch_with(
104        driver: PlaywrightDriver,
105        options: PlaywrightLaunch,
106    ) -> Result<Self, ProtocolError> {
107        let driver = Arc::new(driver);
108        driver
109            .connection()
110            .set_default_timeout(Some(ACTION_TIMEOUT_MS));
111        let chromium = chromium(&driver)?;
112        let params = launch_params(&options)?;
113        let timeout = options.timeout.map(millis).unwrap_or(LAUNCH_TIMEOUT_MS);
114        let result: BrowserTypeLaunchPersistentContextResult = chromium
115            .channel()
116            .send_with_timeout("launchPersistentContext", &params, Some(timeout))
117            .await?;
118        let connection = driver.connection();
119        let browser = connection.object(&result.browser)?;
120        let context: BrowserContext = connection.object(&result.context)?;
121        let page = match connection
122            .children::<Page>(context.guid())
123            .into_iter()
124            .next()
125        {
126            Some(page) => page,
127            None => connection.object(&context.new_page().await?.page)?,
128        };
129        let adapter = Self::assemble(driver, browser, context, page, true)?;
130        if let Err(error) = adapter.page.bring_to_front().await {
131            tracing::debug!(%error, "bringToFront failed");
132        }
133        Ok(adapter)
134    }
135
136    /// Start a driver and attach it to a running browser over CDP.
137    pub async fn connect(options: PlaywrightConnect) -> Result<Self, ProtocolError> {
138        let driver = PlaywrightDriver::launch(options.driver.clone()).await?;
139        Self::connect_with(driver, options).await
140    }
141
142    /// Attach a running driver to a running browser over CDP.
143    pub async fn connect_with(
144        driver: PlaywrightDriver,
145        options: PlaywrightConnect,
146    ) -> Result<Self, ProtocolError> {
147        let driver = Arc::new(driver);
148        driver
149            .connection()
150            .set_default_timeout(Some(ACTION_TIMEOUT_MS));
151        let chromium = chromium(&driver)?;
152        let params = BrowserTypeConnectOverCDPParams {
153            endpoint_url: Some(options.endpoint.clone()),
154            slow_mo: (options.slow_mo > 0).then_some(options.slow_mo as f64),
155            ..Default::default()
156        };
157        let timeout = options.timeout.map(millis).unwrap_or(ACTION_TIMEOUT_MS);
158        let result: BrowserTypeConnectOverCDPResult = chromium
159            .channel()
160            .send_with_timeout("connectOverCDP", &params, Some(timeout))
161            .await?;
162        let connection = driver.connection();
163        let browser: Browser = connection.object(&result.browser)?;
164        let context: BrowserContext = match &result.default_context {
165            Some(context) => connection.object(context)?,
166            None => connection
167                .children::<BrowserContext>(browser.guid())
168                .into_iter()
169                .next()
170                .ok_or_else(|| {
171                    ProtocolError::Driver(
172                        "Connected browser did not expose a default context".to_string(),
173                    )
174                })?,
175        };
176        let pages = connection.children::<Page>(context.guid());
177        let page = match pick_foreground_page(connection, pages).await? {
178            Some(page) => page,
179            None => connection.object(&context.new_page().await?.page)?,
180        };
181        if !options.seed_cookies.is_empty() {
182            let cookies = serde_json::from_value(Value::Array(options.seed_cookies.clone()))?;
183            context
184                .add_cookies(BrowserContextAddCookiesParams { cookies })
185                .await?;
186        }
187        let adapter = Self::assemble(driver, browser, context, page, false)?;
188        if let Some(scheme) = &options.color_scheme {
189            let color_scheme = serde_json::from_value(json!(scheme))?;
190            let emulated = adapter
191                .page
192                .emulate_media(PageEmulateMediaParams {
193                    color_scheme: Some(color_scheme),
194                    ..Default::default()
195                })
196                .await;
197            if let Err(error) = emulated {
198                tracing::debug!(%error, "colorScheme failed");
199            }
200        }
201        if let Err(error) = adapter.page.bring_to_front().await {
202            tracing::debug!(%error, "bringToFront failed");
203        }
204        Ok(adapter)
205    }
206
207    fn assemble(
208        driver: Arc<PlaywrightDriver>,
209        browser: Browser,
210        context: BrowserContext,
211        page: Page,
212        launched: bool,
213    ) -> Result<Self, ProtocolError> {
214        let frame = driver
215            .connection()
216            .object(&page.initializer()?.main_frame)?;
217        Ok(Self {
218            driver,
219            browser,
220            context,
221            page,
222            frame,
223            launched,
224        })
225    }
226
227    /// The driver this page talks to.
228    pub fn driver(&self) -> &PlaywrightDriver {
229        &self.driver
230    }
231
232    /// The typed browser, context, page and main frame, for everything the
233    /// [`EngineAdapter`] surface does not cover.
234    pub fn objects(&self) -> (&Browser, &BrowserContext, &Page, &Frame) {
235        (&self.browser, &self.context, &self.page, &self.frame)
236    }
237
238    /// Close the browser (a launched one) or disconnect from it (an attached
239    /// one), then stop the driver.
240    pub async fn close(&self) -> Result<(), EngineError> {
241        let closed = if self.launched {
242            self.context.close(Default::default()).await
243        } else {
244            self.browser.close(Default::default()).await
245        };
246        self.driver.close().await;
247        closed.map_err(engine_error)
248    }
249
250    async fn call(&self, function: &str, arg: Value) -> Result<Value, EngineError> {
251        evaluate(&self.frame, function, Some(true), &arg)
252            .await
253            .map_err(engine_error)
254    }
255
256    async fn call_selector(&self, function: &str, selector: &str) -> Result<Value, EngineError> {
257        self.call(function, json!(selector)).await
258    }
259}
260
261fn chromium(driver: &PlaywrightDriver) -> Result<BrowserType, ProtocolError> {
262    driver
263        .connection()
264        .object(&driver.playwright().initializer()?.chromium)
265}
266
267fn launch_params(
268    options: &PlaywrightLaunch,
269) -> Result<BrowserTypeLaunchPersistentContextParams, ProtocolError> {
270    let mut args = options.args.clone();
271    if !options.sandbox {
272        for flag in ["--no-sandbox", "--disable-setuid-sandbox"] {
273            if !args.iter().any(|arg| arg == flag) {
274                args.push(flag.to_string());
275            }
276        }
277    }
278    // The browser gets the driver's environment plus the caller's, as the
279    // bridge gives it Node's; this process's environment is not changed.
280    let env = options.env.as_ref().map(|extra| {
281        let mut merged: Vec<(String, String)> = std::env::vars()
282            .filter(|(name, _)| !extra.contains_key(name))
283            .chain(
284                extra
285                    .iter()
286                    .map(|(name, value)| (name.clone(), value.clone())),
287            )
288            .collect();
289        merged.sort();
290        merged
291            .into_iter()
292            .map(|(name, value)| NameValue { name, value })
293            .collect()
294    });
295    let color_scheme = options
296        .color_scheme
297        .as_ref()
298        .map(|scheme| serde_json::from_value(json!(scheme)))
299        .transpose()?;
300    Ok(BrowserTypeLaunchPersistentContextParams {
301        launch_options: LaunchOptions {
302            channel: options.channel.clone(),
303            executable_path: options
304                .executable_path
305                .as_ref()
306                .map(|path| path.to_string_lossy().into_owned()),
307            args: Some(args),
308            ignore_all_default_args: options.ignore_all_default_args.then_some(true),
309            ignore_default_args: (!options.ignore_all_default_args)
310                .then(|| options.ignore_default_args.clone()),
311            env,
312            headless: Some(options.headless),
313            chromium_sandbox: Some(options.sandbox),
314            ..Default::default()
315        },
316        context_options: ContextOptions {
317            no_default_viewport: Some(true),
318            color_scheme,
319            ..Default::default()
320        },
321        user_data_dir: options.user_data_dir.to_string_lossy().into_owned(),
322        slow_mo: (options.slow_mo > 0).then_some(options.slow_mo as f64),
323    })
324}
325
326/// A fresh profile can open a tab that takes the foreground after startup,
327/// so the visible tab wins over the first one.
328async fn pick_foreground_page(
329    connection: &crate::playwright::Connection,
330    pages: Vec<Page>,
331) -> Result<Option<Page>, ProtocolError> {
332    for page in &pages {
333        let frame: Frame = connection.object(&page.initializer()?.main_frame)?;
334        match evaluate(&frame, "document.visibilityState", None, &Value::Null).await {
335            Ok(state) if state == "visible" => return Ok(Some(page.clone())),
336            Ok(_) => {}
337            Err(error) => tracing::debug!(%error, "visibilityState failed"),
338        }
339    }
340    Ok(pages.into_iter().next())
341}
342
343async fn evaluate(
344    frame: &Frame,
345    expression: &str,
346    is_function: Option<bool>,
347    arg: &Value,
348) -> Result<Value, ProtocolError> {
349    let result = frame
350        .evaluate_expression(FrameEvaluateExpressionParams {
351            expression: expression.to_string(),
352            is_function,
353            arg: SerializedArgument {
354                value: serialize_value(arg),
355                handles: Vec::new(),
356            },
357        })
358        .await?;
359    Ok(deserialize_value(&result.value))
360}
361
362fn engine_error(error: ProtocolError) -> EngineError {
363    if error.is_timeout() {
364        EngineError::Timeout(error.to_string())
365    } else {
366        EngineError::Browser(error.to_string())
367    }
368}
369
370fn millis(duration: Duration) -> f64 {
371    duration.as_millis() as f64
372}
373
374fn decode_binary(value: &str) -> Result<Vec<u8>, EngineError> {
375    base64::engine::general_purpose::STANDARD
376        .decode(value)
377        .map_err(|err| EngineError::Browser(format!("driver returned invalid base64: {err}")))
378}
379
380/// JSON to the protocol's `SerializedValue`.
381pub fn serialize_value(value: &Value) -> SerializedValue {
382    match value {
383        Value::Null => SerializedValue {
384            v: Some(SerializedValueV::Null),
385            ..Default::default()
386        },
387        Value::Bool(b) => SerializedValue {
388            b: Some(*b),
389            ..Default::default()
390        },
391        Value::Number(n) => SerializedValue {
392            n: n.as_f64(),
393            ..Default::default()
394        },
395        Value::String(s) => SerializedValue {
396            s: Some(s.clone()),
397            ..Default::default()
398        },
399        Value::Array(items) => SerializedValue {
400            a: Some(items.iter().map(serialize_value).collect()),
401            ..Default::default()
402        },
403        Value::Object(entries) => SerializedValue {
404            o: Some(
405                entries
406                    .iter()
407                    .map(|(k, v)| SerializedValueO {
408                        k: k.clone(),
409                        v: serialize_value(v),
410                    })
411                    .collect(),
412            ),
413            ..Default::default()
414        },
415    }
416}
417
418/// The protocol's `SerializedValue` to JSON, as `JSON.stringify` would see
419/// it: `undefined`, `NaN`, the infinities, handles and cycles become `null`;
420/// dates, URLs, big integers and regular expressions become strings; errors
421/// become `{name, message, stack}`; typed arrays become arrays of numbers.
422pub fn deserialize_value(value: &SerializedValue) -> Value {
423    if let Some(n) = value.n {
424        return number(n);
425    }
426    if let Some(b) = value.b {
427        return Value::Bool(b);
428    }
429    if let Some(s) = &value.s {
430        return Value::String(s.clone());
431    }
432    if let Some(special) = value.v {
433        return match special {
434            SerializedValueV::Negative0 => json!(0),
435            _ => Value::Null,
436        };
437    }
438    if let Some(text) = value.d.as_ref().or(value.u.as_ref()).or(value.bi.as_ref()) {
439        return Value::String(text.clone());
440    }
441    if let Some(regex) = &value.r {
442        return Value::String(format!("/{}/{}", regex.p, regex.f));
443    }
444    if let Some(error) = &value.e {
445        return json!({ "name": error.n, "message": error.m, "stack": error.s });
446    }
447    if let Some(array) = &value.ta {
448        return typed_array(array);
449    }
450    if let Some(items) = &value.a {
451        return Value::Array(items.iter().map(deserialize_value).collect());
452    }
453    if let Some(entries) = &value.o {
454        let object: Map<String, Value> = entries
455            .iter()
456            .map(|entry| (entry.k.clone(), deserialize_value(&entry.v)))
457            .collect();
458        return Value::Object(object);
459    }
460    Value::Null
461}
462
463/// A JavaScript number as `JSON.stringify` prints it: integral values have
464/// no fraction, non-finite ones are `null`.
465fn number(n: f64) -> Value {
466    const SAFE: f64 = 9_007_199_254_740_991.0;
467    if n.fract() == 0.0 && n.abs() <= SAFE {
468        json!(n as i64)
469    } else {
470        serde_json::Number::from_f64(n).map_or(Value::Null, Value::Number)
471    }
472}
473
474fn typed_array(array: &SerializedValueTa) -> Value {
475    use SerializedValueTaK as K;
476    let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(&array.b) else {
477        return Value::Null;
478    };
479    let size = match array.k {
480        K::I8 | K::Ui8 | K::Ui8c => 1,
481        K::I16 | K::Ui16 => 2,
482        K::I32 | K::Ui32 | K::F32 => 4,
483        K::F64 | K::Bi64 | K::Bui64 => 8,
484    };
485    let items = bytes.chunks_exact(size).map(|chunk| {
486        let mut word = [0u8; 8];
487        word[..size].copy_from_slice(chunk);
488        let unsigned = u64::from_le_bytes(word);
489        match array.k {
490            K::I8 => json!(chunk[0] as i8),
491            K::Ui8 | K::Ui8c => json!(chunk[0]),
492            K::I16 => json!(unsigned as u16 as i16),
493            K::Ui16 => json!(unsigned as u16),
494            K::I32 => json!(unsigned as u32 as i32),
495            K::Ui32 => json!(unsigned as u32),
496            K::F32 => number(f32::from_bits(unsigned as u32) as f64),
497            K::F64 => number(f64::from_bits(unsigned)),
498            K::Bi64 => json!((unsigned as i64).to_string()),
499            K::Bui64 => json!(unsigned.to_string()),
500        }
501    });
502    Value::Array(items.collect())
503}
504
505fn element_info(value: &Value) -> Option<ElementInfo> {
506    if value.is_null() {
507        return None;
508    }
509    let bounding_box = value["boundingBox"].as_array().and_then(|items| {
510        let item = |index: usize| items.get(index).and_then(Value::as_f64);
511        Some((item(0)?, item(1)?, item(2)?, item(3)?))
512    });
513    Some(ElementInfo {
514        tag_name: value["tagName"].as_str().unwrap_or("UNKNOWN").to_string(),
515        text_content: value["textContent"].as_str().map(ToString::to_string),
516        is_visible: value["isVisible"].as_bool().unwrap_or(false),
517        is_enabled: value["isEnabled"].as_bool().unwrap_or(true),
518        bounding_box,
519    })
520}
521
522const DESCRIBE: &str = r#"(el) => {
523  const rect = el.getBoundingClientRect();
524  const style = window.getComputedStyle(el);
525  const isVisible = style.display !== "none" && style.visibility !== "hidden" &&
526    rect.width > 0 && rect.height > 0;
527  return {
528    tagName: el.tagName,
529    textContent: el.textContent,
530    isVisible,
531    isEnabled: !el.disabled,
532    boundingBox: isVisible ? [rect.x, rect.y, rect.width, rect.height] : null,
533  };
534}"#;
535
536fn query_one() -> String {
537    format!(
538        "(selector) => {{ const el = document.querySelector(selector); return el ? ({DESCRIBE})(el) : null; }}"
539    )
540}
541
542fn query_all() -> String {
543    format!("(selector) => Array.from(document.querySelectorAll(selector), {DESCRIBE})")
544}
545
546const COUNT: &str = "(selector) => document.querySelectorAll(selector).length";
547const TEXT_CONTENT: &str =
548    "(selector) => { const el = document.querySelector(selector); return el ? el.textContent : null; }";
549const INPUT_VALUE: &str = "(selector) => { const el = document.querySelector(selector); return el && \"value\" in el ? el.value : null; }";
550const GET_ATTRIBUTE: &str = "({ selector, attribute }) => { const el = document.querySelector(selector); return el ? el.getAttribute(attribute) : null; }";
551const IS_ENABLED: &str =
552    "(selector) => { const el = document.querySelector(selector); return el ? !el.disabled : false; }";
553const SCROLL_INTO_VIEW: &str = r#"(selector) => {
554  const el = document.querySelector(selector);
555  if (!el) throw new Error(`Element not found: ${selector}`);
556  el.scrollIntoView({ block: "center", inline: "center" });
557}"#;
558const RESTORE_LOCAL_STORAGE: &str = r#"(origins) => {
559  const entry = origins.find((item) => item.origin === globalThis.location.origin);
560  if (!entry) return;
561  for (const item of entry.localStorage) globalThis.localStorage.setItem(item.name, item.value);
562}"#;
563const VERSION_PAGE_READY: &str = r#"() => document.getElementById("command_line")?.textContent"#;
564
565#[async_trait]
566impl EngineAdapter for PlaywrightDriverPage {
567    fn engine_type(&self) -> EngineType {
568        EngineType::Playwright
569    }
570
571    async fn url(&self) -> Result<String, EngineError> {
572        Ok(self.frame.initializer().map_err(engine_error)?.url)
573    }
574
575    async fn goto(&self, url: &str) -> Result<(), EngineError> {
576        self.frame
577            .goto(FrameGotoParams {
578                url: url.to_string(),
579                wait_until: Some(LifecycleEvent::Load),
580                ..Default::default()
581            })
582            .await
583            .map_err(engine_error)?;
584        Ok(())
585    }
586
587    async fn query_selector(&self, selector: &str) -> Result<Option<ElementInfo>, EngineError> {
588        Ok(element_info(
589            &self.call_selector(&query_one(), selector).await?,
590        ))
591    }
592
593    async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementInfo>, EngineError> {
594        let value = self.call_selector(&query_all(), selector).await?;
595        Ok(value
596            .as_array()
597            .map(|items| items.iter().filter_map(element_info).collect())
598            .unwrap_or_default())
599    }
600
601    async fn count(&self, selector: &str) -> Result<usize, EngineError> {
602        let value = self.call_selector(COUNT, selector).await?;
603        Ok(value.as_u64().unwrap_or(0) as usize)
604    }
605
606    async fn click(&self, selector: &str) -> Result<(), EngineError> {
607        self.frame
608            .click(FrameClickParams {
609                selector: selector.to_string(),
610                ..Default::default()
611            })
612            .await
613            .map_err(engine_error)
614    }
615
616    async fn mouse_click(&self, x: f64, y: f64) -> Result<(), EngineError> {
617        self.page
618            .mouse_click(PageMouseClickParams {
619                x,
620                y,
621                ..Default::default()
622            })
623            .await
624            .map_err(engine_error)
625    }
626
627    async fn fill(&self, selector: &str, text: &str) -> Result<(), EngineError> {
628        self.frame
629            .fill(FrameFillParams {
630                selector: selector.to_string(),
631                value: text.to_string(),
632                ..Default::default()
633            })
634            .await
635            .map_err(engine_error)
636    }
637
638    async fn type_text(&self, selector: &str, text: &str) -> Result<(), EngineError> {
639        self.frame
640            .focus(FrameFocusParams {
641                selector: selector.to_string(),
642                ..Default::default()
643            })
644            .await
645            .map_err(engine_error)?;
646        self.keyboard_type(text).await
647    }
648
649    async fn text_content(&self, selector: &str) -> Result<Option<String>, EngineError> {
650        let value = self.call_selector(TEXT_CONTENT, selector).await?;
651        Ok(value.as_str().map(ToString::to_string))
652    }
653
654    async fn input_value(&self, selector: &str) -> Result<Option<String>, EngineError> {
655        let value = self.call_selector(INPUT_VALUE, selector).await?;
656        Ok(value.as_str().map(ToString::to_string))
657    }
658
659    async fn get_attribute(
660        &self,
661        selector: &str,
662        attribute: &str,
663    ) -> Result<Option<String>, EngineError> {
664        let value = self
665            .call(
666                GET_ATTRIBUTE,
667                json!({ "selector": selector, "attribute": attribute }),
668            )
669            .await?;
670        Ok(value.as_str().map(ToString::to_string))
671    }
672
673    async fn is_visible(&self, selector: &str) -> Result<bool, EngineError> {
674        Ok(self
675            .query_selector(selector)
676            .await?
677            .is_some_and(|info| info.is_visible))
678    }
679
680    async fn is_enabled(&self, selector: &str) -> Result<bool, EngineError> {
681        let value = self.call_selector(IS_ENABLED, selector).await?;
682        Ok(value.as_bool().unwrap_or(false))
683    }
684
685    async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), EngineError> {
686        let params = FrameWaitForSelectorParams {
687            selector: selector.to_string(),
688            state: Some(FrameWaitForSelectorParamsState::Visible),
689            ..Default::default()
690        };
691        let _: FrameWaitForSelectorResult = self
692            .frame
693            .channel()
694            .send_with_timeout("waitForSelector", &params, Some(timeout_ms as f64))
695            .await
696            .map_err(engine_error)?;
697        Ok(())
698    }
699
700    async fn scroll_into_view(&self, selector: &str) -> Result<(), EngineError> {
701        self.call_selector(SCROLL_INTO_VIEW, selector).await?;
702        Ok(())
703    }
704
705    async fn evaluate(&self, script: &str) -> Result<Value, EngineError> {
706        // Like `page.evaluate(string)`: the driver calls the expression when
707        // it evaluates to a function.
708        evaluate(&self.frame, script, None, &Value::Null)
709            .await
710            .map_err(engine_error)
711    }
712
713    async fn read_browser_version_page(&self) -> Result<Value, EngineError> {
714        let connection = self.driver.connection();
715        let created = self.context.new_page().await.map_err(engine_error)?;
716        let page: Page = connection.object(&created.page).map_err(engine_error)?;
717        let read = async {
718            let frame: Frame = connection.object(&page.initializer()?.main_frame)?;
719            frame
720                .goto(FrameGotoParams {
721                    url: "chrome://version".to_string(),
722                    ..Default::default()
723                })
724                .await?;
725            let ready = FrameWaitForFunctionParams {
726                expression: VERSION_PAGE_READY.to_string(),
727                is_function: Some(true),
728                arg: SerializedArgument {
729                    value: serialize_value(&Value::Null),
730                    handles: Vec::new(),
731                },
732                ..Default::default()
733            };
734            let _: FrameWaitForFunctionResult = frame
735                .channel()
736                .send_with_timeout("waitForFunction", &ready, Some(VERSION_PAGE_TIMEOUT_MS))
737                .await?;
738            evaluate(
739                &frame,
740                crate::parity::VERSION_EXPRESSION,
741                None,
742                &Value::Null,
743            )
744            .await
745        }
746        .await;
747        let _ = page.close(Default::default()).await;
748        read.map_err(engine_error)
749    }
750
751    async fn restore_storage_state(&self, state: Value) -> Result<(), EngineError> {
752        let cookies = state.get("cookies").cloned().unwrap_or_else(|| json!([]));
753        let cookies: Vec<SetNetworkCookie> = serde_json::from_value(cookies)
754            .map_err(|err| EngineError::Browser(format!("invalid storage state cookie: {err}")))?;
755        let origins = state.get("origins").cloned().unwrap_or_else(|| json!([]));
756        if !cookies.is_empty() {
757            self.context
758                .add_cookies(BrowserContextAddCookiesParams { cookies })
759                .await
760                .map_err(engine_error)?;
761        }
762        if origins.as_array().is_some_and(|items| !items.is_empty()) {
763            self.context
764                .add_init_script(BrowserContextAddInitScriptParams {
765                    source: format!("({RESTORE_LOCAL_STORAGE})({origins})"),
766                })
767                .await
768                .map_err(engine_error)?;
769            let connection = self.driver.connection();
770            for page in connection.children::<Page>(self.context.guid()) {
771                let frame: Frame = page
772                    .initializer()
773                    .and_then(|init| connection.object(&init.main_frame))
774                    .map_err(engine_error)?;
775                evaluate(&frame, RESTORE_LOCAL_STORAGE, Some(true), &origins)
776                    .await
777                    .map_err(engine_error)?;
778            }
779        }
780        Ok(())
781    }
782
783    async fn export_storage_state(&self) -> Result<Value, EngineError> {
784        let state = self
785            .context
786            .storage_state(Default::default())
787            .await
788            .map_err(engine_error)?;
789        serde_json::to_value(state).map_err(|err| EngineError::Browser(err.to_string()))
790    }
791
792    async fn screenshot(&self) -> Result<Vec<u8>, EngineError> {
793        let shot = self
794            .page
795            .screenshot(Default::default())
796            .await
797            .map_err(engine_error)?;
798        decode_binary(&shot.binary)
799    }
800
801    async fn pdf(&self, options: PdfOptions) -> Result<Vec<u8>, EngineError> {
802        let margin = PagePdfParamsMargin {
803            top: options.margin_top,
804            right: options.margin_right,
805            bottom: options.margin_bottom,
806            left: options.margin_left,
807        };
808        let has_margin = margin != PagePdfParamsMargin::default();
809        let result = self
810            .page
811            .pdf(PagePdfParams {
812                format: options.format,
813                print_background: Some(options.print_background),
814                margin: has_margin.then_some(margin),
815                scale: options.scale,
816                ..Default::default()
817            })
818            .await
819            .map_err(engine_error)?;
820        let bytes = decode_binary(&result.pdf)?;
821        if let Some(path) = options.path {
822            tokio::fs::write(&path, &bytes)
823                .await
824                .map_err(|err| EngineError::Browser(format!("cannot write {path}: {err}")))?;
825        }
826        Ok(bytes)
827    }
828
829    async fn bring_to_front(&self) -> Result<(), EngineError> {
830        self.page.bring_to_front().await.map_err(engine_error)
831    }
832
833    async fn wait_for_navigation(&self, timeout_ms: u64) -> Result<(), EngineError> {
834        // Subscribe before looking, so a `load` between the two is not missed.
835        let mut events = self.frame.events();
836        let loaded = |frame: &Frame| {
837            frame
838                .initializer()
839                .map(|init| init.load_states.contains(&LifecycleEvent::Load))
840        };
841        if loaded(&self.frame).map_err(engine_error)? {
842            return Ok(());
843        }
844        let wait = async {
845            loop {
846                if let FrameEvent::Loadstate(FrameLoadstateEventParams {
847                    add: Some(LifecycleEvent::Load),
848                    ..
849                }) = events.recv().await?
850                {
851                    return Ok::<(), ProtocolError>(());
852                }
853            }
854        };
855        tokio::time::timeout(Duration::from_millis(timeout_ms), wait)
856            .await
857            .map_err(|_| {
858                EngineError::Timeout(format!(
859                    "waiting for the load event exceeded {timeout_ms} ms"
860                ))
861            })?
862            .map_err(engine_error)
863    }
864
865    async fn keyboard_press(&self, key: &str) -> Result<(), EngineError> {
866        self.page
867            .keyboard_press(PageKeyboardPressParams {
868                key: key.to_string(),
869                ..Default::default()
870            })
871            .await
872            .map_err(engine_error)
873    }
874
875    async fn keyboard_type(&self, text: &str) -> Result<(), EngineError> {
876        self.page
877            .keyboard_type(PageKeyboardTypeParams {
878                text: text.to_string(),
879                ..Default::default()
880            })
881            .await
882            .map_err(engine_error)
883    }
884
885    async fn keyboard_down(&self, key: &str) -> Result<(), EngineError> {
886        self.page
887            .keyboard_down(PageKeyboardDownParams {
888                key: key.to_string(),
889            })
890            .await
891            .map_err(engine_error)
892    }
893
894    async fn keyboard_up(&self, key: &str) -> Result<(), EngineError> {
895        self.page
896            .keyboard_up(PageKeyboardUpParams {
897                key: key.to_string(),
898            })
899            .await
900            .map_err(engine_error)
901    }
902}
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907
908    #[test]
909    fn json_round_trips_through_serialized_values() {
910        let value = json!({ "a": [1, 2.5, "x", null, true], "b": { "c": false } });
911        assert_eq!(deserialize_value(&serialize_value(&value)), value);
912    }
913
914    #[test]
915    fn values_json_cannot_hold_take_their_json_stringify_shape() {
916        let parse = |raw: Value| deserialize_value(&serde_json::from_value(raw).unwrap());
917        assert_eq!(parse(json!({ "v": "undefined" })), Value::Null);
918        assert_eq!(parse(json!({ "v": "NaN" })), Value::Null);
919        assert_eq!(parse(json!({ "v": "-0" })), json!(0));
920        assert_eq!(
921            parse(json!({ "d": "2020-01-01T00:00:00.000Z" })),
922            "2020-01-01T00:00:00.000Z"
923        );
924        assert_eq!(
925            parse(json!({ "bi": "12345678901234567890" })),
926            "12345678901234567890"
927        );
928        assert_eq!(parse(json!({ "r": { "p": "a+", "f": "g" } })), "/a+/g");
929        assert_eq!(
930            parse(json!({ "e": { "n": "TypeError", "m": "boom", "s": "at x" } })),
931            json!({ "name": "TypeError", "message": "boom", "stack": "at x" })
932        );
933        // Int16Array [1, -2]
934        assert_eq!(
935            parse(json!({ "ta": { "b": "AQD+/w==", "k": "i16" } })),
936            json!([1, -2])
937        );
938    }
939
940    #[test]
941    fn launch_params_follow_the_bridge() {
942        let options = PlaywrightLaunch {
943            user_data_dir: PathBuf::from("/tmp/profile"),
944            headless: true,
945            args: vec!["--disable-blink-features=AutomationControlled".to_string()],
946            env: Some(HashMap::from([("TZ".to_string(), "UTC".to_string())])),
947            ignore_default_args: vec!["--enable-automation".to_string()],
948            color_scheme: Some("dark".to_string()),
949            sandbox: false,
950            executable_path: Some(PathBuf::from("/opt/chrome")),
951            ..Default::default()
952        };
953        let params = serde_json::to_value(launch_params(&options).unwrap()).unwrap();
954
955        assert_eq!(params["userDataDir"], "/tmp/profile");
956        assert_eq!(params["headless"], true);
957        assert_eq!(params["chromiumSandbox"], false);
958        assert_eq!(params["noDefaultViewport"], true);
959        assert_eq!(params["colorScheme"], "dark");
960        assert_eq!(params["executablePath"], "/opt/chrome");
961        assert_eq!(params["ignoreDefaultArgs"], json!(["--enable-automation"]));
962        assert!(params.get("ignoreAllDefaultArgs").is_none());
963        assert!(params.get("slowMo").is_none());
964        let args = params["args"].as_array().unwrap();
965        assert!(args.contains(&json!("--no-sandbox")));
966        assert!(args.contains(&json!("--disable-setuid-sandbox")));
967        let env = params["env"].as_array().unwrap();
968        assert!(env.contains(&json!({ "name": "TZ", "value": "UTC" })));
969        assert!(env.len() > 1, "the driver's own environment is kept");
970    }
971
972    #[test]
973    fn ignoring_every_default_switch_sends_the_flag_alone() {
974        let options = PlaywrightLaunch {
975            ignore_all_default_args: true,
976            ignore_default_args: vec!["--x".to_string()],
977            sandbox: true,
978            ..Default::default()
979        };
980        let params = serde_json::to_value(launch_params(&options).unwrap()).unwrap();
981        assert_eq!(params["ignoreAllDefaultArgs"], true);
982        assert!(params.get("ignoreDefaultArgs").is_none());
983        assert!(params.get("env").is_none());
984        assert_eq!(params["args"], json!([]));
985    }
986}