1use 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
22pub const ACTION_TIMEOUT_MS: f64 = 30_000.0;
24pub const LAUNCH_TIMEOUT_MS: f64 = 180_000.0;
26const VERSION_PAGE_TIMEOUT_MS: f64 = 10_000.0;
27
28#[derive(Debug, Clone, Default)]
30pub struct PlaywrightLaunch {
31 pub driver: DriverOptions,
33 pub user_data_dir: PathBuf,
35 pub headless: bool,
37 pub slow_mo: u64,
39 pub args: Vec<String>,
41 pub env: Option<HashMap<String, String>>,
43 pub ignore_all_default_args: bool,
45 pub ignore_default_args: Vec<String>,
47 pub color_scheme: Option<String>,
49 pub sandbox: bool,
51 pub channel: Option<String>,
53 pub executable_path: Option<PathBuf>,
55 pub timeout: Option<Duration>,
57}
58
59#[derive(Debug, Clone, Default)]
61pub struct PlaywrightConnect {
62 pub driver: DriverOptions,
64 pub endpoint: String,
66 pub slow_mo: u64,
68 pub timeout: Option<Duration>,
70 pub seed_cookies: Vec<Value>,
72 pub color_scheme: Option<String>,
74}
75
76pub 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 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 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", ¶ms, 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 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 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", ¶ms, 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 pub fn driver(&self) -> &PlaywrightDriver {
229 &self.driver
230 }
231
232 pub fn objects(&self) -> (&Browser, &BrowserContext, &Page, &Frame) {
235 (&self.browser, &self.context, &self.page, &self.frame)
236 }
237
238 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 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
326async 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
380pub 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
418pub 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
463fn 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", ¶ms, 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 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 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 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}