playwright_rs/protocol/page.rs
1// Page protocol object
2//
3// Represents a web page within a browser context.
4// Pages are isolated tabs or windows within a context.
5
6use crate::error::{Error, Result};
7use crate::protocol::browser_context::Viewport;
8use crate::protocol::{Dialog, Download, Request, ResponseObject, Route, WebSocket, Worker};
9use crate::server::channel::Channel;
10use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
11use crate::server::connection::{ConnectionExt, downcast_parent};
12use base64::Engine;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::any::Any;
16use std::collections::HashMap;
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::sync::{Arc, Mutex, RwLock};
21
22use crate::protocol::event_registry::{EventRegistry, Handler};
23use tracing::Instrument;
24
25/// Page represents a web page within a browser context.
26///
27/// A Page is created when you call `BrowserContext::new_page()` or `Browser::new_page()`.
28/// Each page is an isolated tab/window within its parent context.
29///
30/// Initially, pages are navigated to "about:blank". Use navigation methods
31/// Use navigation methods to navigate to URLs.
32///
33/// # Example
34///
35/// ```no_run
36/// use playwright_rs::protocol::{
37/// Playwright, ScreenshotOptions, ScreenshotType, AddStyleTagOptions, AddScriptTagOptions,
38/// EmulateMediaOptions, Media, ColorScheme, Viewport,
39/// };
40/// use std::path::PathBuf;
41///
42/// #[tokio::main]
43/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
44/// let playwright = Playwright::launch().await?;
45/// let browser = playwright.chromium().launch().await?;
46/// let page = browser.new_page().await?;
47///
48/// // Demonstrate url() - initially at about:blank
49/// assert_eq!(page.url(), "about:blank");
50///
51/// // Demonstrate goto() - navigate to a page
52/// let html = r#"<!DOCTYPE html>
53/// <html>
54/// <head><title>Test Page</title></head>
55/// <body>
56/// <h1 id="heading">Hello World</h1>
57/// <p>First paragraph</p>
58/// <p>Second paragraph</p>
59/// <button onclick="alert('Alert!')">Alert</button>
60/// <a href="data:text/plain,file" download="test.txt">Download</a>
61/// </body>
62/// </html>
63/// "#;
64/// // Data URLs may not return a response (this is normal)
65/// let _response = page.goto(&format!("data:text/html,{}", html), None).await?;
66///
67/// // Demonstrate title()
68/// let title = page.title().await?;
69/// assert_eq!(title, "Test Page");
70///
71/// // Demonstrate content() - returns full HTML including DOCTYPE
72/// let content = page.content().await?;
73/// assert!(content.contains("<!DOCTYPE html>") || content.to_lowercase().contains("<!doctype html>"));
74/// assert!(content.contains("<title>Test Page</title>"));
75/// assert!(content.contains("Hello World"));
76///
77/// // Demonstrate locator()
78/// let heading = page.locator("#heading");
79/// let text = heading.text_content().await?;
80/// assert_eq!(text, Some("Hello World".to_string()));
81///
82/// // Demonstrate query_selector()
83/// let element = page.query_selector("h1").await?;
84/// assert!(element.is_some(), "Should find the h1 element");
85///
86/// // Demonstrate query_selector_all()
87/// let paragraphs = page.query_selector_all("p").await?;
88/// assert_eq!(paragraphs.len(), 2);
89///
90/// // Demonstrate evaluate()
91/// page.evaluate::<(), ()>("console.log('Hello from Playwright!')", None).await?;
92///
93/// // Demonstrate evaluate_value()
94/// let result = page.evaluate_value("1 + 1").await?;
95/// assert_eq!(result, "2");
96///
97/// // Demonstrate screenshot()
98/// let bytes = page.screenshot(None).await?;
99/// assert!(!bytes.is_empty());
100///
101/// // Demonstrate screenshot_to_file()
102/// let temp_dir = std::env::temp_dir();
103/// let path = temp_dir.join("playwright_doctest_screenshot.png");
104/// let bytes = page.screenshot_to_file(&path, Some(
105/// ScreenshotOptions::builder()
106/// .screenshot_type(ScreenshotType::Png)
107/// .build()
108/// )).await?;
109/// assert!(!bytes.is_empty());
110///
111/// // Demonstrate reload()
112/// // Data URLs may not return a response on reload (this is normal)
113/// let _response = page.reload(None).await?;
114///
115/// // Demonstrate route() - network interception
116/// page.route("**/*.png", |route| async move {
117/// route.abort(None).await
118/// }).await?;
119///
120/// // Demonstrate on_download() - download handler
121/// page.on_download(|download| async move {
122/// println!("Download started: {}", download.url());
123/// Ok(())
124/// }).await?;
125///
126/// // Demonstrate on_dialog() - dialog handler
127/// page.on_dialog(|dialog| async move {
128/// println!("Dialog: {} - {}", dialog.type_(), dialog.message());
129/// dialog.accept(None).await
130/// }).await?;
131///
132/// // Demonstrate add_style_tag() - inject CSS
133/// page.add_style_tag(
134/// AddStyleTagOptions::builder()
135/// .content("body { background-color: blue; }")
136/// .build()
137/// ).await?;
138///
139/// // Demonstrate set_extra_http_headers() - set page-level headers
140/// let mut headers = std::collections::HashMap::new();
141/// headers.insert("x-custom-header".to_string(), "value".to_string());
142/// page.set_extra_http_headers(headers).await?;
143///
144/// // Demonstrate emulate_media() - emulate print media type
145/// page.emulate_media(Some(
146/// EmulateMediaOptions::builder()
147/// .media(Media::Print)
148/// .color_scheme(ColorScheme::Dark)
149/// .build()
150/// )).await?;
151///
152/// // Demonstrate add_script_tag() - inject a script
153/// page.add_script_tag(Some(
154/// AddScriptTagOptions::builder()
155/// .content("window.injectedByScriptTag = true;")
156/// .build()
157/// )).await?;
158///
159/// // Demonstrate pdf() - generate PDF (Chromium only)
160/// let pdf_bytes = page.pdf(None).await?;
161/// assert!(!pdf_bytes.is_empty());
162///
163/// // Demonstrate set_viewport_size() - responsive testing
164/// let mobile_viewport = Viewport {
165/// width: 375,
166/// height: 667,
167/// };
168/// page.set_viewport_size(mobile_viewport).await?;
169///
170/// // Demonstrate close()
171/// page.close().await?;
172///
173/// browser.close().await?;
174/// Ok(())
175/// }
176/// ```
177///
178/// See: <https://playwright.dev/docs/api/class-page>
179#[derive(Clone)]
180pub struct Page {
181 base: ChannelOwnerImpl,
182 /// The page's main frame, resolved once at construction (the protocol
183 /// guarantees the Frame object exists before the Page that references it)
184 main_frame: crate::protocol::Frame,
185 /// Route handlers for network interception
186 route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
187 /// Download event handlers and one-shot `expect_download` waiters.
188 download: Arc<EventRegistry<Download>>,
189 /// Dialog event handlers (no `expect_*`; waiter queue stays empty).
190 dialog: Arc<EventRegistry<Dialog>>,
191 /// Request event handlers and one-shot `expect_request` waiters.
192 request: Arc<EventRegistry<Request>>,
193 /// RequestFinished event handlers (the event has no `expect_*`, so its
194 /// registry's waiter queue simply stays empty).
195 request_finished: Arc<EventRegistry<Request>>,
196 /// RequestFailed event handlers (no `expect_*`; waiter queue stays empty).
197 request_failed: Arc<EventRegistry<Request>>,
198 /// Response event handlers and one-shot `expect_response` waiters.
199 response: Arc<EventRegistry<ResponseObject>>,
200 /// WebSocket event handlers
201 websocket_handlers: Arc<Mutex<Vec<WebSocketHandler>>>,
202 /// WebSocketRoute handlers for route_web_socket()
203 ws_route_handlers: Arc<Mutex<Vec<WsRouteHandlerEntry>>>,
204 /// Current viewport size (None when no_viewport is set).
205 /// Updated by set_viewport_size().
206 viewport: Arc<RwLock<Option<Viewport>>>,
207 /// Whether this page has been closed.
208 /// Set to true when close() is called or a "close" event is received.
209 is_closed: Arc<AtomicBool>,
210 /// Default timeout for actions (milliseconds), stored as f64 bits.
211 default_timeout_ms: Arc<AtomicU64>,
212 /// Default timeout for navigation operations (milliseconds), stored as f64 bits.
213 default_navigation_timeout_ms: Arc<AtomicU64>,
214 /// Page-level binding callbacks registered via expose_function / expose_binding
215 binding_callbacks: Arc<Mutex<HashMap<String, PageBindingCallback>>>,
216 /// Screencast frame handlers
217 screencast_frame_handlers: Arc<Mutex<Vec<ScreencastFrameHandler>>>,
218 /// Active screencast Artifact GUID (set when `screencastStart` was
219 /// called with a path; cleared on `screencastStop`).
220 screencast_artifact_guid: Arc<Mutex<Option<String>>>,
221 /// Path to save the screencast Artifact to on stop.
222 screencast_save_path: Arc<Mutex<Option<std::path::PathBuf>>>,
223 /// FileChooser event handlers and one-shot `expect_file_chooser` waiters.
224 filechooser: Arc<EventRegistry<crate::protocol::FileChooser>>,
225 /// Console event handlers and one-shot `expect_console_message` waiters.
226 console: Arc<EventRegistry<crate::protocol::ConsoleMessage>>,
227 /// `close` event: one-time transition; `dispatch_all` wakes every waiter.
228 close: Arc<EventRegistry<()>>,
229 /// `load` event: one-time transition; `dispatch_all` wakes every waiter.
230 load: Arc<EventRegistry<()>>,
231 /// `crash` event: one-time transition; `dispatch_all` wakes every waiter.
232 crash: Arc<EventRegistry<()>>,
233 /// `pageError` event: handlers and one-shot waiters.
234 pageerror: Arc<EventRegistry<String>>,
235 /// Popup event handlers and one-shot `expect_popup` waiters.
236 popup: Arc<EventRegistry<Page>>,
237 /// `frameAttached` event: handlers and one-shot waiters.
238 frameattached: Arc<EventRegistry<crate::protocol::Frame>>,
239 /// `frameDetached` event: handlers and one-shot waiters.
240 framedetached: Arc<EventRegistry<crate::protocol::Frame>>,
241 /// `frameNavigated` event: handlers and one-shot waiters.
242 framenavigated: Arc<EventRegistry<crate::protocol::Frame>>,
243 /// worker event handlers (fires when a web worker is created in the page)
244 worker_handlers: Arc<Mutex<Vec<WorkerHandler>>>,
245 /// One-shot senders waiting for the next "worker" event (expect_event("worker"))
246 worker_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::Worker>>>>,
247 /// Accumulated console messages received so far (appended by trigger_console_event)
248 console_messages_log: Arc<Mutex<Vec<crate::protocol::ConsoleMessage>>>,
249 /// Accumulated uncaught JS error messages received so far (appended by trigger_pageerror_event)
250 page_errors_log: Arc<Mutex<Vec<String>>>,
251 /// Active web workers tracked via "worker" events (appended on creation)
252 workers_list: Arc<Mutex<Vec<Worker>>>,
253 /// Video object — Some when this page was created in a record_video context.
254 /// The inner Video is created eagerly on Page construction; the underlying
255 /// Artifact GUID is read from the Page initializer and resolved asynchronously.
256 video: Option<crate::protocol::Video>,
257 /// Registered locator handlers: maps uid -> (selector, handler fn, times_remaining)
258 /// times_remaining is None when the handler should run indefinitely.
259 locator_handlers: Arc<Mutex<Vec<LocatorHandlerEntry>>>,
260}
261
262/// Type alias for boxed route handler future
263type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
264
265/// Type alias for boxed websocket handler future
266type WebSocketHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
267
268/// Type alias for boxed WebSocketRoute handler future
269type WebSocketRouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
270
271/// Storage for a single WebSocket route handler entry
272#[derive(Clone)]
273struct WsRouteHandlerEntry {
274 pattern: String,
275 handler:
276 Arc<dyn Fn(crate::protocol::WebSocketRoute) -> WebSocketRouteHandlerFuture + Send + Sync>,
277}
278
279/// Storage for a single route handler
280#[derive(Clone)]
281struct RouteHandlerEntry {
282 pattern: String,
283 handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
284}
285
286/// WebSocket event handler
287type WebSocketHandler = Arc<dyn Fn(WebSocket) -> WebSocketHandlerFuture + Send + Sync>;
288
289/// Type alias for boxed screencast frame handler future
290type ScreencastFrameHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
291
292/// Screencast frame handler
293type ScreencastFrameHandler =
294 Arc<dyn Fn(crate::protocol::ScreencastFrame) -> ScreencastFrameHandlerFuture + Send + Sync>;
295
296/// Type alias for boxed worker handler future
297type WorkerHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
298
299/// worker event handler — receives the new Worker
300type WorkerHandler = Arc<dyn Fn(crate::protocol::Worker) -> WorkerHandlerFuture + Send + Sync>;
301
302/// Type alias for boxed page-level binding callback future
303type PageBindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
304
305/// Page-level binding callback: receives deserialized JS args, returns a JSON value
306type PageBindingCallback =
307 Arc<dyn Fn(Vec<serde_json::Value>) -> PageBindingCallbackFuture + Send + Sync>;
308
309/// Type alias for boxed locator handler future
310type LocatorHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
311
312/// Locator handler callback: receives the matching Locator
313type LocatorHandlerFn = Arc<dyn Fn(crate::protocol::Locator) -> LocatorHandlerFuture + Send + Sync>;
314
315/// Entry in the locator handler registry
316struct LocatorHandlerEntry {
317 uid: u32,
318 selector: String,
319 handler: LocatorHandlerFn,
320 /// Remaining invocations; `None` means unlimited.
321 times_remaining: Option<u32>,
322}
323
324impl Page {
325 /// Creates a new Page from protocol initialization
326 ///
327 /// This is called by the object factory when the server sends a `__create__` message
328 /// for a Page object.
329 ///
330 /// # Arguments
331 ///
332 /// * `parent` - The parent BrowserContext object
333 /// * `type_name` - The protocol type name ("Page")
334 /// * `guid` - The unique identifier for this page
335 /// * `initializer` - The initialization data from the server
336 ///
337 /// # Errors
338 ///
339 /// Returns error if initializer is malformed
340 pub fn new(
341 parent: Arc<dyn ChannelOwner>,
342 type_name: String,
343 guid: Arc<str>,
344 initializer: Value,
345 main_frame: crate::protocol::Frame,
346 ) -> Result<Self> {
347 // Check the parent BrowserContext's initializer for record_video before
348 // moving `parent` into ChannelOwnerImpl. The Playwright server delivers
349 // the video artifact GUID directly in the Page initializer's "video" field.
350 let has_video = parent
351 .initializer()
352 .get("options")
353 .and_then(|opts| opts.get("recordVideo"))
354 .is_some();
355
356 let video_artifact_guid: Option<String> = initializer
357 .get("video")
358 .and_then(|v| v.get("guid"))
359 .and_then(|v| v.as_str())
360 .map(|s| s.to_string());
361
362 let base = ChannelOwnerImpl::new(
363 ParentOrConnection::Parent(parent),
364 type_name,
365 guid,
366 initializer,
367 );
368
369 // Initialize URL to about:blank
370
371 // Initialize empty route handlers
372 let route_handlers = Arc::new(Mutex::new(Vec::new()));
373
374 // Initialize empty event handlers
375 let websocket_handlers = Arc::new(Mutex::new(Vec::new()));
376 let ws_route_handlers = Arc::new(Mutex::new(Vec::new()));
377
378 // Initialize cached main frame as empty (will be populated on first access)
379
380 // Extract viewport from initializer (may be null for no_viewport contexts)
381 let initial_viewport: Option<Viewport> =
382 base.initializer().get("viewportSize").and_then(|v| {
383 if v.is_null() {
384 None
385 } else {
386 serde_json::from_value(v.clone()).ok()
387 }
388 });
389 let viewport = Arc::new(RwLock::new(initial_viewport));
390
391 let video = if has_video {
392 let v = crate::protocol::Video::new();
393 // Resolve the artifact from the initializer-provided GUID.
394 if let Some(artifact_guid) = video_artifact_guid {
395 let connection = base.connection();
396 let v_clone = v.clone();
397 tokio::spawn(
398 async move {
399 match connection.get_object(&artifact_guid).await {
400 Ok(artifact_arc) => v_clone.set_artifact(artifact_arc),
401 Err(e) => tracing::warn!(
402 "Failed to resolve video artifact {} from initializer: {}",
403 artifact_guid,
404 e
405 ),
406 }
407 }
408 .in_current_span(),
409 );
410 }
411 Some(v)
412 } else {
413 None
414 };
415
416 Ok(Self {
417 base,
418 main_frame,
419 route_handlers,
420 download: EventRegistry::new("download"),
421 dialog: EventRegistry::new("dialog"),
422 request: EventRegistry::new("request"),
423 request_finished: EventRegistry::new("requestFinished"),
424 request_failed: EventRegistry::new("requestFailed"),
425 response: EventRegistry::new("response"),
426 websocket_handlers,
427 ws_route_handlers,
428 viewport,
429 is_closed: Arc::new(AtomicBool::new(false)),
430 default_timeout_ms: Arc::new(AtomicU64::new(crate::DEFAULT_TIMEOUT_MS.to_bits())),
431 default_navigation_timeout_ms: Arc::new(AtomicU64::new(
432 crate::DEFAULT_TIMEOUT_MS.to_bits(),
433 )),
434 binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
435 screencast_frame_handlers: Arc::new(Mutex::new(Vec::new())),
436 screencast_artifact_guid: Arc::new(Mutex::new(None)),
437 screencast_save_path: Arc::new(Mutex::new(None)),
438 filechooser: EventRegistry::new("fileChooser"),
439 console: EventRegistry::new("console"),
440 close: EventRegistry::new("close"),
441 load: EventRegistry::new("load"),
442 crash: EventRegistry::new("crash"),
443 pageerror: EventRegistry::new("pageError"),
444 popup: EventRegistry::new("popup"),
445 frameattached: EventRegistry::new("frameAttached"),
446 framedetached: EventRegistry::new("frameDetached"),
447 framenavigated: EventRegistry::new("frameNavigated"),
448 worker_handlers: Arc::new(Mutex::new(Vec::new())),
449 worker_waiters: Arc::new(Mutex::new(Vec::new())),
450 console_messages_log: Arc::new(Mutex::new(Vec::new())),
451 page_errors_log: Arc::new(Mutex::new(Vec::new())),
452 workers_list: Arc::new(Mutex::new(Vec::new())),
453 video,
454 locator_handlers: Arc::new(Mutex::new(Vec::new())),
455 })
456 }
457
458 /// Returns the channel for sending protocol messages
459 ///
460 /// Used internally for sending RPC calls to the page.
461 fn channel(&self) -> &Channel {
462 self.base.channel()
463 }
464
465 /// Returns the main frame of the page.
466 ///
467 /// The main frame is where navigation and DOM operations actually happen.
468 ///
469 /// This method also wires up the back-reference from the frame to the page so that
470 /// `frame.page()`, `frame.locator()`, and `frame.get_by_*()` work correctly.
471 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
472 pub async fn main_frame(&self) -> Result<crate::protocol::Frame> {
473 Ok(self.main_frame_wired())
474 }
475
476 /// Waits until `expression` returns a truthy value in the main frame,
477 /// then resolves to its result as a [`JSHandle`](crate::protocol::JSHandle).
478 ///
479 /// Polls on `requestAnimationFrame` by default; set
480 /// [`WaitForFunctionOptions::polling_interval`](crate::protocol::WaitForFunctionOptions)
481 /// to poll on a timer instead, which is what you want for state the page
482 /// changes off-frame.
483 ///
484 /// # Errors
485 ///
486 /// Returns an error if the expression does not become truthy within the
487 /// timeout (default 30s), or if the page closes first. The timeout is
488 /// enforced by the driver, so it surfaces as a protocol error carrying
489 /// the driver's "Timeout ...ms exceeded" message.
490 ///
491 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-function>
492 pub async fn wait_for_function(
493 &self,
494 expression: &str,
495 options: impl Into<Option<crate::protocol::WaitForFunctionOptions>>,
496 ) -> Result<std::sync::Arc<crate::protocol::JSHandle>> {
497 // Resolve the page's configured default here: the frame only knows
498 // it through a back-reference that a bare Frame may not have.
499 let mut options = options.into().unwrap_or_default();
500 if options.timeout.is_none() {
501 options.timeout = Some(self.default_timeout_ms());
502 }
503 self.main_frame()
504 .await?
505 .wait_for_function(expression, options)
506 .await
507 }
508
509 /// Clone of the construction-time main frame with the page back-reference
510 /// wired, so `frame.page()` / `frame.locator()` work. Infallible: the
511 /// frame is resolved when the Page is created.
512 pub(crate) fn main_frame_wired(&self) -> crate::protocol::Frame {
513 let frame = self.main_frame.clone();
514 frame.set_page(self.clone());
515 frame
516 }
517
518 /// Returns the current URL of the page.
519 ///
520 /// This returns the last committed URL, including hash fragments from anchor navigation.
521 /// Initially, pages are at "about:blank".
522 ///
523 /// See: <https://playwright.dev/docs/api/class-page#page-url>
524 pub fn url(&self) -> String {
525 // The main frame is the source of truth for navigation, including
526 // hash fragments from anchor navigation.
527 self.main_frame.url()
528 }
529
530 /// Closes the page.
531 ///
532 /// This is a graceful operation that sends a close command to the page
533 /// and waits for it to shut down properly.
534 ///
535 /// # Errors
536 ///
537 /// Returns error if:
538 /// - Page has already been closed
539 /// - Communication with browser process fails
540 ///
541 /// See: <https://playwright.dev/docs/api/class-page#page-close>
542 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
543 pub async fn close(&self) -> Result<()> {
544 // Send close RPC to server
545 let result = self
546 .channel()
547 .send_no_result("close", serde_json::json!({}))
548 .await;
549 // Mark as closed regardless of error (best-effort)
550 self.is_closed.store(true, Ordering::Relaxed);
551 result
552 }
553
554 /// Returns whether the page has been closed.
555 ///
556 /// Returns `true` after `close()` has been called on this page, or after the
557 /// page receives a close event from the server (e.g. when the browser context
558 /// is closed).
559 ///
560 /// See: <https://playwright.dev/docs/api/class-page#page-is-closed>
561 pub fn is_closed(&self) -> bool {
562 self.is_closed.load(Ordering::Relaxed)
563 }
564
565 /// Returns all console messages received so far on this page.
566 ///
567 /// Messages are accumulated in order as they arrive via the `console` event.
568 /// Each call returns a snapshot; new messages arriving concurrently may or may not
569 /// be included depending on timing.
570 ///
571 /// To get a filtered subset, chain a standard iterator filter:
572 ///
573 /// ```no_run
574 /// # use playwright_rs::Playwright;
575 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
576 /// # let pw = Playwright::launch().await?;
577 /// # let browser = pw.chromium().launch().await?;
578 /// # let page = browser.new_page().await?;
579 /// let errors: Vec<_> = page
580 /// .console_messages()
581 /// .into_iter()
582 /// .filter(|m| m.type_() == "error")
583 /// .collect();
584 /// # Ok(())
585 /// # }
586 /// ```
587 ///
588 /// Use [`clear_console_messages`](Self::clear_console_messages) to drop
589 /// the accumulator (e.g. between test phases).
590 ///
591 /// See: <https://playwright.dev/docs/api/class-page#page-console-messages>
592 pub fn console_messages(&self) -> Vec<crate::protocol::ConsoleMessage> {
593 self.console_messages_log.lock().unwrap().clone()
594 }
595
596 /// Drops every console message accumulated so far. New messages arriving
597 /// after this call still get recorded; the accumulator just starts empty
598 /// again. Useful between test phases when you want to assert against
599 /// only messages from a specific phase.
600 ///
601 /// See: <https://playwright.dev/docs/api/class-page#page-clear-console-messages>
602 pub fn clear_console_messages(&self) {
603 self.console_messages_log.lock().unwrap().clear();
604 }
605
606 /// Returns all uncaught JavaScript error messages received so far on this page.
607 ///
608 /// Errors are accumulated in order as they arrive via the `pageError` event.
609 /// Each string is the `.message` field of the thrown `Error`.
610 ///
611 /// To get a filtered subset, chain a standard iterator filter:
612 ///
613 /// ```no_run
614 /// # use playwright_rs::Playwright;
615 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
616 /// # let pw = Playwright::launch().await?;
617 /// # let browser = pw.chromium().launch().await?;
618 /// # let page = browser.new_page().await?;
619 /// let typeerrors: Vec<_> = page
620 /// .page_errors()
621 /// .into_iter()
622 /// .filter(|e| e.starts_with("TypeError"))
623 /// .collect();
624 /// # Ok(())
625 /// # }
626 /// ```
627 ///
628 /// Use [`clear_page_errors`](Self::clear_page_errors) to drop the
629 /// accumulator (e.g. between test phases).
630 pub fn page_errors(&self) -> Vec<String> {
631 self.page_errors_log.lock().unwrap().clone()
632 }
633
634 /// Drops every page error accumulated so far. New errors arriving after
635 /// this call still get recorded.
636 ///
637 /// See: <https://playwright.dev/docs/api/class-page#page-clear-page-errors>
638 pub fn clear_page_errors(&self) {
639 self.page_errors_log.lock().unwrap().clear();
640 }
641
642 /// Returns the page that opened this popup, or `None` if this page was not opened
643 /// by another page.
644 ///
645 /// The opener is available from the page's initializer — it is the page that called
646 /// `window.open()` or triggered a link with `target="_blank"`. Returns `None` for
647 /// top-level pages that were not opened as popups.
648 ///
649 /// # Errors
650 ///
651 /// Returns error if the opener page GUID is present in the initializer but the
652 /// object is not found in the connection registry.
653 ///
654 /// See: <https://playwright.dev/docs/api/class-page#page-opener>
655 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
656 pub async fn opener(&self) -> Result<Option<Page>> {
657 // The opener guid is stored in the page initializer as {"opener": {"guid": "..."}}.
658 // It is set when the page is created as a popup; absent for non-popup pages.
659 let opener_guid = self
660 .base
661 .initializer()
662 .get("opener")
663 .and_then(|v| v.get("guid"))
664 .and_then(|v| v.as_str())
665 .map(|s| s.to_string());
666
667 match opener_guid {
668 None => Ok(None),
669 Some(guid) => {
670 let page = self.connection().get_typed::<Page>(&guid).await?;
671 Ok(Some(page))
672 }
673 }
674 }
675
676 /// Returns all active web workers belonging to this page.
677 ///
678 /// Workers are tracked as they are created (`worker` event) and this method
679 /// returns a snapshot of the current list.
680 ///
681 /// See: <https://playwright.dev/docs/api/class-page#page-workers>
682 pub fn workers(&self) -> Vec<Worker> {
683 self.workers_list.lock().unwrap().clone()
684 }
685
686 /// Sets the default timeout for all operations on this page.
687 ///
688 /// The timeout applies to actions such as `click`, `fill`, `locator.wait_for`, etc.
689 /// Pass `0` to disable timeouts.
690 ///
691 /// This stores the value locally so that subsequent action calls use it when
692 /// no explicit timeout is provided, and also notifies the Playwright server
693 /// so it can apply the same default on its side.
694 ///
695 /// # Arguments
696 ///
697 /// * `timeout` - Timeout in milliseconds
698 ///
699 /// See: <https://playwright.dev/docs/api/class-page#page-set-default-timeout>
700 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
701 pub async fn set_default_timeout(&self, timeout: f64) {
702 self.default_timeout_ms
703 .store(timeout.to_bits(), Ordering::Relaxed);
704 set_timeout_and_notify(self.channel(), "setDefaultTimeoutNoReply", timeout).await;
705 }
706
707 /// Sets the default timeout for navigation operations on this page.
708 ///
709 /// The timeout applies to navigation actions such as `goto`, `reload`,
710 /// `go_back`, and `go_forward`. Pass `0` to disable timeouts.
711 ///
712 /// # Arguments
713 ///
714 /// * `timeout` - Timeout in milliseconds
715 ///
716 /// See: <https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout>
717 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
718 pub async fn set_default_navigation_timeout(&self, timeout: f64) {
719 self.default_navigation_timeout_ms
720 .store(timeout.to_bits(), Ordering::Relaxed);
721 set_timeout_and_notify(
722 self.channel(),
723 "setDefaultNavigationTimeoutNoReply",
724 timeout,
725 )
726 .await;
727 }
728
729 /// Returns the current default action timeout in milliseconds.
730 pub fn default_timeout_ms(&self) -> f64 {
731 f64::from_bits(self.default_timeout_ms.load(Ordering::Relaxed))
732 }
733
734 /// Returns the current default navigation timeout in milliseconds.
735 pub fn default_navigation_timeout_ms(&self) -> f64 {
736 f64::from_bits(self.default_navigation_timeout_ms.load(Ordering::Relaxed))
737 }
738
739 /// Returns GotoOptions with the navigation timeout filled in if not already set.
740 ///
741 /// Used internally to ensure the page's configured default navigation timeout
742 /// is used when the caller does not provide an explicit timeout.
743 fn with_navigation_timeout(&self, options: Option<GotoOptions>) -> GotoOptions {
744 let nav_timeout = self.default_navigation_timeout_ms();
745 match options {
746 Some(opts) if opts.timeout.is_some() => opts,
747 Some(mut opts) => {
748 opts.timeout = Some(std::time::Duration::from_millis(nav_timeout as u64));
749 opts
750 }
751 None => GotoOptions {
752 timeout: Some(std::time::Duration::from_millis(nav_timeout as u64)),
753 wait_until: None,
754 },
755 }
756 }
757
758 /// Returns all frames in the page, including the main frame.
759 ///
760 /// Currently returns only the main (top-level) frame. Iframe enumeration
761 /// is not yet implemented and will be added in a future release.
762 ///
763 /// # Errors
764 ///
765 /// Returns error if:
766 /// - Page has been closed
767 /// - Communication with browser process fails
768 ///
769 /// See: <https://playwright.dev/docs/api/class-page#page-frames>
770 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
771 pub async fn frames(&self) -> Result<Vec<crate::protocol::Frame>> {
772 // Start with the main frame
773 let main = self.main_frame().await?;
774 Ok(vec![main])
775 }
776
777 /// Navigates to the specified URL.
778 ///
779 /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
780 /// about:blank). This matches Playwright's behavior across all language bindings.
781 ///
782 /// # Arguments
783 ///
784 /// * `url` - The URL to navigate to
785 /// * `options` - Optional navigation options (timeout, wait_until)
786 ///
787 /// # Errors
788 ///
789 /// Returns error if:
790 /// - URL is invalid
791 /// - Navigation timeout (default 30s)
792 /// - Network error
793 ///
794 /// See: <https://playwright.dev/docs/api/class-page#page-goto>
795 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), url = %url, status = tracing::field::Empty))]
796 pub async fn goto(
797 &self,
798 url: &str,
799 options: impl Into<Option<GotoOptions>>,
800 ) -> Result<Option<Response>> {
801 let options = options.into();
802 // Inject the page-level navigation timeout when no explicit timeout is given
803 let options = self.with_navigation_timeout(options);
804
805 // Delegate to main frame
806 let frame = self.main_frame().await.map_err(|e| match e {
807 Error::TargetClosed { context, .. } => Error::TargetClosed {
808 target_type: "Page".to_string(),
809 context,
810 },
811 other => other,
812 })?;
813
814 let response = frame.goto(url, Some(options)).await.map_err(|e| match e {
815 Error::TargetClosed { context, .. } => Error::TargetClosed {
816 target_type: "Page".to_string(),
817 context,
818 },
819 other => other,
820 })?;
821
822 if let Some(ref resp) = response {
823 tracing::Span::current().record("status", resp.status());
824 }
825 Ok(response)
826 }
827
828 /// Returns the browser context that the page belongs to.
829 pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
830 downcast_parent::<crate::protocol::BrowserContext>(self)
831 .ok_or_else(|| Error::ProtocolError("Page parent is not a BrowserContext".to_string()))
832 }
833
834 /// Returns the Clock object for this page's browser context.
835 ///
836 /// This is a convenience accessor that delegates to the parent context's clock.
837 /// All clock RPCs are sent on the BrowserContext channel regardless of whether
838 /// the Clock is obtained via `page.clock()` or `context.clock()`.
839 ///
840 /// # Errors
841 ///
842 /// Returns error if the page's parent is not a BrowserContext.
843 ///
844 /// See: <https://playwright.dev/docs/api/class-clock>
845 pub fn clock(&self) -> Result<crate::protocol::clock::Clock> {
846 Ok(self.context()?.clock())
847 }
848
849 /// Returns the `Video` object associated with this page, if video recording is enabled.
850 ///
851 /// Returns `Some(Video)` when the browser context was created with the `record_video`
852 /// option; returns `None` otherwise.
853 ///
854 /// The `Video` shell is created eagerly. The underlying recording artifact is wired
855 /// up when the Playwright server fires the internal `"video"` event (which typically
856 /// happens when the page is first navigated). Calling [`crate::protocol::Video::save_as`] or
857 /// [`crate::protocol::Video::path`] before the artifact arrives returns an error; close the page
858 /// first to guarantee the artifact is ready.
859 ///
860 /// See: <https://playwright.dev/docs/api/class-page#page-video>
861 pub fn video(&self) -> Option<crate::protocol::Video> {
862 self.video.clone()
863 }
864
865 /// Pauses script execution.
866 ///
867 /// Playwright will stop executing the script and wait for the user to either press
868 /// "Resume" in the page overlay or in the debugger.
869 ///
870 /// See: <https://playwright.dev/docs/api/class-page#page-pause>
871 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
872 pub async fn pause(&self) -> Result<()> {
873 self.context()?.pause().await
874 }
875
876 /// Returns the page's title.
877 ///
878 /// See: <https://playwright.dev/docs/api/class-page#page-title>
879 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
880 pub async fn title(&self) -> Result<String> {
881 // Delegate to main frame
882 let frame = self.main_frame().await?;
883 frame.title().await
884 }
885
886 /// Returns the full HTML content of the page, including the DOCTYPE.
887 ///
888 /// This method retrieves the complete HTML markup of the page,
889 /// including the doctype declaration and all DOM elements.
890 ///
891 /// See: <https://playwright.dev/docs/api/class-page#page-content>
892 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
893 pub async fn content(&self) -> Result<String> {
894 // Delegate to main frame
895 let frame = self.main_frame().await?;
896 frame.content().await
897 }
898
899 /// Sets the content of the page.
900 ///
901 /// See: <https://playwright.dev/docs/api/class-page#page-set-content>
902 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
903 pub async fn set_content(
904 &self,
905 html: &str,
906 options: impl Into<Option<GotoOptions>>,
907 ) -> Result<()> {
908 let options = options.into();
909 let frame = self.main_frame().await?;
910 frame.set_content(html, options).await
911 }
912
913 /// Waits for the required load state to be reached.
914 ///
915 /// This resolves when the page reaches a required load state, `load` by default.
916 /// The navigation must have been committed when this method is called. If the current
917 /// document has already reached the required state, resolves immediately.
918 ///
919 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-load-state>
920 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
921 pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
922 let frame = self.main_frame().await?;
923 frame.wait_for_load_state(state).await
924 }
925
926 /// Waits for the main frame to navigate to a URL matching the given string or glob pattern.
927 ///
928 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-url>
929 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %url))]
930 pub async fn wait_for_url(
931 &self,
932 url: &str,
933 options: impl Into<Option<GotoOptions>>,
934 ) -> Result<()> {
935 let options = options.into();
936 let frame = self.main_frame().await?;
937 frame.wait_for_url(url, options).await
938 }
939
940 /// Replace the URL fragment without firing a navigation.
941 ///
942 /// Wraps `history.replaceState(null, '', <pathname+search+#hash>)`.
943 /// A leading `#` on `hash` is optional — both `"foo"` and `"#foo"`
944 /// produce the same result.
945 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
946 pub async fn set_url_fragment(&self, hash: &str) -> Result<()> {
947 let normalized = if hash.starts_with('#') {
948 hash.to_string()
949 } else {
950 format!("#{hash}")
951 };
952 // JSON-encode so quotes / backslashes / control chars in `hash`
953 // don't break the surrounding JS string literal.
954 let json = serde_json::to_string(&normalized).map_err(|e| {
955 crate::error::Error::ProtocolError(format!("serialize url fragment: {e}"))
956 })?;
957 let js =
958 format!("history.replaceState(null, '', location.pathname + location.search + {json})");
959 self.evaluate_expression(&js).await
960 }
961
962 /// Clear the URL fragment without firing a navigation.
963 ///
964 /// Wraps `history.replaceState(null, '', <pathname+search>)`,
965 /// stripping any trailing `#...`. Pairs with
966 /// [`set_url_fragment`](Self::set_url_fragment).
967 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
968 pub async fn clear_url_fragment(&self) -> Result<()> {
969 self.evaluate_expression(
970 "history.replaceState(null, '', location.pathname + location.search)",
971 )
972 .await
973 }
974
975 /// Creates a locator for finding elements on the page.
976 ///
977 /// Locators are the central piece of Playwright's auto-waiting and retry-ability.
978 /// They don't execute queries until an action is performed.
979 ///
980 /// # Arguments
981 ///
982 /// * `selector` - CSS selector or other locating strategy
983 ///
984 /// See: <https://playwright.dev/docs/api/class-page#page-locator>
985 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), selector = tracing::field::Empty))]
986 pub fn locator(&self, selector: impl Into<String>) -> crate::protocol::Locator {
987 let selector = selector.into();
988 tracing::Span::current().record("selector", selector.as_str());
989 let frame = self.main_frame_wired();
990
991 crate::protocol::Locator::new(Arc::new(frame), selector, self.clone())
992 }
993
994 /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) for an iframe on this page.
995 ///
996 /// The `selector` identifies the iframe element (e.g., `"iframe[name='content']"`).
997 ///
998 /// See: <https://playwright.dev/docs/api/class-page#page-frame-locator>
999 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), selector = %selector))]
1000 pub fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
1001 let frame = self.main_frame_wired();
1002 crate::protocol::FrameLocator::new(Arc::new(frame), selector.to_string(), self.clone())
1003 }
1004
1005 /// Returns a locator that matches elements containing the given text.
1006 ///
1007 /// By default, matching is case-insensitive and searches for a substring.
1008 /// Set `exact` to `true` for case-sensitive exact matching.
1009 ///
1010 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-text>
1011 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1012 pub fn get_by_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
1013 self.locator(crate::protocol::locator::get_by_text_selector(text, exact))
1014 }
1015
1016 /// Returns a locator that matches elements by their associated label text.
1017 ///
1018 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-label>
1019 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1020 pub fn get_by_label(&self, text: &str, exact: bool) -> crate::protocol::Locator {
1021 self.locator(crate::protocol::locator::get_by_label_selector(text, exact))
1022 }
1023
1024 /// Returns a locator that matches elements by their placeholder text.
1025 ///
1026 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-placeholder>
1027 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1028 pub fn get_by_placeholder(&self, text: &str, exact: bool) -> crate::protocol::Locator {
1029 self.locator(crate::protocol::locator::get_by_placeholder_selector(
1030 text, exact,
1031 ))
1032 }
1033
1034 /// Returns a locator that matches elements by their alt text.
1035 ///
1036 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-alt-text>
1037 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1038 pub fn get_by_alt_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
1039 self.locator(crate::protocol::locator::get_by_alt_text_selector(
1040 text, exact,
1041 ))
1042 }
1043
1044 /// Returns a locator that matches elements by their title attribute.
1045 ///
1046 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-title>
1047 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1048 pub fn get_by_title(&self, text: &str, exact: bool) -> crate::protocol::Locator {
1049 self.locator(crate::protocol::locator::get_by_title_selector(text, exact))
1050 }
1051
1052 /// Returns a locator that matches elements by their test ID attribute.
1053 ///
1054 /// By default, uses the `data-testid` attribute. Call
1055 /// [`playwright.selectors().set_test_id_attribute()`](crate::protocol::Selectors::set_test_id_attribute)
1056 /// to change the attribute name.
1057 ///
1058 /// Always uses exact matching (case-sensitive).
1059 ///
1060 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-test-id>
1061 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1062 pub fn get_by_test_id(&self, test_id: &str) -> crate::protocol::Locator {
1063 let attr = self.connection().selectors().test_id_attribute();
1064 self.locator(crate::protocol::locator::get_by_test_id_selector_with_attr(
1065 test_id, &attr,
1066 ))
1067 }
1068
1069 /// Returns a locator that matches elements by their ARIA role.
1070 ///
1071 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
1072 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1073 pub fn get_by_role(
1074 &self,
1075 role: crate::protocol::locator::AriaRole,
1076 options: Option<crate::protocol::locator::GetByRoleOptions>,
1077 ) -> crate::protocol::Locator {
1078 self.locator(crate::protocol::locator::get_by_role_selector(
1079 role, options,
1080 ))
1081 }
1082
1083 /// Returns the keyboard instance for low-level keyboard control.
1084 ///
1085 /// See: <https://playwright.dev/docs/api/class-page#page-keyboard>
1086 pub fn keyboard(&self) -> crate::protocol::Keyboard {
1087 crate::protocol::Keyboard::new(self.clone())
1088 }
1089
1090 /// Returns the mouse instance for low-level mouse control.
1091 ///
1092 /// See: <https://playwright.dev/docs/api/class-page#page-mouse>
1093 pub fn mouse(&self) -> crate::protocol::Mouse {
1094 crate::protocol::Mouse::new(self.clone())
1095 }
1096
1097 // Internal keyboard methods (called by Keyboard struct)
1098
1099 pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
1100 self.channel()
1101 .send_no_result(
1102 "keyboardDown",
1103 serde_json::json!({
1104 "key": key
1105 }),
1106 )
1107 .await
1108 }
1109
1110 pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
1111 self.channel()
1112 .send_no_result(
1113 "keyboardUp",
1114 serde_json::json!({
1115 "key": key
1116 }),
1117 )
1118 .await
1119 }
1120
1121 pub(crate) async fn keyboard_press(
1122 &self,
1123 key: &str,
1124 options: Option<crate::protocol::KeyboardOptions>,
1125 ) -> Result<()> {
1126 let mut params = serde_json::json!({
1127 "key": key
1128 });
1129
1130 if let Some(opts) = options {
1131 let opts_json = opts.to_json();
1132 if let Some(obj) = params.as_object_mut()
1133 && let Some(opts_obj) = opts_json.as_object()
1134 {
1135 obj.extend(opts_obj.clone());
1136 }
1137 }
1138
1139 self.channel().send_no_result("keyboardPress", params).await
1140 }
1141
1142 pub(crate) async fn keyboard_type(
1143 &self,
1144 text: &str,
1145 options: Option<crate::protocol::KeyboardOptions>,
1146 ) -> Result<()> {
1147 let mut params = serde_json::json!({
1148 "text": text
1149 });
1150
1151 if let Some(opts) = options {
1152 let opts_json = opts.to_json();
1153 if let Some(obj) = params.as_object_mut()
1154 && let Some(opts_obj) = opts_json.as_object()
1155 {
1156 obj.extend(opts_obj.clone());
1157 }
1158 }
1159
1160 self.channel().send_no_result("keyboardType", params).await
1161 }
1162
1163 pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
1164 self.channel()
1165 .send_no_result(
1166 "keyboardInsertText",
1167 serde_json::json!({
1168 "text": text
1169 }),
1170 )
1171 .await
1172 }
1173
1174 // Internal mouse methods (called by Mouse struct)
1175
1176 pub(crate) async fn mouse_move(
1177 &self,
1178 x: f64,
1179 y: f64,
1180 options: Option<crate::protocol::MouseOptions>,
1181 ) -> Result<()> {
1182 let mut params = serde_json::json!({
1183 "x": x,
1184 "y": y
1185 });
1186
1187 if let Some(opts) = options {
1188 let opts_json = opts.to_json();
1189 if let Some(obj) = params.as_object_mut()
1190 && let Some(opts_obj) = opts_json.as_object()
1191 {
1192 obj.extend(opts_obj.clone());
1193 }
1194 }
1195
1196 self.channel().send_no_result("mouseMove", params).await
1197 }
1198
1199 pub(crate) async fn mouse_click(
1200 &self,
1201 x: f64,
1202 y: f64,
1203 options: Option<crate::protocol::MouseOptions>,
1204 ) -> Result<()> {
1205 let mut params = serde_json::json!({
1206 "x": x,
1207 "y": y
1208 });
1209
1210 if let Some(opts) = options {
1211 let opts_json = opts.to_json();
1212 if let Some(obj) = params.as_object_mut()
1213 && let Some(opts_obj) = opts_json.as_object()
1214 {
1215 obj.extend(opts_obj.clone());
1216 }
1217 }
1218
1219 self.channel().send_no_result("mouseClick", params).await
1220 }
1221
1222 pub(crate) async fn mouse_dblclick(
1223 &self,
1224 x: f64,
1225 y: f64,
1226 options: Option<crate::protocol::MouseOptions>,
1227 ) -> Result<()> {
1228 let mut params = serde_json::json!({
1229 "x": x,
1230 "y": y,
1231 "clickCount": 2
1232 });
1233
1234 if let Some(opts) = options {
1235 let opts_json = opts.to_json();
1236 if let Some(obj) = params.as_object_mut()
1237 && let Some(opts_obj) = opts_json.as_object()
1238 {
1239 obj.extend(opts_obj.clone());
1240 }
1241 }
1242
1243 self.channel().send_no_result("mouseClick", params).await
1244 }
1245
1246 pub(crate) async fn mouse_down(
1247 &self,
1248 options: Option<crate::protocol::MouseOptions>,
1249 ) -> Result<()> {
1250 let mut params = serde_json::json!({});
1251
1252 if let Some(opts) = options {
1253 let opts_json = opts.to_json();
1254 if let Some(obj) = params.as_object_mut()
1255 && let Some(opts_obj) = opts_json.as_object()
1256 {
1257 obj.extend(opts_obj.clone());
1258 }
1259 }
1260
1261 self.channel().send_no_result("mouseDown", params).await
1262 }
1263
1264 pub(crate) async fn mouse_up(
1265 &self,
1266 options: Option<crate::protocol::MouseOptions>,
1267 ) -> Result<()> {
1268 let mut params = serde_json::json!({});
1269
1270 if let Some(opts) = options {
1271 let opts_json = opts.to_json();
1272 if let Some(obj) = params.as_object_mut()
1273 && let Some(opts_obj) = opts_json.as_object()
1274 {
1275 obj.extend(opts_obj.clone());
1276 }
1277 }
1278
1279 self.channel().send_no_result("mouseUp", params).await
1280 }
1281
1282 pub(crate) async fn mouse_wheel(&self, delta_x: f64, delta_y: f64) -> Result<()> {
1283 self.channel()
1284 .send_no_result(
1285 "mouseWheel",
1286 serde_json::json!({
1287 "deltaX": delta_x,
1288 "deltaY": delta_y
1289 }),
1290 )
1291 .await
1292 }
1293
1294 // Internal touchscreen method (called by Touchscreen struct)
1295
1296 pub(crate) async fn touchscreen_tap(&self, x: f64, y: f64) -> Result<()> {
1297 self.channel()
1298 .send_no_result(
1299 "touchscreenTap",
1300 serde_json::json!({
1301 "x": x,
1302 "y": y
1303 }),
1304 )
1305 .await
1306 }
1307
1308 /// Returns the touchscreen instance for low-level touch input simulation.
1309 ///
1310 /// Requires a touch-enabled browser context (`has_touch: true` in
1311 /// [`BrowserContextOptions`](crate::protocol::browser_context::BrowserContext)).
1312 ///
1313 /// See: <https://playwright.dev/docs/api/class-page#page-touchscreen>
1314 pub fn touchscreen(&self) -> crate::protocol::Touchscreen {
1315 crate::protocol::Touchscreen::new(self.clone())
1316 }
1317
1318 /// Performs a drag from source selector to target selector.
1319 ///
1320 /// This is the page-level equivalent of `Locator::drag_to()`. It resolves
1321 /// both selectors in the main frame and performs the drag.
1322 ///
1323 /// # Arguments
1324 ///
1325 /// * `source` - A CSS selector for the element to drag from
1326 /// * `target` - A CSS selector for the element to drop onto
1327 /// * `options` - Optional drag options (positions, force, timeout, trial)
1328 ///
1329 /// # Errors
1330 ///
1331 /// Returns error if either selector does not resolve to an element, the
1332 /// drag action times out, or the page has been closed.
1333 ///
1334 /// See: <https://playwright.dev/docs/api/class-page#page-drag-and-drop>
1335 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1336 pub async fn drag_and_drop(
1337 &self,
1338 source: &str,
1339 target: &str,
1340 options: impl Into<Option<crate::protocol::DragToOptions>>,
1341 ) -> Result<()> {
1342 let options = options.into();
1343 let frame = self.main_frame().await?;
1344 frame.locator_drag_to(source, target, options).await
1345 }
1346
1347 /// Reloads the current page.
1348 ///
1349 /// # Arguments
1350 ///
1351 /// * `options` - Optional reload options (timeout, wait_until)
1352 ///
1353 /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
1354 /// about:blank). This matches Playwright's behavior across all language bindings.
1355 ///
1356 /// See: <https://playwright.dev/docs/api/class-page#page-reload>
1357 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1358 pub async fn reload(
1359 &self,
1360 options: impl Into<Option<GotoOptions>>,
1361 ) -> Result<Option<Response>> {
1362 let options = options.into();
1363 self.navigate_history("reload", options).await
1364 }
1365
1366 /// Navigates to the previous page in history.
1367 ///
1368 /// Returns the main resource response. In case of multiple server redirects, the navigation
1369 /// will resolve with the response of the last redirect. If can not go back, returns `None`.
1370 ///
1371 /// See: <https://playwright.dev/docs/api/class-page#page-go-back>
1372 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1373 pub async fn go_back(
1374 &self,
1375 options: impl Into<Option<GotoOptions>>,
1376 ) -> Result<Option<Response>> {
1377 let options = options.into();
1378 self.navigate_history("goBack", options).await
1379 }
1380
1381 /// Navigates to the next page in history.
1382 ///
1383 /// Returns the main resource response. In case of multiple server redirects, the navigation
1384 /// will resolve with the response of the last redirect. If can not go forward, returns `None`.
1385 ///
1386 /// See: <https://playwright.dev/docs/api/class-page#page-go-forward>
1387 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1388 pub async fn go_forward(
1389 &self,
1390 options: impl Into<Option<GotoOptions>>,
1391 ) -> Result<Option<Response>> {
1392 let options = options.into();
1393 self.navigate_history("goForward", options).await
1394 }
1395
1396 /// Shared implementation for reload, go_back and go_forward.
1397 async fn navigate_history(
1398 &self,
1399 method: &str,
1400 options: Option<GotoOptions>,
1401 ) -> Result<Option<Response>> {
1402 // Inject the page-level navigation timeout when no explicit timeout is given
1403 let opts = self.with_navigation_timeout(options);
1404 let mut params = serde_json::json!({});
1405
1406 // opts.timeout is always Some(...) because with_navigation_timeout guarantees it
1407 if let Some(timeout) = opts.timeout {
1408 params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
1409 } else {
1410 params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1411 }
1412 if let Some(wait_until) = opts.wait_until {
1413 params["waitUntil"] = serde_json::json!(wait_until.as_str());
1414 }
1415
1416 #[derive(Deserialize)]
1417 struct NavigationResponse {
1418 response: Option<ResponseReference>,
1419 }
1420
1421 #[derive(Deserialize)]
1422 struct ResponseReference {
1423 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
1424 guid: Arc<str>,
1425 }
1426
1427 let result: NavigationResponse = self.channel().send(method, params).await?;
1428
1429 if let Some(response_ref) = result.response {
1430 // The Response's __create__ may arrive just after the response.
1431 let response_arc = self
1432 .connection()
1433 .wait_for_object(&response_ref.guid)
1434 .await?;
1435
1436 let initializer = response_arc.initializer();
1437
1438 let status = initializer["status"].as_u64().ok_or_else(|| {
1439 crate::error::Error::ProtocolError("Response missing status".to_string())
1440 })? as u16;
1441
1442 let headers = initializer["headers"]
1443 .as_array()
1444 .ok_or_else(|| {
1445 crate::error::Error::ProtocolError("Response missing headers".to_string())
1446 })?
1447 .iter()
1448 .filter_map(|h| {
1449 let name = h["name"].as_str()?;
1450 let value = h["value"].as_str()?;
1451 Some((name.to_string(), value.to_string()))
1452 })
1453 .collect();
1454
1455 let response = Response::new(
1456 initializer["url"]
1457 .as_str()
1458 .ok_or_else(|| {
1459 crate::error::Error::ProtocolError("Response missing url".to_string())
1460 })?
1461 .to_string(),
1462 status,
1463 initializer["statusText"].as_str().unwrap_or("").to_string(),
1464 headers,
1465 Some(response_arc),
1466 );
1467
1468 Ok(Some(response))
1469 } else {
1470 Ok(None)
1471 }
1472 }
1473
1474 /// Returns the first element matching the selector, or None if not found.
1475 ///
1476 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
1477 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1478 pub async fn query_selector(
1479 &self,
1480 selector: &str,
1481 ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
1482 let frame = self.main_frame().await?;
1483 frame.query_selector(selector).await
1484 }
1485
1486 /// Returns all elements matching the selector.
1487 ///
1488 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
1489 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1490 pub async fn query_selector_all(
1491 &self,
1492 selector: &str,
1493 ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
1494 let frame = self.main_frame().await?;
1495 frame.query_selector_all(selector).await
1496 }
1497
1498 /// Takes a screenshot of the page and returns the image bytes.
1499 ///
1500 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1501 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), bytes_len = tracing::field::Empty))]
1502 pub async fn screenshot(
1503 &self,
1504 options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
1505 ) -> Result<Vec<u8>> {
1506 let options = options.into();
1507 let params = if let Some(opts) = options {
1508 opts.to_json()
1509 } else {
1510 // Default to PNG with required timeout
1511 serde_json::json!({
1512 "type": "png",
1513 "timeout": crate::DEFAULT_TIMEOUT_MS
1514 })
1515 };
1516
1517 #[derive(Deserialize)]
1518 struct ScreenshotResponse {
1519 binary: String,
1520 }
1521
1522 let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
1523
1524 // Decode base64 to bytes
1525 let bytes = base64::prelude::BASE64_STANDARD
1526 .decode(&response.binary)
1527 .map_err(|e| {
1528 crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
1529 })?;
1530
1531 tracing::Span::current().record("bytes_len", bytes.len());
1532 Ok(bytes)
1533 }
1534
1535 /// Takes a screenshot and saves it to a file, also returning the bytes.
1536 ///
1537 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1538 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1539 pub async fn screenshot_to_file(
1540 &self,
1541 path: &std::path::Path,
1542 options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
1543 ) -> Result<Vec<u8>> {
1544 let options = options.into();
1545 // Get the screenshot bytes
1546 let bytes = self.screenshot(options).await?;
1547
1548 // Write to file
1549 tokio::fs::write(path, &bytes).await.map_err(|e| {
1550 crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
1551 })?;
1552
1553 Ok(bytes)
1554 }
1555
1556 /// Evaluates JavaScript in the page context (without return value).
1557 ///
1558 /// Executes the provided JavaScript expression or function within the page's
1559 /// context without returning a value.
1560 ///
1561 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1562 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1563 pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
1564 // Delegate to the main frame
1565 let frame = self.main_frame().await?;
1566 frame.frame_evaluate_expression(expression).await
1567 }
1568
1569 /// Evaluates JavaScript in the page context with optional arguments,
1570 /// deserializing the result into any `DeserializeOwned` type.
1571 ///
1572 /// This is the right method whenever a test needs structured data out of
1573 /// the page: define a struct for the shape the JS returns and let serde do
1574 /// the parsing. Reaching for [`evaluate_value`](Self::evaluate_value) and
1575 /// string-parsing its output is never necessary.
1576 ///
1577 /// # Arguments
1578 ///
1579 /// * `expression` - JavaScript code to evaluate
1580 /// * `arg` - Optional argument to pass to the expression (must implement
1581 /// Serialize). With no argument, name the type: `None::<&()>`.
1582 ///
1583 /// # Example
1584 ///
1585 /// ```no_run
1586 /// # use playwright_rs::Playwright;
1587 /// # #[derive(serde::Deserialize)]
1588 /// # struct Metrics { width: f64, height: f64, title: String }
1589 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1590 /// # let pw = Playwright::launch().await?;
1591 /// # let page = pw.chromium().launch().await?.new_page().await?;
1592 /// let metrics: Metrics = page
1593 /// .evaluate(
1594 /// "() => ({ width: innerWidth, height: innerHeight, title: document.title })",
1595 /// None::<&()>,
1596 /// )
1597 /// .await?;
1598 /// assert!(!metrics.title.is_empty());
1599 /// # Ok(())
1600 /// # }
1601 /// ```
1602 ///
1603 /// A runnable walkthrough (structs in and out, element geometry) lives in
1604 /// `examples/evaluate_typed.rs`.
1605 ///
1606 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1607 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1608 pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
1609 &self,
1610 expression: &str,
1611 arg: Option<&T>,
1612 ) -> Result<U> {
1613 // Delegate to the main frame
1614 let frame = self.main_frame().await?;
1615 let result = frame.evaluate(expression, arg).await?;
1616 serde_json::from_value(result).map_err(Error::from)
1617 }
1618
1619 /// Evaluates a JavaScript expression and returns the result coerced to a
1620 /// String.
1621 ///
1622 /// Convenient for one-off scalar probes (`document.title`, a count, a
1623 /// flag). For anything structured, prefer [`evaluate`](Self::evaluate),
1624 /// which deserializes straight into your own type; returning delimited
1625 /// strings from JS and splitting them in Rust is a smell that `evaluate`
1626 /// removes.
1627 ///
1628 /// # Arguments
1629 ///
1630 /// * `expression` - JavaScript code to evaluate
1631 ///
1632 /// # Returns
1633 ///
1634 /// The result converted to a String
1635 ///
1636 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1637 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1638 pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
1639 let frame = self.main_frame().await?;
1640 frame.frame_evaluate_expression_value(expression).await
1641 }
1642
1643 /// Evaluates `expression` with a Rust closure bound as its argument.
1644 ///
1645 /// The closure arrives in JavaScript as an async function: calling it
1646 /// routes the arguments back to Rust, awaits the closure, and resolves
1647 /// with its return value. It is not installed on `window`; the expression
1648 /// receives it as its argument and decides what to do with it.
1649 ///
1650 /// This is the Rust shape of upstream's function-valued evaluate
1651 /// arguments. JavaScript callers pass a closure directly; Rust has no
1652 /// function value that can travel inside serialized data, so the
1653 /// callback is a dedicated parameter instead — the capability is the
1654 /// same, the composition point is the method signature.
1655 ///
1656 /// Each call registers a binding that lives until the page closes, which
1657 /// is what lets the expression stash the function and call it later
1658 /// (e.g. from an event listener). The cost is that the binding is never
1659 /// reclaimed earlier: calling this in a tight loop against a long-lived
1660 /// page accretes one binding per call. Register once and stash when you
1661 /// need repetition.
1662 ///
1663 /// # Example
1664 ///
1665 /// ```no_run
1666 /// # use playwright_rs::protocol::Playwright;
1667 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1668 /// # let pw = Playwright::launch().await?;
1669 /// # let browser = pw.chromium().launch().await?;
1670 /// # let page = browser.new_page().await?;
1671 /// let sum: i64 = page
1672 /// .evaluate_with_callback("async cb => await cb(20, 22)", |args| async move {
1673 /// let a = args[0].as_i64().unwrap_or(0);
1674 /// let b = args[1].as_i64().unwrap_or(0);
1675 /// serde_json::json!(a + b)
1676 /// })
1677 /// .await?;
1678 /// assert_eq!(sum, 42);
1679 /// # Ok(())
1680 /// # }
1681 /// ```
1682 ///
1683 /// # Errors
1684 ///
1685 /// Returns an error if the expression throws, if the page or its context
1686 /// has closed, or if the result does not deserialize into `U`.
1687 ///
1688 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1689 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
1690 pub async fn evaluate_with_callback<U, F, Fut>(
1691 &self,
1692 expression: &str,
1693 callback: F,
1694 ) -> Result<U>
1695 where
1696 U: serde::de::DeserializeOwned,
1697 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1698 Fut: Future<Output = serde_json::Value> + Send + 'static,
1699 {
1700 use std::sync::atomic::{AtomicU64, Ordering};
1701 // The `__pw_fn_` prefix matches upstream's kFunctionBindingPrefix
1702 // and is load-bearing: the server-to-page serializer only carries a
1703 // function value whose binding name starts with it, so a rename off
1704 // the prefix would make the argument deserialize as `undefined` in
1705 // the page.
1706 static CALLBACK_SEQ: AtomicU64 = AtomicU64::new(0);
1707 let name = format!(
1708 "__pw_fn_rs_{}",
1709 CALLBACK_SEQ.fetch_add(1, Ordering::Relaxed)
1710 );
1711
1712 self.expose_binding_internal(&name, true, callback).await?;
1713
1714 let frame = self.main_frame().await?;
1715 let result = frame.evaluate_with_fn_arg(expression, &name).await?;
1716 serde_json::from_value(result).map_err(Error::from)
1717 }
1718
1719 /// Registers a route handler for network interception.
1720 ///
1721 /// When a request matches the specified pattern, the handler will be called
1722 /// with a Route object that can abort, continue, or fulfill the request.
1723 ///
1724 /// # Arguments
1725 ///
1726 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
1727 /// * `handler` - Async closure that handles the route
1728 ///
1729 /// See: <https://playwright.dev/docs/api/class-page#page-route>
1730 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
1731 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1732 where
1733 F: Fn(Route) -> Fut + Send + Sync + 'static,
1734 Fut: Future<Output = Result<()>> + Send + 'static,
1735 {
1736 // 1. Wrap handler in Arc with type erasure
1737 let handler =
1738 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1739
1740 // 2. Store in handlers list
1741 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1742 pattern: pattern.to_string(),
1743 handler,
1744 });
1745
1746 // 3. Enable network interception via protocol
1747 self.enable_network_interception().await?;
1748
1749 Ok(())
1750 }
1751
1752 /// Updates network interception patterns for this page
1753 async fn enable_network_interception(&self) -> Result<()> {
1754 // Collect all patterns from registered handlers
1755 // Each pattern must be an object with "glob" field
1756 let patterns: Vec<serde_json::Value> = self
1757 .route_handlers
1758 .lock()
1759 .unwrap()
1760 .iter()
1761 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1762 .collect();
1763
1764 // Send protocol command to update network interception patterns
1765 // Follows playwright-python's approach
1766 self.channel()
1767 .send_no_result(
1768 "setNetworkInterceptionPatterns",
1769 serde_json::json!({
1770 "patterns": patterns
1771 }),
1772 )
1773 .await
1774 }
1775
1776 /// Removes route handler(s) matching the given URL pattern.
1777 ///
1778 /// # Arguments
1779 ///
1780 /// * `pattern` - URL pattern to remove handlers for
1781 ///
1782 /// See: <https://playwright.dev/docs/api/class-page#page-unroute>
1783 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
1784 pub async fn unroute(&self, pattern: &str) -> Result<()> {
1785 self.route_handlers
1786 .lock()
1787 .unwrap()
1788 .retain(|entry| entry.pattern != pattern);
1789 self.enable_network_interception().await
1790 }
1791
1792 /// Removes all registered route handlers.
1793 ///
1794 /// # Arguments
1795 ///
1796 /// * `behavior` - Optional behavior for in-flight handlers
1797 ///
1798 /// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
1799 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1800 pub async fn unroute_all(
1801 &self,
1802 _behavior: Option<crate::protocol::route::UnrouteBehavior>,
1803 ) -> Result<()> {
1804 self.route_handlers.lock().unwrap().clear();
1805 self.enable_network_interception().await
1806 }
1807
1808 /// Replays network requests from a HAR file recorded previously.
1809 ///
1810 /// Requests matching `options.url` (or all requests if omitted) will be
1811 /// served from the archive instead of hitting the network. Unmatched
1812 /// requests are either aborted or passed through depending on
1813 /// `options.not_found` (`"abort"` is the default).
1814 ///
1815 /// # Arguments
1816 ///
1817 /// * `har_path` - Path to the `.har` file on disk
1818 /// * `options` - Optional settings (url filter, not_found policy, update mode)
1819 ///
1820 /// # Errors
1821 ///
1822 /// Returns error if:
1823 /// - `har_path` does not exist or cannot be read by the Playwright server
1824 /// - The Playwright server fails to open the archive
1825 ///
1826 /// See: <https://playwright.dev/docs/api/class-page#page-route-from-har>
1827 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1828 pub async fn route_from_har(
1829 &self,
1830 har_path: &str,
1831 options: impl Into<Option<RouteFromHarOptions>>,
1832 ) -> Result<()> {
1833 let options = options.into();
1834 let opts = options.unwrap_or_default();
1835 let not_found = opts.not_found.unwrap_or_else(|| "abort".to_string());
1836 let url_filter = opts.url.clone();
1837
1838 // Resolve to an absolute path so the Playwright server can open it
1839 // regardless of its working directory.
1840 let abs_path = std::path::Path::new(har_path).canonicalize().map_err(|e| {
1841 Error::InvalidPath(format!(
1842 "route_from_har: cannot resolve '{}': {}",
1843 har_path, e
1844 ))
1845 })?;
1846 let abs_str = abs_path.to_string_lossy().into_owned();
1847
1848 // Locate LocalUtils in the connection object registry by type name.
1849 // The Playwright server registers it with a guid like "localUtils@1"
1850 // so we scan all objects for the one with type_name "LocalUtils".
1851 let connection = self.connection();
1852 let local_utils = {
1853 let all = connection.all_objects_sync();
1854 all.into_iter()
1855 .find(|o| o.type_name() == "LocalUtils")
1856 .and_then(|o| {
1857 o.as_any()
1858 .downcast_ref::<crate::protocol::LocalUtils>()
1859 .cloned()
1860 })
1861 .ok_or_else(|| {
1862 Error::ProtocolError(
1863 "route_from_har: LocalUtils not found in connection registry".to_string(),
1864 )
1865 })?
1866 };
1867
1868 // Open the HAR archive on the server side.
1869 let har_id = local_utils.har_open(&abs_str).await?;
1870
1871 // Determine the URL pattern to intercept.
1872 let pattern = url_filter.clone().unwrap_or_else(|| "**/*".to_string());
1873
1874 // Register a route handler that performs HAR lookup for each request.
1875 let har_id_clone = har_id.clone();
1876 let local_utils_clone = local_utils.clone();
1877 let not_found_clone = not_found.clone();
1878
1879 self.route(&pattern, move |route| {
1880 let har_id = har_id_clone.clone();
1881 let local_utils = local_utils_clone.clone();
1882 let not_found = not_found_clone.clone();
1883 async move {
1884 let request = route.request();
1885 let req_url = request.url().to_string();
1886 let req_method = request.method().to_string();
1887
1888 // Build headers array as [{name, value}]
1889 let headers: Vec<serde_json::Value> = request
1890 .headers()
1891 .iter()
1892 .map(|(k, v)| serde_json::json!({"name": k, "value": v}))
1893 .collect();
1894
1895 let lookup = local_utils
1896 .har_lookup(
1897 &har_id,
1898 &req_url,
1899 &req_method,
1900 headers,
1901 None,
1902 request.is_navigation_request(),
1903 )
1904 .await;
1905
1906 match lookup {
1907 Err(e) => {
1908 tracing::warn!("har_lookup error for {}: {}", req_url, e);
1909 route.continue_(None).await
1910 }
1911 Ok(result) => match result.action.as_str() {
1912 "redirect" => {
1913 let redirect_url = result.redirect_url.unwrap_or_default();
1914 let opts = crate::protocol::ContinueOptions::builder()
1915 .url(redirect_url)
1916 .build();
1917 route.continue_(Some(opts)).await
1918 }
1919 "fulfill" => {
1920 let status = result.status.unwrap_or(200);
1921
1922 // Decode base64 body if present
1923 let body_bytes = result.body.as_deref().map(|b64| {
1924 base64::engine::general_purpose::STANDARD
1925 .decode(b64)
1926 .unwrap_or_default()
1927 });
1928
1929 // Build headers map
1930 let mut headers_map = std::collections::HashMap::new();
1931 if let Some(raw_headers) = result.headers {
1932 for h in raw_headers {
1933 if let (Some(name), Some(value)) = (
1934 h.get("name").and_then(|v| v.as_str()),
1935 h.get("value").and_then(|v| v.as_str()),
1936 ) {
1937 headers_map.insert(name.to_string(), value.to_string());
1938 }
1939 }
1940 }
1941
1942 let mut builder =
1943 crate::protocol::FulfillOptions::builder().status(status);
1944
1945 if !headers_map.is_empty() {
1946 builder = builder.headers(headers_map);
1947 }
1948
1949 if let Some(body) = body_bytes {
1950 builder = builder.body(body);
1951 }
1952
1953 route.fulfill(Some(builder.build())).await
1954 }
1955 _ => {
1956 // "fallback" or "error" or unknown
1957 if not_found == "fallback" {
1958 route.fallback(None).await
1959 } else {
1960 route.abort(None).await
1961 }
1962 }
1963 },
1964 }
1965 }
1966 })
1967 .await
1968 }
1969
1970 /// Intercepts WebSocket connections matching the given URL pattern.
1971 ///
1972 /// When a WebSocket connection from the page matches `url`, the `handler`
1973 /// is called with a [`WebSocketRoute`](crate::protocol::WebSocketRoute) object.
1974 /// The handler must call [`connect_to_server`](crate::protocol::WebSocketRoute::connect_to_server)
1975 /// to forward the connection to the real server, or
1976 /// [`close`](crate::protocol::WebSocketRoute::close) to terminate it.
1977 ///
1978 /// # Arguments
1979 ///
1980 /// * `url` — URL glob pattern (e.g. `"ws://**"` or `"wss://example.com/ws"`).
1981 /// * `handler` — Async closure receiving a `WebSocketRoute`.
1982 ///
1983 /// # Errors
1984 ///
1985 /// Returns an error if the RPC call to enable interception fails.
1986 ///
1987 /// See: <https://playwright.dev/docs/api/class-page#page-route-web-socket>
1988 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %url))]
1989 pub async fn route_web_socket<F, Fut>(&self, url: &str, handler: F) -> Result<()>
1990 where
1991 F: Fn(crate::protocol::WebSocketRoute) -> Fut + Send + Sync + 'static,
1992 Fut: Future<Output = Result<()>> + Send + 'static,
1993 {
1994 let handler = Arc::new(
1995 move |route: crate::protocol::WebSocketRoute| -> WebSocketRouteHandlerFuture {
1996 Box::pin(handler(route))
1997 },
1998 );
1999
2000 self.ws_route_handlers
2001 .lock()
2002 .unwrap()
2003 .push(WsRouteHandlerEntry {
2004 pattern: url.to_string(),
2005 handler,
2006 });
2007
2008 self.enable_ws_interception().await
2009 }
2010
2011 /// Updates WebSocket interception patterns for this page.
2012 async fn enable_ws_interception(&self) -> Result<()> {
2013 let patterns: Vec<serde_json::Value> = self
2014 .ws_route_handlers
2015 .lock()
2016 .unwrap()
2017 .iter()
2018 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
2019 .collect();
2020
2021 self.channel()
2022 .send_no_result(
2023 "setWebSocketInterceptionPatterns",
2024 serde_json::json!({ "patterns": patterns }),
2025 )
2026 .await
2027 }
2028
2029 /// Handles a route event from the protocol
2030 ///
2031 /// Called by on_event when a "route" event is received.
2032 /// Supports handler chaining via `route.fallback()` — if a handler calls
2033 /// `fallback()` instead of `continue_()`, `abort()`, or `fulfill()`, the
2034 /// next matching handler in the chain is tried.
2035 async fn on_route_event(&self, route: Route) {
2036 let handlers = self.route_handlers.lock().unwrap().clone();
2037 let url = route.request().url().to_string();
2038
2039 // Find matching handler (last registered wins, with fallback chaining)
2040 for entry in handlers.iter().rev() {
2041 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
2042 let handler = entry.handler.clone();
2043 if let Err(e) = handler(route.clone()).await {
2044 tracing::warn!("Route handler error: {}", e);
2045 break;
2046 }
2047 // If handler called fallback(), try the next matching handler
2048 if !route.was_handled() {
2049 continue;
2050 }
2051 break;
2052 }
2053 }
2054 }
2055
2056 /// Registers a download event handler.
2057 ///
2058 /// The handler will be called when a download is triggered by the page.
2059 /// Downloads occur when the page initiates a file download (e.g., clicking a link
2060 /// with the download attribute, or a server response with Content-Disposition: attachment).
2061 ///
2062 /// # Arguments
2063 ///
2064 /// * `handler` - Async closure that receives the Download object
2065 ///
2066 /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
2067 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2068 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
2069 where
2070 F: Fn(Download) -> Fut + Send + Sync + 'static,
2071 Fut: Future<Output = Result<()>> + Send + 'static,
2072 {
2073 let handler: Handler<Download> = Arc::new(move |download| Box::pin(handler(download)));
2074 // "download" events are auto-emitted; no subscription needed.
2075 self.download.add_handler(handler);
2076
2077 Ok(())
2078 }
2079
2080 /// Registers a dialog event handler.
2081 ///
2082 /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
2083 /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
2084 ///
2085 /// # Arguments
2086 ///
2087 /// * `handler` - Async closure that receives the Dialog object
2088 ///
2089 /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
2090 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2091 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
2092 where
2093 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
2094 Fut: Future<Output = Result<()>> + Send + 'static,
2095 {
2096 let handler: Handler<Dialog> = Arc::new(move |dialog| Box::pin(handler(dialog)));
2097 // Dialog events are auto-emitted (no subscription needed).
2098 self.dialog.add_handler(handler);
2099
2100 Ok(())
2101 }
2102
2103 /// Registers a console event handler.
2104 ///
2105 /// The handler is called whenever the page emits a JavaScript console message
2106 /// (e.g. `console.log`, `console.error`, `console.warn`, etc.).
2107 ///
2108 /// The server only sends console events after the first handler is registered
2109 /// (subscription is managed automatically).
2110 ///
2111 /// # Arguments
2112 ///
2113 /// * `handler` - Async closure that receives the [`ConsoleMessage`](crate::protocol::ConsoleMessage)
2114 ///
2115 /// See: <https://playwright.dev/docs/api/class-page#page-event-console>
2116 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2117 pub async fn on_console<F, Fut>(&self, handler: F) -> Result<()>
2118 where
2119 F: Fn(crate::protocol::ConsoleMessage) -> Fut + Send + Sync + 'static,
2120 Fut: Future<Output = Result<()>> + Send + 'static,
2121 {
2122 let handler: Handler<crate::protocol::ConsoleMessage> =
2123 Arc::new(move |msg| Box::pin(handler(msg)));
2124
2125 self.subscribe_if_idle(&self.console).await;
2126 self.console.add_handler(handler);
2127
2128 Ok(())
2129 }
2130
2131 /// Registers a handler for file chooser events.
2132 ///
2133 /// The handler is called whenever the page opens a file chooser dialog
2134 /// (e.g. when the user clicks an `<input type="file">` element).
2135 ///
2136 /// Use [`FileChooser::set_files`](crate::protocol::FileChooser::set_files) inside
2137 /// the handler to satisfy the file chooser without OS-level interaction.
2138 ///
2139 /// The server only sends `"fileChooser"` events after the first handler is
2140 /// registered (subscription is managed automatically via `updateSubscription`).
2141 ///
2142 /// # Arguments
2143 ///
2144 /// * `handler` - Async closure that receives a [`FileChooser`](crate::protocol::FileChooser)
2145 ///
2146 /// # Example
2147 ///
2148 /// ```no_run
2149 /// # use playwright_rs::Playwright;
2150 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2151 /// # let pw = Playwright::launch().await?;
2152 /// # let browser = pw.chromium().launch().await?;
2153 /// # let page = browser.new_page().await?;
2154 /// page.on_filechooser(|chooser| async move {
2155 /// chooser.set_files(&[std::path::PathBuf::from("/tmp/file.txt")]).await
2156 /// }).await?;
2157 /// # Ok(())
2158 /// # }
2159 /// ```
2160 ///
2161 /// See: <https://playwright.dev/docs/api/class-page#page-event-file-chooser>
2162 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2163 pub async fn on_filechooser<F, Fut>(&self, handler: F) -> Result<()>
2164 where
2165 F: Fn(crate::protocol::FileChooser) -> Fut + Send + Sync + 'static,
2166 Fut: Future<Output = Result<()>> + Send + 'static,
2167 {
2168 let handler: Handler<crate::protocol::FileChooser> =
2169 Arc::new(move |chooser| Box::pin(handler(chooser)));
2170
2171 self.subscribe_if_idle(&self.filechooser).await;
2172 self.filechooser.add_handler(handler);
2173
2174 Ok(())
2175 }
2176
2177 /// Creates a one-shot waiter that resolves when the next file chooser opens.
2178 ///
2179 /// The waiter **must** be created before the action that triggers the file
2180 /// chooser to avoid a race condition.
2181 ///
2182 /// # Arguments
2183 ///
2184 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2185 ///
2186 /// # Errors
2187 ///
2188 /// Returns [`crate::error::Error::Timeout`] if the file chooser
2189 /// does not open within the timeout.
2190 ///
2191 /// # Example
2192 ///
2193 /// ```no_run
2194 /// # use playwright_rs::Playwright;
2195 /// # use std::path::PathBuf;
2196 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2197 /// # let pw = Playwright::launch().await?;
2198 /// # let browser = pw.chromium().launch().await?;
2199 /// # let page = browser.new_page().await?;
2200 /// // Set up waiter BEFORE triggering the file chooser
2201 /// let waiter = page.expect_file_chooser(None).await?;
2202 /// page.locator("input[type=file]").click(None).await?;
2203 /// let chooser = waiter.wait().await?;
2204 /// chooser.set_files(&[PathBuf::from("/tmp/file.txt")]).await?;
2205 /// # Ok(())
2206 /// # }
2207 /// ```
2208 ///
2209 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2210 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2211 pub async fn expect_file_chooser(
2212 &self,
2213 timeout: Option<f64>,
2214 ) -> Result<crate::protocol::EventWaiter<crate::protocol::FileChooser>> {
2215 self.subscribe_if_idle(&self.filechooser).await;
2216 let rx = self.filechooser.wait();
2217
2218 Ok(crate::protocol::EventWaiter::new(
2219 rx,
2220 timeout.or(Some(30_000.0)),
2221 ))
2222 }
2223
2224 /// Creates a one-shot waiter that resolves when the next popup window opens.
2225 ///
2226 /// The waiter **must** be created before the action that opens the popup to
2227 /// avoid a race condition.
2228 ///
2229 /// # Arguments
2230 ///
2231 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2232 ///
2233 /// # Errors
2234 ///
2235 /// Returns [`crate::error::Error::Timeout`] if no popup
2236 /// opens within the timeout.
2237 ///
2238 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2239 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2240 pub async fn expect_popup(
2241 &self,
2242 timeout: Option<f64>,
2243 ) -> Result<crate::protocol::EventWaiter<Page>> {
2244 let rx = self.popup.wait();
2245 Ok(crate::protocol::EventWaiter::new(
2246 rx,
2247 timeout.or(Some(30_000.0)),
2248 ))
2249 }
2250
2251 /// Creates a one-shot waiter that resolves when the next download starts.
2252 ///
2253 /// The waiter **must** be created before the action that triggers the download
2254 /// to avoid a race condition.
2255 ///
2256 /// # Arguments
2257 ///
2258 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2259 ///
2260 /// # Errors
2261 ///
2262 /// Returns [`crate::error::Error::Timeout`] if no download
2263 /// starts within the timeout.
2264 ///
2265 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2266 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2267 pub async fn expect_download(
2268 &self,
2269 timeout: Option<f64>,
2270 ) -> Result<crate::protocol::EventWaiter<Download>> {
2271 let rx = self.download.wait();
2272 Ok(crate::protocol::EventWaiter::new(
2273 rx,
2274 timeout.or(Some(30_000.0)),
2275 ))
2276 }
2277
2278 /// Creates a one-shot waiter that resolves when the next network response is received.
2279 ///
2280 /// The waiter **must** be created before the action that triggers the response
2281 /// to avoid a race condition.
2282 ///
2283 /// # Arguments
2284 ///
2285 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2286 ///
2287 /// # Errors
2288 ///
2289 /// Returns [`crate::error::Error::Timeout`] if no response
2290 /// arrives within the timeout.
2291 ///
2292 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2293 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2294 pub async fn expect_response(
2295 &self,
2296 timeout: Option<f64>,
2297 ) -> Result<crate::protocol::EventWaiter<ResponseObject>> {
2298 self.subscribe_if_idle(&self.response).await;
2299 let rx = self.response.wait();
2300
2301 Ok(crate::protocol::EventWaiter::new(
2302 rx,
2303 timeout.or(Some(30_000.0)),
2304 ))
2305 }
2306
2307 /// Creates a one-shot waiter that resolves when the next network request is issued.
2308 ///
2309 /// The waiter **must** be created before the action that issues the request
2310 /// to avoid a race condition.
2311 ///
2312 /// # Arguments
2313 ///
2314 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2315 ///
2316 /// # Errors
2317 ///
2318 /// Returns [`crate::error::Error::Timeout`] if no request
2319 /// is issued within the timeout.
2320 ///
2321 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2322 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2323 pub async fn expect_request(
2324 &self,
2325 timeout: Option<f64>,
2326 ) -> Result<crate::protocol::EventWaiter<Request>> {
2327 self.subscribe_if_idle(&self.request).await;
2328 let rx = self.request.wait();
2329
2330 Ok(crate::protocol::EventWaiter::new(
2331 rx,
2332 timeout.or(Some(30_000.0)),
2333 ))
2334 }
2335
2336 /// Creates a one-shot waiter that resolves when the next console message is produced.
2337 ///
2338 /// The waiter **must** be created before the action that produces the console
2339 /// message to avoid a race condition.
2340 ///
2341 /// # Arguments
2342 ///
2343 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2344 ///
2345 /// # Errors
2346 ///
2347 /// Returns [`crate::error::Error::Timeout`] if no console
2348 /// message is produced within the timeout.
2349 ///
2350 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2351 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2352 pub async fn expect_console_message(
2353 &self,
2354 timeout: Option<f64>,
2355 ) -> Result<crate::protocol::EventWaiter<crate::protocol::ConsoleMessage>> {
2356 self.subscribe_if_idle(&self.console).await;
2357 let rx = self.console.wait();
2358
2359 Ok(crate::protocol::EventWaiter::new(
2360 rx,
2361 timeout.or(Some(30_000.0)),
2362 ))
2363 }
2364
2365 /// Waits for the given event to fire and returns a typed `EventValue`.
2366 ///
2367 /// This is the generic version of the specific `expect_*` methods. It matches
2368 /// the playwright-python / playwright-js `page.expect_event(event_name)` API.
2369 ///
2370 /// The waiter **must** be created before the action that triggers the event.
2371 ///
2372 /// # Supported event names
2373 ///
2374 /// `"request"`, `"response"`, `"popup"`, `"download"`, `"console"`,
2375 /// `"filechooser"`, `"close"`, `"load"`, `"crash"`, `"pageerror"`,
2376 /// `"frameattached"`, `"framedetached"`, `"framenavigated"`, `"worker"`
2377 ///
2378 /// # Arguments
2379 ///
2380 /// * `event` - Event name (case-sensitive, matches Playwright protocol names).
2381 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2382 ///
2383 /// # Errors
2384 ///
2385 /// Returns [`crate::error::Error::InvalidArgument`] for unknown event names.
2386 /// Returns [`crate::error::Error::Timeout`] if the event does not fire within the timeout.
2387 ///
2388 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2389 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2390 pub async fn expect_event(
2391 &self,
2392 event: &str,
2393 timeout: Option<f64>,
2394 ) -> Result<crate::protocol::EventWaiter<crate::protocol::EventValue>> {
2395 use crate::protocol::EventValue;
2396 use tokio::sync::oneshot;
2397
2398 let timeout_ms = timeout.or(Some(30_000.0));
2399
2400 match event {
2401 "request" => {
2402 let (mut tx, rx) = oneshot::channel::<EventValue>();
2403
2404 self.subscribe_if_idle(&self.request).await;
2405 let inner_rx = self.request.wait();
2406
2407 // select: drop the registry receiver when the caller times
2408 // out, or a stale FIFO waiter swallows the next event.
2409 tokio::spawn(
2410 async move {
2411 tokio::select! {
2412 v = inner_rx => {
2413 if let Ok(v) = v {
2414 let _ = tx.send(EventValue::Request(v));
2415 }
2416 }
2417 () = tx.closed() => {}
2418 }
2419 }
2420 .in_current_span(),
2421 );
2422
2423 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2424 }
2425
2426 "response" => {
2427 let (mut tx, rx) = oneshot::channel::<EventValue>();
2428
2429 self.subscribe_if_idle(&self.response).await;
2430 let inner_rx = self.response.wait();
2431
2432 // select: see the "request" arm.
2433 tokio::spawn(
2434 async move {
2435 tokio::select! {
2436 v = inner_rx => {
2437 if let Ok(v) = v {
2438 let _ = tx.send(EventValue::Response(v));
2439 }
2440 }
2441 () = tx.closed() => {}
2442 }
2443 }
2444 .in_current_span(),
2445 );
2446
2447 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2448 }
2449
2450 "popup" => {
2451 let (mut tx, rx) = oneshot::channel::<EventValue>();
2452 let inner_rx = self.popup.wait();
2453
2454 // select: see the "request" arm.
2455 tokio::spawn(
2456 async move {
2457 tokio::select! {
2458 v = inner_rx => {
2459 if let Ok(v) = v {
2460 let _ = tx.send(EventValue::Page(v));
2461 }
2462 }
2463 () = tx.closed() => {}
2464 }
2465 }
2466 .in_current_span(),
2467 );
2468
2469 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2470 }
2471
2472 "download" => {
2473 let (mut tx, rx) = oneshot::channel::<EventValue>();
2474 let inner_rx = self.download.wait();
2475
2476 // select: see the "request" arm.
2477 tokio::spawn(
2478 async move {
2479 tokio::select! {
2480 v = inner_rx => {
2481 if let Ok(v) = v {
2482 let _ = tx.send(EventValue::Download(v));
2483 }
2484 }
2485 () = tx.closed() => {}
2486 }
2487 }
2488 .in_current_span(),
2489 );
2490
2491 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2492 }
2493
2494 "console" => {
2495 let (mut tx, rx) = oneshot::channel::<EventValue>();
2496
2497 self.subscribe_if_idle(&self.console).await;
2498 let inner_rx = self.console.wait();
2499
2500 // The select is load-bearing: with FIFO waiters, a forwarding
2501 // task that merely awaits `inner_rx` keeps the registry's
2502 // sender alive after the caller's EventWaiter times out, and
2503 // that stale front-of-queue waiter would swallow the next
2504 // event, starving the live waiter behind it. Dropping
2505 // `inner_rx` the moment the outer receiver goes away lets the
2506 // registry's dead-sender skip do its job.
2507 tokio::spawn(
2508 async move {
2509 tokio::select! {
2510 v = inner_rx => {
2511 if let Ok(v) = v {
2512 let _ = tx.send(EventValue::ConsoleMessage(v));
2513 }
2514 }
2515 () = tx.closed() => {}
2516 }
2517 }
2518 .in_current_span(),
2519 );
2520
2521 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2522 }
2523
2524 "filechooser" => {
2525 let (mut tx, rx) = oneshot::channel::<EventValue>();
2526
2527 self.subscribe_if_idle(&self.filechooser).await;
2528 let inner_rx = self.filechooser.wait();
2529
2530 // select: see the "request" arm.
2531 tokio::spawn(
2532 async move {
2533 tokio::select! {
2534 v = inner_rx => {
2535 if let Ok(v) = v {
2536 let _ = tx.send(EventValue::FileChooser(v));
2537 }
2538 }
2539 () = tx.closed() => {}
2540 }
2541 }
2542 .in_current_span(),
2543 );
2544
2545 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2546 }
2547
2548 "close" => {
2549 let (mut tx, rx) = oneshot::channel::<EventValue>();
2550 let inner_rx = self.close.wait();
2551
2552 // select: see the "request" arm.
2553 tokio::spawn(
2554 async move {
2555 tokio::select! {
2556 v = inner_rx => {
2557 if v.is_ok() { let _ = tx.send(EventValue::Close); }
2558 }
2559 () = tx.closed() => {}
2560 }
2561 }
2562 .in_current_span(),
2563 );
2564
2565 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2566 }
2567
2568 "load" => {
2569 let (mut tx, rx) = oneshot::channel::<EventValue>();
2570 let inner_rx = self.load.wait();
2571
2572 // select: see the "request" arm.
2573 tokio::spawn(
2574 async move {
2575 tokio::select! {
2576 v = inner_rx => {
2577 if v.is_ok() { let _ = tx.send(EventValue::Load); }
2578 }
2579 () = tx.closed() => {}
2580 }
2581 }
2582 .in_current_span(),
2583 );
2584
2585 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2586 }
2587
2588 "crash" => {
2589 let (mut tx, rx) = oneshot::channel::<EventValue>();
2590 let inner_rx = self.crash.wait();
2591
2592 // select: see the "request" arm.
2593 tokio::spawn(
2594 async move {
2595 tokio::select! {
2596 v = inner_rx => {
2597 if v.is_ok() { let _ = tx.send(EventValue::Crash); }
2598 }
2599 () = tx.closed() => {}
2600 }
2601 }
2602 .in_current_span(),
2603 );
2604
2605 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2606 }
2607
2608 "pageerror" => {
2609 let (mut tx, rx) = oneshot::channel::<EventValue>();
2610 let inner_rx = self.pageerror.wait();
2611
2612 // select: see the "request" arm.
2613 tokio::spawn(
2614 async move {
2615 tokio::select! {
2616 msg = inner_rx => {
2617 if let Ok(msg) = msg { let _ = tx.send(EventValue::PageError(msg)); }
2618 }
2619 () = tx.closed() => {}
2620 }
2621 }
2622 .in_current_span(),
2623 );
2624
2625 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2626 }
2627
2628 "frameattached" => {
2629 let (mut tx, rx) = oneshot::channel::<EventValue>();
2630 let inner_rx = self.frameattached.wait();
2631
2632 // select: see the "request" arm.
2633 tokio::spawn(
2634 async move {
2635 tokio::select! {
2636 v = inner_rx => {
2637 if let Ok(v) = v { let _ = tx.send(EventValue::Frame(v)); }
2638 }
2639 () = tx.closed() => {}
2640 }
2641 }
2642 .in_current_span(),
2643 );
2644
2645 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2646 }
2647
2648 "framedetached" => {
2649 let (mut tx, rx) = oneshot::channel::<EventValue>();
2650 let inner_rx = self.framedetached.wait();
2651
2652 // select: see the "request" arm.
2653 tokio::spawn(
2654 async move {
2655 tokio::select! {
2656 v = inner_rx => {
2657 if let Ok(v) = v { let _ = tx.send(EventValue::Frame(v)); }
2658 }
2659 () = tx.closed() => {}
2660 }
2661 }
2662 .in_current_span(),
2663 );
2664
2665 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2666 }
2667
2668 "framenavigated" => {
2669 let (mut tx, rx) = oneshot::channel::<EventValue>();
2670 let inner_rx = self.framenavigated.wait();
2671
2672 // select: see the "request" arm.
2673 tokio::spawn(
2674 async move {
2675 tokio::select! {
2676 v = inner_rx => {
2677 if let Ok(v) = v { let _ = tx.send(EventValue::Frame(v)); }
2678 }
2679 () = tx.closed() => {}
2680 }
2681 }
2682 .in_current_span(),
2683 );
2684
2685 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2686 }
2687
2688 "worker" => {
2689 let (tx, rx) = oneshot::channel::<EventValue>();
2690 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Worker>();
2691 self.worker_waiters.lock().unwrap().push(inner_tx);
2692
2693 tokio::spawn(
2694 async move {
2695 if let Ok(v) = inner_rx.await {
2696 let _ = tx.send(EventValue::Worker(v));
2697 }
2698 }
2699 .in_current_span(),
2700 );
2701
2702 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2703 }
2704
2705 other => Err(Error::InvalidArgument(format!(
2706 "Unknown event name '{}'. Supported: request, response, popup, download, \
2707 console, filechooser, close, load, crash, pageerror, \
2708 frameattached, framedetached, framenavigated, worker",
2709 other
2710 ))),
2711 }
2712 }
2713
2714 /// See: <https://playwright.dev/docs/api/class-page#page-event-request>
2715 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2716 pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
2717 where
2718 F: Fn(Request) -> Fut + Send + Sync + 'static,
2719 Fut: Future<Output = Result<()>> + Send + 'static,
2720 {
2721 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
2722
2723 self.subscribe_if_idle(&self.request).await;
2724 self.request.add_handler(handler);
2725
2726 Ok(())
2727 }
2728
2729 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-finished>
2730 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2731 pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
2732 where
2733 F: Fn(Request) -> Fut + Send + Sync + 'static,
2734 Fut: Future<Output = Result<()>> + Send + 'static,
2735 {
2736 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
2737
2738 self.subscribe_if_idle(&self.request_finished).await;
2739 self.request_finished.add_handler(handler);
2740
2741 Ok(())
2742 }
2743
2744 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-failed>
2745 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2746 pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
2747 where
2748 F: Fn(Request) -> Fut + Send + Sync + 'static,
2749 Fut: Future<Output = Result<()>> + Send + 'static,
2750 {
2751 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
2752
2753 self.subscribe_if_idle(&self.request_failed).await;
2754 self.request_failed.add_handler(handler);
2755
2756 Ok(())
2757 }
2758
2759 /// See: <https://playwright.dev/docs/api/class-page#page-event-response>
2760 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2761 pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
2762 where
2763 F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
2764 Fut: Future<Output = Result<()>> + Send + 'static,
2765 {
2766 let handler: Handler<ResponseObject> =
2767 Arc::new(move |response| Box::pin(handler(response)));
2768
2769 self.subscribe_if_idle(&self.response).await;
2770 self.response.add_handler(handler);
2771
2772 Ok(())
2773 }
2774
2775 /// Adds a listener for the `websocket` event.
2776 ///
2777 /// The handler will be called when a WebSocket request is dispatched.
2778 ///
2779 /// # Arguments
2780 ///
2781 /// * `handler` - The function to call when the event occurs
2782 ///
2783 /// See: <https://playwright.dev/docs/api/class-page#page-on-websocket>
2784 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2785 pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
2786 where
2787 F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
2788 Fut: Future<Output = Result<()>> + Send + 'static,
2789 {
2790 let handler =
2791 Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
2792 self.websocket_handlers.lock().unwrap().push(handler);
2793 Ok(())
2794 }
2795
2796 /// Registers a handler for the `worker` event.
2797 ///
2798 /// The handler is called when a new Web Worker is created in the page.
2799 ///
2800 /// # Arguments
2801 ///
2802 /// * `handler` - Async closure called with the new [`Worker`] object
2803 ///
2804 /// See: <https://playwright.dev/docs/api/class-page#page-event-worker>
2805 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2806 pub async fn on_worker<F, Fut>(&self, handler: F) -> Result<()>
2807 where
2808 F: Fn(Worker) -> Fut + Send + Sync + 'static,
2809 Fut: Future<Output = Result<()>> + Send + 'static,
2810 {
2811 let handler = Arc::new(move |w: Worker| -> WorkerHandlerFuture { Box::pin(handler(w)) });
2812 self.worker_handlers.lock().unwrap().push(handler);
2813 Ok(())
2814 }
2815
2816 /// Registers a handler for the `close` event.
2817 ///
2818 /// The handler is called when the page is closed, either by calling `page.close()`,
2819 /// by the browser context being closed, or when the browser process exits.
2820 ///
2821 /// # Arguments
2822 ///
2823 /// * `handler` - Async closure called with no arguments when the page closes
2824 ///
2825 /// See: <https://playwright.dev/docs/api/class-page#page-event-close>
2826 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2827 pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
2828 where
2829 F: Fn() -> Fut + Send + Sync + 'static,
2830 Fut: Future<Output = Result<()>> + Send + 'static,
2831 {
2832 let handler: Handler<()> = Arc::new(move |()| Box::pin(handler()));
2833 self.close.add_handler(handler);
2834 Ok(())
2835 }
2836
2837 /// Registers a handler for the `load` event.
2838 ///
2839 /// The handler is called when the page's `load` event fires, i.e. after
2840 /// all resources including stylesheets and images have finished loading.
2841 ///
2842 /// The server only sends `"load"` events after the first handler is registered
2843 /// (subscription is managed automatically).
2844 ///
2845 /// # Arguments
2846 ///
2847 /// * `handler` - Async closure called with no arguments when the page loads
2848 ///
2849 /// See: <https://playwright.dev/docs/api/class-page#page-event-load>
2850 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2851 pub async fn on_load<F, Fut>(&self, handler: F) -> Result<()>
2852 where
2853 F: Fn() -> Fut + Send + Sync + 'static,
2854 Fut: Future<Output = Result<()>> + Send + 'static,
2855 {
2856 let handler: Handler<()> = Arc::new(move |()| Box::pin(handler()));
2857 // "load" events come via Frame's "loadstate" event, no subscription needed.
2858 self.load.add_handler(handler);
2859 Ok(())
2860 }
2861
2862 /// Registers a handler for the `crash` event.
2863 ///
2864 /// The handler is called when the page crashes (e.g. runs out of memory).
2865 ///
2866 /// # Arguments
2867 ///
2868 /// * `handler` - Async closure called with no arguments when the page crashes
2869 ///
2870 /// See: <https://playwright.dev/docs/api/class-page#page-event-crash>
2871 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2872 pub async fn on_crash<F, Fut>(&self, handler: F) -> Result<()>
2873 where
2874 F: Fn() -> Fut + Send + Sync + 'static,
2875 Fut: Future<Output = Result<()>> + Send + 'static,
2876 {
2877 let handler: Handler<()> = Arc::new(move |()| Box::pin(handler()));
2878 self.crash.add_handler(handler);
2879 Ok(())
2880 }
2881
2882 /// Registers a handler for the `pageError` event.
2883 ///
2884 /// The handler is called when an uncaught JavaScript exception is thrown in the page.
2885 /// The handler receives the error message as a `String`.
2886 ///
2887 /// The server only sends `"pageError"` events after the first handler is registered
2888 /// (subscription is managed automatically).
2889 ///
2890 /// # Arguments
2891 ///
2892 /// * `handler` - Async closure that receives the error message string
2893 ///
2894 /// See: <https://playwright.dev/docs/api/class-page#page-event-page-error>
2895 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2896 pub async fn on_pageerror<F, Fut>(&self, handler: F) -> Result<()>
2897 where
2898 F: Fn(String) -> Fut + Send + Sync + 'static,
2899 Fut: Future<Output = Result<()>> + Send + 'static,
2900 {
2901 let handler: Handler<String> = Arc::new(move |msg| Box::pin(handler(msg)));
2902 // "pageError" events come via BrowserContext, no subscription needed.
2903 self.pageerror.add_handler(handler);
2904 Ok(())
2905 }
2906
2907 /// Registers a handler for the `popup` event.
2908 ///
2909 /// The handler is called when the page opens a popup window (e.g. via `window.open()`).
2910 /// The handler receives the new popup [`Page`] object.
2911 ///
2912 /// The server only sends `"popup"` events after the first handler is registered
2913 /// (subscription is managed automatically).
2914 ///
2915 /// # Arguments
2916 ///
2917 /// * `handler` - Async closure that receives the popup Page
2918 ///
2919 /// See: <https://playwright.dev/docs/api/class-page#page-event-popup>
2920 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2921 pub async fn on_popup<F, Fut>(&self, handler: F) -> Result<()>
2922 where
2923 F: Fn(Page) -> Fut + Send + Sync + 'static,
2924 Fut: Future<Output = Result<()>> + Send + 'static,
2925 {
2926 let handler: Handler<Page> = Arc::new(move |page| Box::pin(handler(page)));
2927 // "popup" events arrive via BrowserContext's "page" event when a page has an opener.
2928 self.popup.add_handler(handler);
2929 Ok(())
2930 }
2931
2932 /// Registers a handler for the `frameAttached` event.
2933 ///
2934 /// The handler is called when a new frame (iframe) is attached to the page.
2935 /// The handler receives the attached [`Frame`](crate::protocol::Frame) object.
2936 ///
2937 /// # Arguments
2938 ///
2939 /// * `handler` - Async closure that receives the attached Frame
2940 ///
2941 /// See: <https://playwright.dev/docs/api/class-page#page-event-frameattached>
2942 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2943 pub async fn on_frameattached<F, Fut>(&self, handler: F) -> Result<()>
2944 where
2945 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2946 Fut: Future<Output = Result<()>> + Send + 'static,
2947 {
2948 let handler: Handler<crate::protocol::Frame> =
2949 Arc::new(move |frame| Box::pin(handler(frame)));
2950 self.frameattached.add_handler(handler);
2951 Ok(())
2952 }
2953
2954 /// Registers a handler for the `frameDetached` event.
2955 ///
2956 /// The handler is called when a frame (iframe) is detached from the page.
2957 /// The handler receives the detached [`Frame`](crate::protocol::Frame) object.
2958 ///
2959 /// # Arguments
2960 ///
2961 /// * `handler` - Async closure that receives the detached Frame
2962 ///
2963 /// See: <https://playwright.dev/docs/api/class-page#page-event-framedetached>
2964 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2965 pub async fn on_framedetached<F, Fut>(&self, handler: F) -> Result<()>
2966 where
2967 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2968 Fut: Future<Output = Result<()>> + Send + 'static,
2969 {
2970 let handler: Handler<crate::protocol::Frame> =
2971 Arc::new(move |frame| Box::pin(handler(frame)));
2972 self.framedetached.add_handler(handler);
2973 Ok(())
2974 }
2975
2976 /// Registers a handler for the `frameNavigated` event.
2977 ///
2978 /// The handler is called when a frame navigates to a new URL.
2979 /// The handler receives the navigated [`Frame`](crate::protocol::Frame) object.
2980 ///
2981 /// # Arguments
2982 ///
2983 /// * `handler` - Async closure that receives the navigated Frame
2984 ///
2985 /// See: <https://playwright.dev/docs/api/class-page#page-event-framenavigated>
2986 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
2987 pub async fn on_framenavigated<F, Fut>(&self, handler: F) -> Result<()>
2988 where
2989 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2990 Fut: Future<Output = Result<()>> + Send + 'static,
2991 {
2992 let handler: Handler<crate::protocol::Frame> =
2993 Arc::new(move |frame| Box::pin(handler(frame)));
2994 self.framenavigated.add_handler(handler);
2995 Ok(())
2996 }
2997
2998 /// Exposes a Rust function to this page as `window[name]` in JavaScript.
2999 ///
3000 /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
3001 /// server fires a `bindingCall` event on the **page** channel that invokes
3002 /// `callback` with the deserialized arguments. The return value is sent back
3003 /// to JS so the `await window[name](…)` expression resolves with it.
3004 ///
3005 /// The binding is page-scoped and not visible to other pages in the same context.
3006 ///
3007 /// # Arguments
3008 ///
3009 /// * `name` – JavaScript identifier that will be available as `window[name]`.
3010 /// * `callback` – Async closure called with `Vec<serde_json::Value>` (JS arguments)
3011 /// returning `serde_json::Value` (the result).
3012 ///
3013 /// # Errors
3014 ///
3015 /// Returns error if:
3016 /// - The page has been closed.
3017 /// - Communication with the browser process fails.
3018 ///
3019 /// See: <https://playwright.dev/docs/api/class-page#page-expose-function>
3020 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), name = %name))]
3021 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
3022 where
3023 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
3024 Fut: Future<Output = serde_json::Value> + Send + 'static,
3025 {
3026 self.expose_binding_internal(name, false, callback).await
3027 }
3028
3029 /// Exposes a Rust function to this page as `window[name]` in JavaScript.
3030 ///
3031 /// Currently identical to [`expose_function`](Self::expose_function):
3032 /// arguments arrive as plain serialized values. Upstream Playwright's
3033 /// `exposeBinding` can additionally hand the callback a source
3034 /// (page/frame) descriptor, which this crate does not surface yet.
3035 ///
3036 /// # Arguments
3037 ///
3038 /// * `name` – JavaScript identifier.
3039 /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
3040 ///
3041 /// # Errors
3042 ///
3043 /// Returns error if:
3044 /// - The page has been closed.
3045 /// - Communication with the browser process fails.
3046 ///
3047 /// See: <https://playwright.dev/docs/api/class-page#page-expose-binding>
3048 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), name = %name))]
3049 pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
3050 where
3051 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
3052 Fut: Future<Output = serde_json::Value> + Send + 'static,
3053 {
3054 self.expose_binding_internal(name, false, callback).await
3055 }
3056
3057 /// Internal implementation shared by page-level expose_function and expose_binding.
3058 async fn expose_binding_internal<F, Fut>(
3059 &self,
3060 name: &str,
3061 no_global: bool,
3062 callback: F,
3063 ) -> Result<()>
3064 where
3065 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
3066 Fut: Future<Output = serde_json::Value> + Send + 'static,
3067 {
3068 let callback: PageBindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
3069 Box::pin(callback(args)) as PageBindingCallbackFuture
3070 });
3071
3072 // Store callback before sending RPC (avoids race with early bindingCall events)
3073 self.binding_callbacks
3074 .lock()
3075 .unwrap()
3076 .insert(name.to_string(), callback);
3077
3078 // Tell the Playwright server to register the binding. `noGlobal`
3079 // suppresses the `window[name]` injection; it is how
3080 // `evaluate_with_callback` passes a function the page can only reach
3081 // through the bindings controller, never off `window`.
3082 let mut params = serde_json::json!({ "name": name });
3083 if no_global {
3084 params["noGlobal"] = serde_json::json!(true);
3085 }
3086 self.channel().send_no_result("exposeBinding", params).await
3087 }
3088
3089 /// Handles a download event from the protocol
3090 async fn on_download_event(&self, download: Download) {
3091 self.download.dispatch(download).await;
3092 }
3093
3094 /// Handles a dialog event from the protocol
3095 async fn on_dialog_event(&self, dialog: Dialog) {
3096 self.dialog.dispatch(dialog).await;
3097 }
3098
3099 async fn on_request_event(&self, request: Request) {
3100 self.request.dispatch(request).await;
3101 }
3102
3103 async fn on_request_failed_event(&self, request: Request) {
3104 self.request_failed.dispatch(request).await;
3105 }
3106
3107 async fn on_request_finished_event(&self, request: Request) {
3108 self.request_finished.dispatch(request).await;
3109 }
3110
3111 async fn on_response_event(&self, response: ResponseObject) {
3112 self.response.dispatch(response).await;
3113 }
3114
3115 /// Registers a handler function that runs whenever a locator matches an element on the page.
3116 ///
3117 /// This is useful for handling overlays (cookie banners, modals, permission dialogs)
3118 /// that appear unexpectedly and need to be dismissed before test actions can proceed.
3119 ///
3120 /// When a matching element appears, Playwright sends a `locatorHandlerTriggered` event.
3121 /// The handler is called with the matching `Locator`. After the handler completes,
3122 /// Playwright is notified via `resolveLocatorHandler` so it can resume pending actions.
3123 ///
3124 /// # Arguments
3125 ///
3126 /// * `locator` - A locator identifying the overlay element to watch for
3127 /// * `handler` - Async function called with the matching Locator when the element appears
3128 /// * `options` - Optional settings (no_wait_after, times)
3129 ///
3130 /// # Errors
3131 ///
3132 /// Returns error if communication with the browser process fails.
3133 ///
3134 /// See: <https://playwright.dev/docs/api/class-page#page-add-locator-handler>
3135 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3136 pub async fn add_locator_handler<F, Fut>(
3137 &self,
3138 locator: &crate::protocol::Locator,
3139 handler: F,
3140 options: impl Into<Option<AddLocatorHandlerOptions>>,
3141 ) -> Result<()>
3142 where
3143 F: Fn(crate::protocol::Locator) -> Fut + Send + Sync + 'static,
3144 Fut: Future<Output = Result<()>> + Send + 'static,
3145 {
3146 let options = options.into();
3147 let selector = locator.selector().to_string();
3148 let no_wait_after = options
3149 .as_ref()
3150 .and_then(|o| o.no_wait_after)
3151 .unwrap_or(false);
3152 let times = options.as_ref().and_then(|o| o.times);
3153
3154 // Send registerLocatorHandler RPC — returns {"uid": N}
3155 let params = serde_json::json!({
3156 "selector": selector,
3157 "noWaitAfter": no_wait_after,
3158 });
3159 let result: Value = self
3160 .channel()
3161 .send("registerLocatorHandler", params)
3162 .await?;
3163
3164 let uid = result
3165 .get("uid")
3166 .and_then(|v| v.as_u64())
3167 .map(|v| v as u32)
3168 .ok_or_else(|| {
3169 Error::ProtocolError("registerLocatorHandler response missing 'uid'".to_string())
3170 })?;
3171
3172 let handler_fn: LocatorHandlerFn = Arc::new(
3173 move |loc: crate::protocol::Locator| -> LocatorHandlerFuture { Box::pin(handler(loc)) },
3174 );
3175
3176 self.locator_handlers
3177 .lock()
3178 .unwrap()
3179 .push(LocatorHandlerEntry {
3180 uid,
3181 selector,
3182 handler: handler_fn,
3183 times_remaining: times,
3184 });
3185
3186 Ok(())
3187 }
3188
3189 /// Removes a previously registered locator handler.
3190 ///
3191 /// Sends `unregisterLocatorHandler` to the Playwright server using the uid
3192 /// that was assigned when the handler was first registered.
3193 ///
3194 /// # Arguments
3195 ///
3196 /// * `locator` - The same locator that was passed to `add_locator_handler`
3197 ///
3198 /// # Errors
3199 ///
3200 /// Returns error if no handler for this locator is registered, or if
3201 /// communication with the browser process fails.
3202 ///
3203 /// See: <https://playwright.dev/docs/api/class-page#page-remove-locator-handler>
3204 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3205 pub async fn remove_locator_handler(&self, locator: &crate::protocol::Locator) -> Result<()> {
3206 let selector = locator.selector();
3207
3208 // Find the uid for this selector
3209 let uid = {
3210 let handlers = self.locator_handlers.lock().unwrap();
3211 handlers
3212 .iter()
3213 .find(|e| e.selector == selector)
3214 .map(|e| e.uid)
3215 };
3216
3217 let uid = uid.ok_or_else(|| {
3218 Error::ProtocolError(format!(
3219 "No locator handler registered for selector '{}'",
3220 selector
3221 ))
3222 })?;
3223
3224 // Send unregisterLocatorHandler RPC
3225 self.channel()
3226 .send_no_result(
3227 "unregisterLocatorHandler",
3228 serde_json::json!({ "uid": uid }),
3229 )
3230 .await?;
3231
3232 // Remove from local registry
3233 self.locator_handlers
3234 .lock()
3235 .unwrap()
3236 .retain(|e| e.uid != uid);
3237
3238 Ok(())
3239 }
3240
3241 /// Triggers dialog event (called by BrowserContext when dialog events arrive)
3242 ///
3243 /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
3244 /// This method is public so BrowserContext can forward dialog events.
3245 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3246 pub async fn trigger_dialog_event(&self, dialog: Dialog) {
3247 self.on_dialog_event(dialog).await;
3248 }
3249
3250 /// Triggers request event (called by BrowserContext when request events arrive)
3251 pub(crate) async fn trigger_request_event(&self, request: Request) {
3252 self.on_request_event(request).await;
3253 }
3254
3255 pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
3256 self.on_request_finished_event(request).await;
3257 }
3258
3259 pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
3260 self.on_request_failed_event(request).await;
3261 }
3262
3263 /// Triggers response event (called by BrowserContext when response events arrive)
3264 pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
3265 self.on_response_event(response).await;
3266 }
3267
3268 /// Triggers console event (called by BrowserContext when console events arrive).
3269 ///
3270 /// The BrowserContext receives all `"console"` events, constructs the
3271 /// [`ConsoleMessage`](crate::protocol::ConsoleMessage), dispatches to
3272 /// context-level handlers, then calls this method to forward to page-level handlers.
3273 pub(crate) async fn trigger_console_event(&self, msg: crate::protocol::ConsoleMessage) {
3274 self.on_console_event(msg).await;
3275 }
3276
3277 /// Subscribe to `reg`'s event if nothing is listening yet.
3278 ///
3279 /// The server only pushes an event once we ask for it, so the first
3280 /// handler or `expect_*` on a given event has to turn the subscription on.
3281 /// The event name comes from the registry, so it is written once at
3282 /// construction instead of restated at every registration site.
3283 async fn subscribe_if_idle<T>(&self, reg: &EventRegistry<T>) {
3284 if reg.is_idle() {
3285 _ = self.channel().update_subscription(reg.name(), true).await;
3286 }
3287 }
3288
3289 async fn on_console_event(&self, msg: crate::protocol::ConsoleMessage) {
3290 // Accumulate message for console_messages() accessor
3291 self.console_messages_log.lock().unwrap().push(msg.clone());
3292 self.console.dispatch(msg).await;
3293 }
3294
3295 /// Dispatches a FileChooser event to registered handlers and one-shot waiters.
3296 async fn on_filechooser_event(&self, chooser: crate::protocol::FileChooser) {
3297 self.filechooser.dispatch(chooser).await;
3298 }
3299
3300 /// Triggers load event (called by Frame when loadstate "load" is added)
3301 pub(crate) async fn trigger_load_event(&self) {
3302 self.on_load_event().await;
3303 }
3304
3305 /// Triggers pageError event (called by BrowserContext when pageError arrives)
3306 pub(crate) async fn trigger_pageerror_event(&self, message: String) {
3307 self.on_pageerror_event(message).await;
3308 }
3309
3310 /// Triggers popup event (called by BrowserContext when a page is opened with an opener)
3311 pub(crate) async fn trigger_popup_event(&self, popup: Page) {
3312 self.on_popup_event(popup).await;
3313 }
3314
3315 /// Triggers frameNavigated event (called by Frame when "navigated" is received)
3316 pub(crate) async fn trigger_framenavigated_event(&self, frame: crate::protocol::Frame) {
3317 self.on_framenavigated_event(frame).await;
3318 }
3319
3320 async fn on_close_event(&self) {
3321 self.close.dispatch_all(()).await;
3322 }
3323
3324 async fn on_load_event(&self) {
3325 self.load.dispatch_all(()).await;
3326 }
3327
3328 async fn on_crash_event(&self) {
3329 self.crash.dispatch_all(()).await;
3330 }
3331
3332 async fn on_pageerror_event(&self, message: String) {
3333 // Accumulate error for page_errors() accessor
3334 self.page_errors_log.lock().unwrap().push(message.clone());
3335 self.pageerror.dispatch(message).await;
3336 }
3337
3338 async fn on_popup_event(&self, popup: Page) {
3339 self.popup.dispatch(popup).await;
3340 }
3341
3342 async fn on_frameattached_event(&self, frame: crate::protocol::Frame) {
3343 self.frameattached.dispatch(frame).await;
3344 }
3345
3346 async fn on_framedetached_event(&self, frame: crate::protocol::Frame) {
3347 self.framedetached.dispatch(frame).await;
3348 }
3349
3350 async fn on_framenavigated_event(&self, frame: crate::protocol::Frame) {
3351 self.framenavigated.dispatch(frame).await;
3352 }
3353
3354 /// Adds a `<style>` tag into the page with the desired content.
3355 ///
3356 /// # Arguments
3357 ///
3358 /// * `options` - Style tag options (content, url, or path)
3359 ///
3360 /// # Returns
3361 ///
3362 /// Returns an ElementHandle pointing to the injected `<style>` tag
3363 ///
3364 /// # Example
3365 ///
3366 /// ```no_run
3367 /// # use playwright_rs::protocol::Playwright;
3368 /// # #[tokio::main]
3369 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3370 /// # let playwright = Playwright::launch().await?;
3371 /// # let browser = playwright.chromium().launch().await?;
3372 /// # let context = browser.new_context().await?;
3373 /// # let page = context.new_page().await?;
3374 /// use playwright_rs::protocol::AddStyleTagOptions;
3375 ///
3376 /// // With inline CSS
3377 /// page.add_style_tag(
3378 /// AddStyleTagOptions::builder()
3379 /// .content("body { background-color: red; }")
3380 /// .build()
3381 /// ).await?;
3382 ///
3383 /// // With external URL
3384 /// page.add_style_tag(
3385 /// AddStyleTagOptions::builder()
3386 /// .url("https://example.com/style.css")
3387 /// .build()
3388 /// ).await?;
3389 ///
3390 /// // From file
3391 /// page.add_style_tag(
3392 /// AddStyleTagOptions::builder()
3393 /// .path("./styles/custom.css")
3394 /// .build()
3395 /// ).await?;
3396 /// # Ok(())
3397 /// # }
3398 /// ```
3399 ///
3400 /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
3401 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3402 pub async fn add_style_tag(
3403 &self,
3404 options: AddStyleTagOptions,
3405 ) -> Result<Arc<crate::protocol::ElementHandle>> {
3406 let frame = self.main_frame().await?;
3407 frame.add_style_tag(options).await
3408 }
3409
3410 /// Adds a script which would be evaluated in one of the following scenarios:
3411 /// - Whenever the page is navigated
3412 /// - Whenever a child frame is attached or navigated
3413 ///
3414 /// The script is evaluated after the document was created but before any of its scripts were run.
3415 ///
3416 /// # Arguments
3417 ///
3418 /// * `script` - JavaScript code to be injected into the page
3419 ///
3420 /// # Example
3421 ///
3422 /// ```no_run
3423 /// # use playwright_rs::protocol::Playwright;
3424 /// # #[tokio::main]
3425 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3426 /// # let playwright = Playwright::launch().await?;
3427 /// # let browser = playwright.chromium().launch().await?;
3428 /// # let context = browser.new_context().await?;
3429 /// # let page = context.new_page().await?;
3430 /// page.add_init_script("window.injected = 123;").await?;
3431 /// # Ok(())
3432 /// # }
3433 /// ```
3434 ///
3435 /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
3436 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3437 pub async fn add_init_script(&self, script: &str) -> Result<()> {
3438 self.channel()
3439 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
3440 .await
3441 }
3442
3443 /// Installs an opt-in fake of the File System Access API
3444 /// (`showSaveFilePicker` / `showOpenFilePicker`) on this page, so
3445 /// save/open flows are testable without a native picker dialog.
3446 ///
3447 /// Returns a [`FakeFileSystem`](crate::testing::FakeFileSystem) handle
3448 /// for seeding openable files, reading back saved bytes, and controlling
3449 /// the permission state. Install before the flow under test runs; see
3450 /// the [`testing`](crate::testing) module docs for the pattern. Pages
3451 /// that never call this keep the browser's real picker functions.
3452 ///
3453 /// This is a playwright-rs convenience with no upstream Playwright
3454 /// equivalent (upstream cannot drive the native pickers either; see
3455 /// <https://github.com/microsoft/playwright/issues/11288>).
3456 ///
3457 /// # Errors
3458 ///
3459 /// Returns an error if the page is closed or installing the script
3460 /// fails.
3461 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3462 pub async fn fake_file_system(&self) -> Result<crate::testing::FakeFileSystem> {
3463 crate::testing::FakeFileSystem::install(self).await
3464 }
3465
3466 /// Sets the viewport size for the page.
3467 ///
3468 /// This method allows dynamic resizing of the viewport after page creation,
3469 /// useful for testing responsive layouts at different screen sizes.
3470 ///
3471 /// # Arguments
3472 ///
3473 /// * `viewport` - The viewport dimensions (width and height in pixels)
3474 ///
3475 /// # Example
3476 ///
3477 /// ```no_run
3478 /// # use playwright_rs::protocol::{Playwright, Viewport};
3479 /// # #[tokio::main]
3480 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3481 /// # let playwright = Playwright::launch().await?;
3482 /// # let browser = playwright.chromium().launch().await?;
3483 /// # let page = browser.new_page().await?;
3484 /// // Set viewport to mobile size
3485 /// let mobile = Viewport {
3486 /// width: 375,
3487 /// height: 667,
3488 /// };
3489 /// page.set_viewport_size(mobile).await?;
3490 ///
3491 /// // Later, test desktop layout
3492 /// let desktop = Viewport {
3493 /// width: 1920,
3494 /// height: 1080,
3495 /// };
3496 /// page.set_viewport_size(desktop).await?;
3497 /// # Ok(())
3498 /// # }
3499 /// ```
3500 ///
3501 /// # Errors
3502 ///
3503 /// Returns error if:
3504 /// - Page has been closed
3505 /// - Communication with browser process fails
3506 ///
3507 /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
3508 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3509 pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
3510 // Store the new viewport locally so viewport_size() can reflect the change
3511 if let Ok(mut guard) = self.viewport.write() {
3512 *guard = Some(viewport.clone());
3513 }
3514 self.channel()
3515 .send_no_result(
3516 "setViewportSize",
3517 serde_json::json!({ "viewportSize": viewport }),
3518 )
3519 .await
3520 }
3521
3522 /// Brings this page to the front (activates the tab).
3523 ///
3524 /// Activates the page in the browser, making it the focused tab. This is
3525 /// useful in multi-page tests to ensure actions target the correct page.
3526 ///
3527 /// # Errors
3528 ///
3529 /// Returns error if:
3530 /// - Page has been closed
3531 /// - Communication with browser process fails
3532 ///
3533 /// See: <https://playwright.dev/docs/api/class-page#page-bring-to-front>
3534 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3535 pub async fn bring_to_front(&self) -> Result<()> {
3536 self.channel()
3537 .send_no_result("bringToFront", serde_json::json!({}))
3538 .await
3539 }
3540
3541 /// Clears all element highlights drawn by [`Locator::highlight`](crate::protocol::Locator::highlight).
3542 ///
3543 /// See: <https://playwright.dev/docs/api/class-page#page-hide-highlight>
3544 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3545 pub async fn hide_highlight(&self) -> Result<()> {
3546 self.channel()
3547 .send_no_result("hideHighlight", serde_json::json!({}))
3548 .await
3549 }
3550
3551 /// Forces garbage collection in the browser (Chromium only).
3552 ///
3553 /// See: <https://playwright.dev/docs/api/class-page#page-request-gc>
3554 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3555 pub async fn request_gc(&self) -> Result<()> {
3556 self.channel()
3557 .send_no_result("requestGC", serde_json::json!({}))
3558 .await
3559 }
3560
3561 /// Enters Playwright Inspector's interactive picker mode and resolves
3562 /// once the user clicks an element. The returned [`Locator`](crate::Locator) points at
3563 /// whatever element was clicked.
3564 ///
3565 /// This is the programmatic entry point to the same picker the
3566 /// Playwright Inspector and codegen tools use. It only resolves after
3567 /// a real DOM click — synthetic clicks (e.g. via `page.mouse.click`)
3568 /// do **not** complete the picker. To abort the picker without a
3569 /// click, call [`Page::cancel_pick_locator`] from a different async
3570 /// context.
3571 ///
3572 /// See: <https://playwright.dev/docs/api/class-page#page-pick-locator>
3573 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
3574 pub async fn pick_locator(&self) -> Result<crate::protocol::Locator> {
3575 #[derive(serde::Deserialize)]
3576 struct PickLocatorResponse {
3577 selector: String,
3578 }
3579 let response: PickLocatorResponse = self
3580 .channel()
3581 .send("pickLocator", serde_json::json!({}))
3582 .await?;
3583 Ok(self.locator(&response.selector))
3584 }
3585
3586 /// Cancels an in-progress [`Page::pick_locator`] call. Has no effect
3587 /// if the picker is not currently active.
3588 ///
3589 /// See: <https://playwright.dev/docs/api/class-page#page-cancel-pick-locator>
3590 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3591 pub async fn cancel_pick_locator(&self) -> Result<()> {
3592 self.channel()
3593 .send_no_result("cancelPickLocator", serde_json::json!({}))
3594 .await
3595 }
3596
3597 /// Sets extra HTTP headers that will be sent with every request from this page.
3598 ///
3599 /// These headers are sent in addition to headers set on the browser context via
3600 /// `BrowserContext::set_extra_http_headers()`. Page-level headers take precedence
3601 /// over context-level headers when names conflict.
3602 ///
3603 /// # Arguments
3604 ///
3605 /// * `headers` - Map of header names to values.
3606 ///
3607 /// # Errors
3608 ///
3609 /// Returns error if:
3610 /// - Page has been closed
3611 /// - Communication with browser process fails
3612 ///
3613 /// See: <https://playwright.dev/docs/api/class-page#page-set-extra-http-headers>
3614 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3615 pub async fn set_extra_http_headers(
3616 &self,
3617 headers: std::collections::HashMap<String, String>,
3618 ) -> Result<()> {
3619 // Playwright protocol expects an array of {name, value} objects
3620 // This RPC is sent on the Page channel (not the Frame channel)
3621 let headers_array: Vec<serde_json::Value> = headers
3622 .into_iter()
3623 .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
3624 .collect();
3625 self.channel()
3626 .send_no_result(
3627 "setExtraHTTPHeaders",
3628 serde_json::json!({ "headers": headers_array }),
3629 )
3630 .await
3631 }
3632
3633 /// Emulates media features for the page.
3634 ///
3635 /// This method allows emulating CSS media features such as `media`, `color-scheme`,
3636 /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
3637 ///
3638 /// To reset a specific feature to the browser default, use the `NoOverride` variant.
3639 ///
3640 /// # Arguments
3641 ///
3642 /// * `options` - Optional emulation options. If `None`, this is a no-op.
3643 ///
3644 /// # Example
3645 ///
3646 /// ```no_run
3647 /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
3648 /// # #[tokio::main]
3649 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3650 /// # let playwright = Playwright::launch().await?;
3651 /// # let browser = playwright.chromium().launch().await?;
3652 /// # let page = browser.new_page().await?;
3653 /// // Emulate print media
3654 /// page.emulate_media(Some(
3655 /// EmulateMediaOptions::builder()
3656 /// .media(Media::Print)
3657 /// .build()
3658 /// )).await?;
3659 ///
3660 /// // Emulate dark color scheme
3661 /// page.emulate_media(Some(
3662 /// EmulateMediaOptions::builder()
3663 /// .color_scheme(ColorScheme::Dark)
3664 /// .build()
3665 /// )).await?;
3666 /// # Ok(())
3667 /// # }
3668 /// ```
3669 ///
3670 /// # Errors
3671 ///
3672 /// Returns error if:
3673 /// - Page has been closed
3674 /// - Communication with browser process fails
3675 ///
3676 /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3677 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3678 pub async fn emulate_media(
3679 &self,
3680 options: impl Into<Option<EmulateMediaOptions>>,
3681 ) -> Result<()> {
3682 let options = options.into();
3683 let mut params = serde_json::json!({});
3684
3685 if let Some(opts) = options {
3686 if let Some(media) = opts.media {
3687 params["media"] = serde_json::to_value(media).map_err(|e| {
3688 crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
3689 })?;
3690 }
3691 if let Some(color_scheme) = opts.color_scheme {
3692 params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
3693 crate::error::Error::ProtocolError(format!(
3694 "Failed to serialize colorScheme: {}",
3695 e
3696 ))
3697 })?;
3698 }
3699 if let Some(reduced_motion) = opts.reduced_motion {
3700 params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
3701 crate::error::Error::ProtocolError(format!(
3702 "Failed to serialize reducedMotion: {}",
3703 e
3704 ))
3705 })?;
3706 }
3707 if let Some(forced_colors) = opts.forced_colors {
3708 params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
3709 crate::error::Error::ProtocolError(format!(
3710 "Failed to serialize forcedColors: {}",
3711 e
3712 ))
3713 })?;
3714 }
3715 }
3716
3717 self.channel().send_no_result("emulateMedia", params).await
3718 }
3719
3720 /// Generates a PDF of the page and returns it as bytes.
3721 ///
3722 /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
3723 /// not supported in Firefox or WebKit.
3724 ///
3725 /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
3726 /// saved to that file.
3727 ///
3728 /// # Arguments
3729 ///
3730 /// * `options` - Optional PDF generation options
3731 ///
3732 /// # Example
3733 ///
3734 /// ```no_run
3735 /// # use playwright_rs::protocol::Playwright;
3736 /// # #[tokio::main]
3737 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3738 /// # let playwright = Playwright::launch().await?;
3739 /// # let browser = playwright.chromium().launch().await?;
3740 /// # let page = browser.new_page().await?;
3741 /// let pdf_bytes = page.pdf(None).await?;
3742 /// assert!(!pdf_bytes.is_empty());
3743 /// # Ok(())
3744 /// # }
3745 /// ```
3746 ///
3747 /// # Errors
3748 ///
3749 /// Returns error if:
3750 /// - The browser is not Chromium (PDF only supported in Chromium)
3751 /// - Page has been closed
3752 /// - Communication with browser process fails
3753 ///
3754 /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
3755 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid(), bytes_len = tracing::field::Empty))]
3756 pub async fn pdf(&self, options: impl Into<Option<PdfOptions>>) -> Result<Vec<u8>> {
3757 let options = options.into();
3758 let mut params = serde_json::json!({});
3759 let mut save_path: Option<std::path::PathBuf> = None;
3760
3761 if let Some(opts) = options {
3762 // Capture the file path before consuming opts
3763 save_path = opts.path;
3764
3765 if let Some(scale) = opts.scale {
3766 params["scale"] = serde_json::json!(scale);
3767 }
3768 if let Some(v) = opts.display_header_footer {
3769 params["displayHeaderFooter"] = serde_json::json!(v);
3770 }
3771 if let Some(v) = opts.header_template {
3772 params["headerTemplate"] = serde_json::json!(v);
3773 }
3774 if let Some(v) = opts.footer_template {
3775 params["footerTemplate"] = serde_json::json!(v);
3776 }
3777 if let Some(v) = opts.print_background {
3778 params["printBackground"] = serde_json::json!(v);
3779 }
3780 if let Some(v) = opts.landscape {
3781 params["landscape"] = serde_json::json!(v);
3782 }
3783 if let Some(v) = opts.page_ranges {
3784 params["pageRanges"] = serde_json::json!(v);
3785 }
3786 if let Some(v) = opts.format {
3787 params["format"] = serde_json::json!(v);
3788 }
3789 if let Some(v) = opts.width {
3790 params["width"] = serde_json::json!(v);
3791 }
3792 if let Some(v) = opts.height {
3793 params["height"] = serde_json::json!(v);
3794 }
3795 if let Some(v) = opts.prefer_css_page_size {
3796 params["preferCSSPageSize"] = serde_json::json!(v);
3797 }
3798 if let Some(margin) = opts.margin {
3799 params["margin"] = serde_json::to_value(margin).map_err(|e| {
3800 crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
3801 })?;
3802 }
3803 }
3804
3805 #[derive(Deserialize)]
3806 struct PdfResponse {
3807 pdf: String,
3808 }
3809
3810 let response: PdfResponse = self.channel().send("pdf", params).await?;
3811
3812 // Decode base64 to bytes
3813 let pdf_bytes = base64::engine::general_purpose::STANDARD
3814 .decode(&response.pdf)
3815 .map_err(|e| {
3816 crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
3817 })?;
3818
3819 // If a path was specified, save the PDF to disk as well
3820 if let Some(path) = save_path {
3821 tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
3822 crate::error::Error::InvalidArgument(format!(
3823 "Failed to write PDF to '{}': {}",
3824 path.display(),
3825 e
3826 ))
3827 })?;
3828 }
3829
3830 tracing::Span::current().record("bytes_len", pdf_bytes.len());
3831 Ok(pdf_bytes)
3832 }
3833
3834 /// Adds a `<script>` tag into the page with the desired URL or content.
3835 ///
3836 /// # Arguments
3837 ///
3838 /// * `options` - Optional script tag options (content, url, or path).
3839 /// If `None`, returns an error because no source is specified.
3840 ///
3841 /// At least one of `content`, `url`, or `path` must be provided.
3842 ///
3843 /// # Example
3844 ///
3845 /// ```no_run
3846 /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
3847 /// # #[tokio::main]
3848 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3849 /// # let playwright = Playwright::launch().await?;
3850 /// # let browser = playwright.chromium().launch().await?;
3851 /// # let context = browser.new_context().await?;
3852 /// # let page = context.new_page().await?;
3853 /// // With inline JavaScript
3854 /// page.add_script_tag(Some(
3855 /// AddScriptTagOptions::builder()
3856 /// .content("window.myVar = 42;")
3857 /// .build()
3858 /// )).await?;
3859 ///
3860 /// // With external URL
3861 /// page.add_script_tag(Some(
3862 /// AddScriptTagOptions::builder()
3863 /// .url("https://example.com/script.js")
3864 /// .build()
3865 /// )).await?;
3866 /// # Ok(())
3867 /// # }
3868 /// ```
3869 ///
3870 /// # Errors
3871 ///
3872 /// Returns error if:
3873 /// - `options` is `None` or no content/url/path is specified
3874 /// - Page has been closed
3875 /// - Script loading fails (e.g., invalid URL)
3876 ///
3877 /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
3878 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
3879 pub async fn add_script_tag(
3880 &self,
3881 options: impl Into<Option<AddScriptTagOptions>>,
3882 ) -> Result<Arc<crate::protocol::ElementHandle>> {
3883 let options = options.into();
3884 let opts = options.ok_or_else(|| {
3885 Error::InvalidArgument(
3886 "At least one of content, url, or path must be specified".to_string(),
3887 )
3888 })?;
3889 let frame = self.main_frame().await?;
3890 frame.add_script_tag(opts).await
3891 }
3892
3893 /// Returns the current viewport size of the page, or `None` if no viewport is set.
3894 ///
3895 /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
3896 /// returns the dimensions configured at context creation time or updated via
3897 /// `set_viewport_size()`.
3898 ///
3899 /// # Example
3900 ///
3901 /// ```no_run
3902 /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
3903 /// # #[tokio::main]
3904 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3905 /// # let playwright = Playwright::launch().await?;
3906 /// # let browser = playwright.chromium().launch().await?;
3907 /// let context = browser.new_context_with_options(
3908 /// BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
3909 /// ).await?;
3910 /// let page = context.new_page().await?;
3911 /// let size = page.viewport_size().expect("Viewport should be set");
3912 /// assert_eq!(size.width, 1280);
3913 /// assert_eq!(size.height, 720);
3914 /// # Ok(())
3915 /// # }
3916 /// ```
3917 ///
3918 /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
3919 pub fn viewport_size(&self) -> Option<Viewport> {
3920 self.viewport.read().ok()?.clone()
3921 }
3922
3923 /// Returns the `Accessibility` object for this page.
3924 ///
3925 /// Use `accessibility().snapshot()` to capture the current state of the
3926 /// page's accessibility tree.
3927 ///
3928 /// See: <https://playwright.dev/docs/api/class-page#page-accessibility>
3929 pub fn accessibility(&self) -> crate::protocol::Accessibility {
3930 crate::protocol::Accessibility::new(self.clone())
3931 }
3932
3933 /// Returns the ARIA accessibility tree for the page as a YAML string.
3934 ///
3935 /// Page-level shorthand for `page.locator("body").aria_snapshot(...)`. Useful
3936 /// for asserting page-wide accessibility structure without first selecting
3937 /// `body` explicitly.
3938 ///
3939 /// Pass `Some(AriaSnapshotOptions::default().mode(AriaSnapshotMode::Ai))`
3940 /// to get the AI-friendly form intended for LLM/codegen consumption.
3941 ///
3942 /// See: <https://playwright.dev/docs/api/class-page#page-aria-snapshot>
3943 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
3944 pub async fn aria_snapshot(
3945 &self,
3946 options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
3947 ) -> Result<String> {
3948 let options = options.into();
3949 let frame = self.main_frame().await?;
3950 let timeout = options
3951 .as_ref()
3952 .and_then(|o| o.timeout)
3953 .unwrap_or_else(|| self.default_timeout_ms());
3954 frame
3955 .aria_snapshot_raw("body", timeout, options.as_ref())
3956 .await
3957 }
3958
3959 /// Returns the `Coverage` object for this page (Chromium only).
3960 ///
3961 /// Use `coverage().start_js_coverage()` / `stop_js_coverage()` and
3962 /// `start_css_coverage()` / `stop_css_coverage()` to collect code coverage data.
3963 ///
3964 /// Coverage is only available in Chromium. Calling coverage methods on
3965 /// Firefox or WebKit will return an error from the Playwright server.
3966 ///
3967 /// See: <https://playwright.dev/docs/api/class-page#page-coverage>
3968 pub fn coverage(&self) -> crate::protocol::Coverage {
3969 crate::protocol::Coverage::new(self.clone())
3970 }
3971
3972 /// Returns the live-screencast handle for this page.
3973 ///
3974 /// Register frame handlers via [`Screencast::on_frame`](crate::Screencast::on_frame), then call
3975 /// [`Screencast::start`](crate::Screencast::start) to begin streaming. JPEG frames arrive on
3976 /// the registered handlers as the browser renders.
3977 ///
3978 /// See: <https://playwright.dev/docs/api/class-page#page-screencast>
3979 pub fn screencast(&self) -> crate::protocol::Screencast {
3980 crate::protocol::Screencast::new(self.clone())
3981 }
3982
3983 /// Access the current origin's `localStorage`.
3984 ///
3985 /// See: <https://playwright.dev/docs/api/class-page#page-local-storage>
3986 pub fn local_storage(&self) -> crate::protocol::WebStorage {
3987 crate::protocol::WebStorage::new(
3988 self.channel().clone(),
3989 crate::protocol::WebStorageKind::Local,
3990 )
3991 }
3992
3993 /// Access the current origin's `sessionStorage`.
3994 ///
3995 /// See: <https://playwright.dev/docs/api/class-page#page-session-storage>
3996 pub fn session_storage(&self) -> crate::protocol::WebStorage {
3997 crate::protocol::WebStorage::new(
3998 self.channel().clone(),
3999 crate::protocol::WebStorageKind::Session,
4000 )
4001 }
4002
4003 pub(crate) async fn screencast_start(
4004 &self,
4005 options: crate::protocol::ScreencastStartOptions,
4006 ) -> Result<()> {
4007 let mut params = serde_json::json!({});
4008 if let Some(size) = options.size {
4009 params["size"] = serde_json::json!({
4010 "width": size.width,
4011 "height": size.height,
4012 });
4013 }
4014 if let Some(quality) = options.quality {
4015 params["quality"] = serde_json::json!(quality);
4016 }
4017 let has_handlers = !self.screencast_frame_handlers.lock().unwrap().is_empty();
4018 params["sendFrames"] = serde_json::json!(has_handlers);
4019 let recording = options.path.is_some();
4020 params["record"] = serde_json::json!(recording);
4021
4022 #[derive(serde::Deserialize)]
4023 struct StartResponse {
4024 artifact: Option<serde_json::Value>,
4025 }
4026 let response: StartResponse = self.channel().send("screencastStart", params).await?;
4027
4028 if recording {
4029 *self.screencast_save_path.lock().unwrap() = options.path;
4030 if let Some(artifact_value) = response.artifact
4031 && let Some(guid) = artifact_value.get("guid").and_then(|v| v.as_str())
4032 {
4033 *self.screencast_artifact_guid.lock().unwrap() = Some(guid.to_string());
4034 }
4035 }
4036 Ok(())
4037 }
4038
4039 pub(crate) async fn screencast_stop(&self) -> Result<()> {
4040 self.channel()
4041 .send_no_result("screencastStop", serde_json::json!({}))
4042 .await?;
4043
4044 let path = self.screencast_save_path.lock().unwrap().take();
4045 let artifact_guid = self.screencast_artifact_guid.lock().unwrap().take();
4046 if let (Some(path), Some(guid)) = (path, artifact_guid) {
4047 let artifact = self
4048 .connection()
4049 .get_typed::<crate::protocol::artifact::Artifact>(&guid)
4050 .await?;
4051 artifact.save_as(path.to_string_lossy().as_ref()).await?;
4052 }
4053 Ok(())
4054 }
4055
4056 pub(crate) fn screencast_on_frame<F, Fut>(&self, handler: F)
4057 where
4058 F: Fn(crate::protocol::ScreencastFrame) -> Fut + Send + Sync + 'static,
4059 Fut: Future<Output = Result<()>> + Send + 'static,
4060 {
4061 let h: ScreencastFrameHandler = Arc::new(
4062 move |f: crate::protocol::ScreencastFrame| -> ScreencastFrameHandlerFuture {
4063 Box::pin(handler(f))
4064 },
4065 );
4066 self.screencast_frame_handlers.lock().unwrap().push(h);
4067 }
4068
4069 pub(crate) async fn screencast_show_actions(
4070 &self,
4071 options: crate::protocol::ShowActionsOptions,
4072 ) -> Result<()> {
4073 let mut params = serde_json::json!({});
4074 if let Some(d) = options.duration {
4075 params["duration"] = serde_json::json!(d);
4076 }
4077 if let Some(p) = options.position {
4078 params["position"] = serde_json::json!(p.as_str());
4079 }
4080 if let Some(f) = options.font_size {
4081 params["fontSize"] = serde_json::json!(f);
4082 }
4083 if let Some(c) = options.cursor {
4084 params["cursor"] = serde_json::json!(c.as_str());
4085 }
4086 self.channel()
4087 .send_no_result("screencastShowActions", params)
4088 .await
4089 }
4090
4091 pub(crate) async fn screencast_hide_actions(&self) -> Result<()> {
4092 self.channel()
4093 .send_no_result("screencastHideActions", serde_json::json!({}))
4094 .await
4095 }
4096
4097 pub(crate) async fn screencast_chapter(
4098 &self,
4099 title: &str,
4100 options: crate::protocol::ChapterOptions,
4101 ) -> Result<()> {
4102 let mut params = serde_json::json!({ "title": title });
4103 if let Some(desc) = options.description {
4104 params["description"] = serde_json::json!(desc);
4105 }
4106 if let Some(d) = options.duration {
4107 params["duration"] = serde_json::json!(d);
4108 }
4109 self.channel()
4110 .send_no_result("screencastChapter", params)
4111 .await
4112 }
4113
4114 pub(crate) async fn screencast_show_overlay(
4115 &self,
4116 html: &str,
4117 options: crate::protocol::ShowOverlayOptions,
4118 ) -> Result<crate::protocol::OverlayId> {
4119 let mut params = serde_json::json!({ "html": html });
4120 if let Some(d) = options.duration {
4121 params["duration"] = serde_json::json!(d);
4122 }
4123 #[derive(serde::Deserialize)]
4124 struct OverlayResponse {
4125 id: String,
4126 }
4127 let response: OverlayResponse =
4128 self.channel().send("screencastShowOverlay", params).await?;
4129 Ok(crate::protocol::OverlayId(response.id))
4130 }
4131
4132 pub(crate) async fn screencast_remove_overlay(
4133 &self,
4134 id: crate::protocol::OverlayId,
4135 ) -> Result<()> {
4136 self.channel()
4137 .send_no_result("screencastRemoveOverlay", serde_json::json!({ "id": id.0 }))
4138 .await
4139 }
4140
4141 pub(crate) async fn screencast_set_overlay_visible(&self, visible: bool) -> Result<()> {
4142 self.channel()
4143 .send_no_result(
4144 "screencastSetOverlayVisible",
4145 serde_json::json!({ "visible": visible }),
4146 )
4147 .await
4148 }
4149
4150 // Internal accessibility method (called by Accessibility struct)
4151 //
4152 // The legacy `accessibilitySnapshot` RPC was removed in modern Playwright.
4153 // We implement snapshot() using `FrameAriaSnapshot` on the main frame, which
4154 // returns the ARIA accessibility tree as a YAML string (the current equivalent).
4155 // The YAML string is returned as a JSON string Value for API compatibility.
4156
4157 pub(crate) async fn accessibility_snapshot(
4158 &self,
4159 _options: Option<crate::protocol::accessibility::AccessibilitySnapshotOptions>,
4160 ) -> Result<serde_json::Value> {
4161 let frame = self.main_frame().await?;
4162 let timeout = self.default_timeout_ms();
4163 let snapshot = frame.aria_snapshot_raw("body", timeout, None).await?;
4164 Ok(serde_json::Value::String(snapshot))
4165 }
4166
4167 // Internal coverage methods (called by Coverage struct)
4168
4169 pub(crate) async fn coverage_start_js(
4170 &self,
4171 options: Option<crate::protocol::coverage::StartJSCoverageOptions>,
4172 ) -> Result<()> {
4173 let mut params = serde_json::json!({});
4174
4175 if let Some(opts) = options {
4176 if let Some(v) = opts.reset_on_navigation {
4177 params["resetOnNavigation"] = serde_json::json!(v);
4178 }
4179 if let Some(v) = opts.report_anonymous_scripts {
4180 params["reportAnonymousScripts"] = serde_json::json!(v);
4181 }
4182 }
4183
4184 self.channel()
4185 .send_no_result("startJSCoverage", params)
4186 .await
4187 }
4188
4189 pub(crate) async fn coverage_stop_js(
4190 &self,
4191 ) -> Result<Vec<crate::protocol::coverage::JSCoverageEntry>> {
4192 #[derive(serde::Deserialize)]
4193 struct StopJSCoverageResponse {
4194 entries: Vec<crate::protocol::coverage::JSCoverageEntry>,
4195 }
4196
4197 let response: StopJSCoverageResponse = self
4198 .channel()
4199 .send("stopJSCoverage", serde_json::json!({}))
4200 .await?;
4201
4202 Ok(response.entries)
4203 }
4204
4205 pub(crate) async fn coverage_start_css(
4206 &self,
4207 options: Option<crate::protocol::coverage::StartCSSCoverageOptions>,
4208 ) -> Result<()> {
4209 let mut params = serde_json::json!({});
4210
4211 if let Some(opts) = options
4212 && let Some(v) = opts.reset_on_navigation
4213 {
4214 params["resetOnNavigation"] = serde_json::json!(v);
4215 }
4216
4217 self.channel()
4218 .send_no_result("startCSSCoverage", params)
4219 .await
4220 }
4221
4222 pub(crate) async fn coverage_stop_css(
4223 &self,
4224 ) -> Result<Vec<crate::protocol::coverage::CSSCoverageEntry>> {
4225 #[derive(serde::Deserialize)]
4226 struct StopCSSCoverageResponse {
4227 entries: Vec<crate::protocol::coverage::CSSCoverageEntry>,
4228 }
4229
4230 let response: StopCSSCoverageResponse = self
4231 .channel()
4232 .send("stopCSSCoverage", serde_json::json!({}))
4233 .await?;
4234
4235 Ok(response.entries)
4236 }
4237}
4238
4239impl ChannelOwner for Page {
4240 fn guid(&self) -> &str {
4241 self.base.guid()
4242 }
4243
4244 fn type_name(&self) -> &str {
4245 self.base.type_name()
4246 }
4247
4248 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
4249 self.base.parent()
4250 }
4251
4252 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
4253 self.base.connection()
4254 }
4255
4256 fn initializer(&self) -> &Value {
4257 self.base.initializer()
4258 }
4259
4260 fn channel(&self) -> &Channel {
4261 self.base.channel()
4262 }
4263
4264 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
4265 self.base.dispose(reason)
4266 }
4267
4268 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
4269 self.base.adopt(child)
4270 }
4271
4272 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
4273 self.base.add_child(guid, child)
4274 }
4275
4276 fn remove_child(&self, guid: &str) {
4277 self.base.remove_child(guid)
4278 }
4279
4280 fn on_event(&self, method: &str, params: Value) {
4281 match method {
4282 "navigated" => {
4283 // The main frame tracks navigation; nothing to update here.
4284 }
4285 "route" => {
4286 // Handle network routing event
4287 if let Some(route_guid) = params
4288 .get("route")
4289 .and_then(|v| v.get("guid"))
4290 .and_then(|v| v.as_str())
4291 {
4292 // Get the Route object from connection's registry
4293 let connection = self.connection();
4294 let route_guid_owned = route_guid.to_string();
4295 let self_clone = self.clone();
4296
4297 tokio::spawn(
4298 async move {
4299 // Get and downcast Route object
4300 let route: Route =
4301 match connection.get_typed::<Route>(&route_guid_owned).await {
4302 Ok(r) => r,
4303 Err(e) => {
4304 tracing::warn!("Failed to get route object: {}", e);
4305 return;
4306 }
4307 };
4308
4309 // Set APIRequestContext on the route for fetch() support.
4310 // Page's parent is BrowserContext, which has the request context.
4311 if let Some(ctx) =
4312 downcast_parent::<crate::protocol::BrowserContext>(&self_clone)
4313 && let Ok(api_ctx) = ctx.request().await
4314 {
4315 route.set_api_request_context(api_ctx);
4316 }
4317
4318 // Call the route handler and wait for completion
4319 self_clone.on_route_event(route).await;
4320 }
4321 .in_current_span(),
4322 );
4323 }
4324 }
4325 "download" => {
4326 // Handle download event
4327 // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
4328 let url = params
4329 .get("url")
4330 .and_then(|v| v.as_str())
4331 .unwrap_or("")
4332 .to_string();
4333
4334 let suggested_filename = params
4335 .get("suggestedFilename")
4336 .and_then(|v| v.as_str())
4337 .unwrap_or("")
4338 .to_string();
4339
4340 if let Some(artifact_guid) = params
4341 .get("artifact")
4342 .and_then(|v| v.get("guid"))
4343 .and_then(|v| v.as_str())
4344 {
4345 let connection = self.connection();
4346 let artifact_guid_owned = artifact_guid.to_string();
4347 let self_clone = self.clone();
4348
4349 tokio::spawn(
4350 async move {
4351 // Wait for Artifact object to be created
4352 let artifact_arc =
4353 match connection.get_object(&artifact_guid_owned).await {
4354 Ok(obj) => obj,
4355 Err(e) => {
4356 tracing::warn!("Failed to get artifact object: {}", e);
4357 return;
4358 }
4359 };
4360
4361 // Create Download wrapper from Artifact + event params
4362 let download = Download::from_artifact(
4363 artifact_arc,
4364 url,
4365 suggested_filename,
4366 self_clone.clone(),
4367 );
4368
4369 // Call the download handlers
4370 self_clone.on_download_event(download).await;
4371 }
4372 .in_current_span(),
4373 );
4374 }
4375 }
4376 "dialog" => {
4377 // Dialog events are handled by BrowserContext and forwarded to Page
4378 // This case should not be reached, but keeping for completeness
4379 }
4380 "webSocket" => {
4381 if let Some(ws_guid) = params
4382 .get("webSocket")
4383 .and_then(|v| v.get("guid"))
4384 .and_then(|v| v.as_str())
4385 {
4386 let connection = self.connection();
4387 let ws_guid_owned = ws_guid.to_string();
4388 let self_clone = self.clone();
4389
4390 tokio::spawn(
4391 async move {
4392 // Get and downcast WebSocket object
4393 let ws: WebSocket =
4394 match connection.get_typed::<WebSocket>(&ws_guid_owned).await {
4395 Ok(ws) => ws,
4396 Err(e) => {
4397 tracing::warn!("Failed to get WebSocket object: {}", e);
4398 return;
4399 }
4400 };
4401
4402 // Call handlers
4403 let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
4404 for handler in handlers {
4405 let ws_clone = ws.clone();
4406 tokio::spawn(
4407 async move {
4408 if let Err(e) = handler(ws_clone).await {
4409 tracing::error!("Error in websocket handler: {}", e);
4410 }
4411 }
4412 .in_current_span(),
4413 );
4414 }
4415 }
4416 .in_current_span(),
4417 );
4418 }
4419 }
4420 "webSocketRoute" => {
4421 // A WebSocket matched a route_web_socket pattern.
4422 // Event format: {webSocketRoute: {guid: "WebSocketRoute@..."}}
4423 if let Some(wsr_guid) = params
4424 .get("webSocketRoute")
4425 .and_then(|v| v.get("guid"))
4426 .and_then(|v| v.as_str())
4427 {
4428 let connection = self.connection();
4429 let wsr_guid_owned = wsr_guid.to_string();
4430 let self_clone = self.clone();
4431
4432 tokio::spawn(
4433 async move {
4434 let route: crate::protocol::WebSocketRoute = match connection
4435 .get_typed::<crate::protocol::WebSocketRoute>(&wsr_guid_owned)
4436 .await
4437 {
4438 Ok(r) => r,
4439 Err(e) => {
4440 tracing::warn!("Failed to get WebSocketRoute object: {}", e);
4441 return;
4442 }
4443 };
4444
4445 let url = route.url().to_string();
4446 let handlers = self_clone.ws_route_handlers.lock().unwrap().clone();
4447 for entry in handlers.iter().rev() {
4448 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
4449 let handler = entry.handler.clone();
4450 let route_clone = route.clone();
4451 tokio::spawn(
4452 async move {
4453 if let Err(e) = handler(route_clone).await {
4454 tracing::error!(
4455 "Error in webSocketRoute handler: {}",
4456 e
4457 );
4458 }
4459 }
4460 .in_current_span(),
4461 );
4462 break;
4463 }
4464 }
4465 }
4466 .in_current_span(),
4467 );
4468 }
4469 }
4470 "worker" => {
4471 // A new Web Worker was created in the page.
4472 // Event format: {worker: {guid: "Worker@..."}}
4473 if let Some(worker_guid) = params
4474 .get("worker")
4475 .and_then(|v| v.get("guid"))
4476 .and_then(|v| v.as_str())
4477 {
4478 let connection = self.connection();
4479 let worker_guid_owned = worker_guid.to_string();
4480 let self_clone = self.clone();
4481
4482 tokio::spawn(
4483 async move {
4484 let worker: Worker =
4485 match connection.get_typed::<Worker>(&worker_guid_owned).await {
4486 Ok(w) => w,
4487 Err(e) => {
4488 tracing::warn!("Failed to get Worker object: {}", e);
4489 return;
4490 }
4491 };
4492
4493 // Track the worker for workers() accessor
4494 self_clone.workers_list.lock().unwrap().push(worker.clone());
4495
4496 let handlers = self_clone.worker_handlers.lock().unwrap().clone();
4497 for handler in handlers {
4498 let worker_clone = worker.clone();
4499 tokio::spawn(
4500 async move {
4501 if let Err(e) = handler(worker_clone).await {
4502 tracing::error!("Error in worker handler: {}", e);
4503 }
4504 }
4505 .in_current_span(),
4506 );
4507 }
4508 // Notify expect_event("worker") waiters
4509 if let Some(tx) = self_clone.worker_waiters.lock().unwrap().pop() {
4510 let _ = tx.send(worker);
4511 }
4512 }
4513 .in_current_span(),
4514 );
4515 }
4516 }
4517 "bindingCall" => {
4518 // A JS caller on this page invoked a page-level exposed function.
4519 // Event format: {binding: {guid: "..."}}
4520 if let Some(binding_guid) = params
4521 .get("binding")
4522 .and_then(|v| v.get("guid"))
4523 .and_then(|v| v.as_str())
4524 {
4525 let connection = self.connection();
4526 let binding_guid_owned = binding_guid.to_string();
4527 let binding_callbacks = self.binding_callbacks.clone();
4528
4529 tokio::spawn(async move {
4530 let binding_call: crate::protocol::BindingCall = match connection
4531 .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
4532 .await
4533 {
4534 Ok(bc) => bc,
4535 Err(e) => {
4536 tracing::warn!("Failed to get BindingCall object: {}", e);
4537 return;
4538 }
4539 };
4540
4541 let name = binding_call.name().to_string();
4542
4543 // Look up page-level callback
4544 let callback = {
4545 let callbacks = binding_callbacks.lock().unwrap();
4546 callbacks.get(&name).cloned()
4547 };
4548
4549 let Some(callback) = callback else {
4550 // No page-level handler — the context-level handler on
4551 // BrowserContext::on_event("bindingCall") will handle it.
4552 return;
4553 };
4554
4555 // Deserialize args from Playwright protocol format
4556 let raw_args = binding_call.args();
4557 let args = crate::protocol::browser_context::BrowserContext::deserialize_binding_args_pub(raw_args);
4558
4559 // Call callback and serialize result
4560 let result_value = callback(args).await;
4561 let serialized =
4562 crate::protocol::evaluate_conversion::serialize_argument(&result_value);
4563
4564 if let Err(e) = binding_call.resolve(serialized).await {
4565 tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
4566 }
4567 }.in_current_span());
4568 }
4569 }
4570 "fileChooser" => {
4571 // FileChooser event: sent when an <input type="file"> is interacted with.
4572 // Event params: {element: {guid: "..."}, isMultiple: bool}
4573 let is_multiple = params
4574 .get("isMultiple")
4575 .and_then(|v| v.as_bool())
4576 .unwrap_or(false);
4577
4578 if let Some(element_guid) = params
4579 .get("element")
4580 .and_then(|v| v.get("guid"))
4581 .and_then(|v| v.as_str())
4582 {
4583 let connection = self.connection();
4584 let element_guid_owned = element_guid.to_string();
4585 let self_clone = self.clone();
4586
4587 tokio::spawn(
4588 async move {
4589 let element: crate::protocol::ElementHandle = match connection
4590 .get_typed::<crate::protocol::ElementHandle>(&element_guid_owned)
4591 .await
4592 {
4593 Ok(e) => e,
4594 Err(err) => {
4595 tracing::warn!(
4596 "Failed to get ElementHandle for fileChooser: {}",
4597 err
4598 );
4599 return;
4600 }
4601 };
4602
4603 let chooser = crate::protocol::FileChooser::new(
4604 self_clone.clone(),
4605 std::sync::Arc::new(element),
4606 is_multiple,
4607 );
4608
4609 self_clone.on_filechooser_event(chooser).await;
4610 }
4611 .in_current_span(),
4612 );
4613 }
4614 }
4615 "close" => {
4616 // Server-initiated close (e.g. context was closed)
4617 self.is_closed.store(true, Ordering::Relaxed);
4618 // Dispatch close handlers
4619 let self_clone = self.clone();
4620 tokio::spawn(
4621 async move {
4622 self_clone.on_close_event().await;
4623 }
4624 .in_current_span(),
4625 );
4626 }
4627 "load" => {
4628 let self_clone = self.clone();
4629 tokio::spawn(
4630 async move {
4631 self_clone.on_load_event().await;
4632 }
4633 .in_current_span(),
4634 );
4635 }
4636 "crash" => {
4637 let self_clone = self.clone();
4638 tokio::spawn(
4639 async move {
4640 self_clone.on_crash_event().await;
4641 }
4642 .in_current_span(),
4643 );
4644 }
4645 "pageError" => {
4646 // params: {"error": {"message": "...", "stack": "..."}}
4647 let message = params
4648 .get("error")
4649 .and_then(|e| e.get("message"))
4650 .and_then(|m| m.as_str())
4651 .unwrap_or("")
4652 .to_string();
4653 let self_clone = self.clone();
4654 tokio::spawn(
4655 async move {
4656 self_clone.on_pageerror_event(message).await;
4657 }
4658 .in_current_span(),
4659 );
4660 }
4661 "screencastFrame" => {
4662 // params: {"frameId": <int>, "data": "<base64 jpeg>", ...}
4663 //
4664 // Playwright 1.62 made frame delivery flow-controlled: the
4665 // driver sends a bounded number of frames and then waits for
4666 // `screencastFrameAck` before sending more. Without the ack a
4667 // live screencast delivers a handful of frames and then goes
4668 // silent forever, which looks like the page stopped animating
4669 // rather than like a protocol error. Ack as soon as the frame
4670 // is taken, not after the handlers finish, so a slow handler
4671 // throttles nothing.
4672 if let Some(frame_id) = params.get("frameId").and_then(|v| v.as_i64()) {
4673 let self_clone = self.clone();
4674 tokio::spawn(
4675 async move {
4676 if let Err(e) = self_clone
4677 .channel()
4678 .send::<_, serde_json::Value>(
4679 "screencastFrameAck",
4680 serde_json::json!({ "frameId": frame_id }),
4681 )
4682 .await
4683 {
4684 tracing::warn!("Failed to ack screencast frame: {}", e);
4685 }
4686 }
4687 .in_current_span(),
4688 );
4689 }
4690
4691 if let Some(b64) = params.get("data").and_then(|v| v.as_str()) {
4692 if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(b64) {
4693 // Wrap once in `Bytes`; each handler-clone below is a refcount bump.
4694 let frame = crate::protocol::ScreencastFrame {
4695 data: bytes::Bytes::from(bytes),
4696 timestamp: params.get("timestamp").and_then(|v| v.as_f64()),
4697 };
4698 let handlers = self.screencast_frame_handlers.lock().unwrap().clone();
4699 for h in handlers {
4700 let f = frame.clone();
4701 tokio::spawn(
4702 async move {
4703 if let Err(e) = h(f).await {
4704 tracing::warn!("Screencast frame handler error: {}", e);
4705 }
4706 }
4707 .in_current_span(),
4708 );
4709 }
4710 } else {
4711 tracing::warn!("Failed to decode screencast frame data");
4712 }
4713 }
4714 }
4715 // "popup" is forwarded from BrowserContext::on_event when a "page" event
4716 // is received for a page that has an opener. No direct "popup" event on Page.
4717 "frameAttached" => {
4718 // params: {"frame": {"guid": "..."}}
4719 if let Some(frame_guid) = params
4720 .get("frame")
4721 .and_then(|v| v.get("guid"))
4722 .and_then(|v| v.as_str())
4723 {
4724 let connection = self.connection();
4725 let frame_guid_owned = frame_guid.to_string();
4726 let self_clone = self.clone();
4727
4728 tokio::spawn(
4729 async move {
4730 let frame: crate::protocol::Frame = match connection
4731 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
4732 .await
4733 {
4734 Ok(f) => f,
4735 Err(e) => {
4736 tracing::warn!("Failed to get Frame for frameAttached: {}", e);
4737 return;
4738 }
4739 };
4740 self_clone.on_frameattached_event(frame).await;
4741 }
4742 .in_current_span(),
4743 );
4744 }
4745 }
4746 "frameDetached" => {
4747 // params: {"frame": {"guid": "..."}}
4748 if let Some(frame_guid) = params
4749 .get("frame")
4750 .and_then(|v| v.get("guid"))
4751 .and_then(|v| v.as_str())
4752 {
4753 let connection = self.connection();
4754 let frame_guid_owned = frame_guid.to_string();
4755 let self_clone = self.clone();
4756
4757 tokio::spawn(
4758 async move {
4759 let frame: crate::protocol::Frame = match connection
4760 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
4761 .await
4762 {
4763 Ok(f) => f,
4764 Err(e) => {
4765 tracing::warn!("Failed to get Frame for frameDetached: {}", e);
4766 return;
4767 }
4768 };
4769 self_clone.on_framedetached_event(frame).await;
4770 }
4771 .in_current_span(),
4772 );
4773 }
4774 }
4775 "frameNavigated" => {
4776 // params: {"frame": {"guid": "..."}}
4777 // Note: frameNavigated may also contain url, name, etc. at top level
4778 // The frame guid is in the "frame" field (same as attached/detached)
4779 if let Some(frame_guid) = params
4780 .get("frame")
4781 .and_then(|v| v.get("guid"))
4782 .and_then(|v| v.as_str())
4783 {
4784 let connection = self.connection();
4785 let frame_guid_owned = frame_guid.to_string();
4786 let self_clone = self.clone();
4787
4788 tokio::spawn(
4789 async move {
4790 let frame: crate::protocol::Frame = match connection
4791 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
4792 .await
4793 {
4794 Ok(f) => f,
4795 Err(e) => {
4796 tracing::warn!("Failed to get Frame for frameNavigated: {}", e);
4797 return;
4798 }
4799 };
4800 self_clone.on_framenavigated_event(frame).await;
4801 }
4802 .in_current_span(),
4803 );
4804 }
4805 }
4806 "locatorHandlerTriggered" => {
4807 // Server fires this when a registered locator matches an element.
4808 // params: {"uid": N}
4809 if let Some(uid) = params.get("uid").and_then(|v| v.as_u64()).map(|v| v as u32) {
4810 let locator_handlers = self.locator_handlers.clone();
4811 let self_clone = self.clone();
4812
4813 tokio::spawn(
4814 async move {
4815 // Look up handler and decrement times_remaining
4816 let (handler, selector, should_remove) = {
4817 let mut handlers = locator_handlers.lock().unwrap();
4818 let entry = handlers.iter_mut().find(|e| e.uid == uid);
4819 match entry {
4820 None => return,
4821 Some(e) => {
4822 let handler = e.handler.clone();
4823 let selector = e.selector.clone();
4824 let remove = match e.times_remaining {
4825 Some(1) => true,
4826 Some(ref mut n) => {
4827 *n -= 1;
4828 false
4829 }
4830 None => false,
4831 };
4832 (handler, selector, remove)
4833 }
4834 }
4835 };
4836
4837 // Build a Locator for the handler to receive
4838 let locator = self_clone.locator(&selector);
4839
4840 // Run the handler
4841 if let Err(e) = handler(locator).await {
4842 tracing::warn!("locator handler error (uid={}): {}", uid, e);
4843 }
4844
4845 // Send resolveLocatorHandler — remove=true if times exhausted
4846 let _ = self_clone
4847 .channel()
4848 .send_no_result(
4849 "resolveLocatorHandler",
4850 serde_json::json!({ "uid": uid, "remove": should_remove }),
4851 )
4852 .await;
4853
4854 // Remove from local registry if one-shot
4855 if should_remove {
4856 self_clone
4857 .locator_handlers
4858 .lock()
4859 .unwrap()
4860 .retain(|e| e.uid != uid);
4861 }
4862 }
4863 .in_current_span(),
4864 );
4865 }
4866 }
4867 _ => {
4868 // Other events not yet handled
4869 }
4870 }
4871 }
4872
4873 fn was_collected(&self) -> bool {
4874 self.base.was_collected()
4875 }
4876
4877 fn as_any(&self) -> &dyn Any {
4878 self
4879 }
4880}
4881
4882impl std::fmt::Debug for Page {
4883 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4884 f.debug_struct("Page")
4885 .field("guid", &self.guid())
4886 .field("url", &self.url())
4887 .finish()
4888 }
4889}
4890
4891/// Options for page.goto() and page.reload()
4892#[derive(Debug, Clone)]
4893#[non_exhaustive]
4894pub struct GotoOptions {
4895 /// Maximum operation time in milliseconds
4896 pub timeout: Option<std::time::Duration>,
4897 /// When to consider operation succeeded
4898 pub wait_until: Option<WaitUntil>,
4899}
4900
4901impl GotoOptions {
4902 /// Creates new GotoOptions with default values
4903 pub fn new() -> Self {
4904 Self {
4905 timeout: None,
4906 wait_until: None,
4907 }
4908 }
4909
4910 /// Sets the timeout
4911 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
4912 self.timeout = Some(timeout);
4913 self
4914 }
4915
4916 /// Sets the wait_until option
4917 pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
4918 self.wait_until = Some(wait_until);
4919 self
4920 }
4921}
4922
4923impl Default for GotoOptions {
4924 fn default() -> Self {
4925 Self::new()
4926 }
4927}
4928
4929/// When to consider navigation succeeded
4930#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4931#[non_exhaustive]
4932pub enum WaitUntil {
4933 /// Consider operation to be finished when the `load` event is fired
4934 Load,
4935 /// Consider operation to be finished when the `DOMContentLoaded` event is fired
4936 DomContentLoaded,
4937 /// Consider operation to be finished when there are no network connections for at least 500ms
4938 NetworkIdle,
4939 /// Consider operation to be finished when the commit event is fired
4940 Commit,
4941}
4942
4943impl WaitUntil {
4944 pub(crate) fn as_str(&self) -> &'static str {
4945 match self {
4946 WaitUntil::Load => "load",
4947 WaitUntil::DomContentLoaded => "domcontentloaded",
4948 WaitUntil::NetworkIdle => "networkidle",
4949 WaitUntil::Commit => "commit",
4950 }
4951 }
4952}
4953
4954/// Options for adding a style tag to the page
4955///
4956/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
4957#[derive(Debug, Clone, Default)]
4958#[non_exhaustive]
4959pub struct AddStyleTagOptions {
4960 /// Raw CSS content to inject
4961 pub content: Option<String>,
4962 /// URL of the `<link>` tag to add
4963 pub url: Option<String>,
4964 /// Path to a CSS file to inject
4965 pub path: Option<String>,
4966}
4967
4968impl AddStyleTagOptions {
4969 /// Creates a new builder for AddStyleTagOptions
4970 pub fn builder() -> AddStyleTagOptionsBuilder {
4971 AddStyleTagOptionsBuilder::default()
4972 }
4973
4974 /// Validates that at least one option is specified
4975 pub(crate) fn validate(&self) -> Result<()> {
4976 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
4977 return Err(Error::InvalidArgument(
4978 "At least one of content, url, or path must be specified".to_string(),
4979 ));
4980 }
4981 Ok(())
4982 }
4983}
4984
4985/// Builder for AddStyleTagOptions
4986#[derive(Debug, Clone, Default)]
4987pub struct AddStyleTagOptionsBuilder {
4988 content: Option<String>,
4989 url: Option<String>,
4990 path: Option<String>,
4991}
4992
4993impl AddStyleTagOptionsBuilder {
4994 /// Sets the CSS content to inject
4995 pub fn content(mut self, content: impl Into<String>) -> Self {
4996 self.content = Some(content.into());
4997 self
4998 }
4999
5000 /// Sets the URL of the stylesheet
5001 pub fn url(mut self, url: impl Into<String>) -> Self {
5002 self.url = Some(url.into());
5003 self
5004 }
5005
5006 /// Sets the path to a CSS file
5007 pub fn path(mut self, path: impl Into<String>) -> Self {
5008 self.path = Some(path.into());
5009 self
5010 }
5011
5012 /// Builds the AddStyleTagOptions
5013 pub fn build(self) -> AddStyleTagOptions {
5014 AddStyleTagOptions {
5015 content: self.content,
5016 url: self.url,
5017 path: self.path,
5018 }
5019 }
5020}
5021
5022// ============================================================================
5023// AddScriptTagOptions
5024// ============================================================================
5025
5026/// Options for adding a `<script>` tag to the page.
5027///
5028/// At least one of `content`, `url`, or `path` must be specified.
5029///
5030/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
5031#[derive(Debug, Clone, Default)]
5032#[non_exhaustive]
5033pub struct AddScriptTagOptions {
5034 /// Raw JavaScript content to inject
5035 pub content: Option<String>,
5036 /// URL of the `<script>` tag to add
5037 pub url: Option<String>,
5038 /// Path to a JavaScript file to inject (file contents will be read and sent as content)
5039 pub path: Option<String>,
5040 /// Script type attribute (e.g., `"module"`)
5041 pub type_: Option<String>,
5042}
5043
5044impl AddScriptTagOptions {
5045 /// Creates a new builder for AddScriptTagOptions
5046 pub fn builder() -> AddScriptTagOptionsBuilder {
5047 AddScriptTagOptionsBuilder::default()
5048 }
5049
5050 /// Validates that at least one option is specified
5051 pub(crate) fn validate(&self) -> Result<()> {
5052 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
5053 return Err(Error::InvalidArgument(
5054 "At least one of content, url, or path must be specified".to_string(),
5055 ));
5056 }
5057 Ok(())
5058 }
5059}
5060
5061/// Builder for AddScriptTagOptions
5062#[derive(Debug, Clone, Default)]
5063pub struct AddScriptTagOptionsBuilder {
5064 content: Option<String>,
5065 url: Option<String>,
5066 path: Option<String>,
5067 type_: Option<String>,
5068}
5069
5070impl AddScriptTagOptionsBuilder {
5071 /// Sets the JavaScript content to inject
5072 pub fn content(mut self, content: impl Into<String>) -> Self {
5073 self.content = Some(content.into());
5074 self
5075 }
5076
5077 /// Sets the URL of the script to load
5078 pub fn url(mut self, url: impl Into<String>) -> Self {
5079 self.url = Some(url.into());
5080 self
5081 }
5082
5083 /// Sets the path to a JavaScript file to inject
5084 pub fn path(mut self, path: impl Into<String>) -> Self {
5085 self.path = Some(path.into());
5086 self
5087 }
5088
5089 /// Sets the script type attribute (e.g., `"module"`)
5090 pub fn type_(mut self, type_: impl Into<String>) -> Self {
5091 self.type_ = Some(type_.into());
5092 self
5093 }
5094
5095 /// Builds the AddScriptTagOptions
5096 pub fn build(self) -> AddScriptTagOptions {
5097 AddScriptTagOptions {
5098 content: self.content,
5099 url: self.url,
5100 path: self.path,
5101 type_: self.type_,
5102 }
5103 }
5104}
5105
5106// ============================================================================
5107// EmulateMediaOptions and related enums
5108// ============================================================================
5109
5110/// Media type for `page.emulate_media()`.
5111///
5112/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
5113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
5114#[serde(rename_all = "lowercase")]
5115#[non_exhaustive]
5116pub enum Media {
5117 /// Emulate screen media type
5118 Screen,
5119 /// Emulate print media type
5120 Print,
5121 /// Reset media emulation to browser default (sends `"no-override"` to protocol)
5122 #[serde(rename = "no-override")]
5123 NoOverride,
5124}
5125
5126/// Preferred color scheme for `page.emulate_media()`.
5127///
5128/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
5129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
5130#[non_exhaustive]
5131pub enum ColorScheme {
5132 /// Emulate light color scheme
5133 #[serde(rename = "light")]
5134 Light,
5135 /// Emulate dark color scheme
5136 #[serde(rename = "dark")]
5137 Dark,
5138 /// Emulate no preference for color scheme
5139 #[serde(rename = "no-preference")]
5140 NoPreference,
5141 /// Reset color scheme to browser default
5142 #[serde(rename = "no-override")]
5143 NoOverride,
5144}
5145
5146/// Reduced motion preference for `page.emulate_media()`.
5147///
5148/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
5149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
5150#[non_exhaustive]
5151pub enum ReducedMotion {
5152 /// Emulate reduced motion preference
5153 #[serde(rename = "reduce")]
5154 Reduce,
5155 /// Emulate no preference for reduced motion
5156 #[serde(rename = "no-preference")]
5157 NoPreference,
5158 /// Reset reduced motion to browser default
5159 #[serde(rename = "no-override")]
5160 NoOverride,
5161}
5162
5163/// Forced colors preference for `page.emulate_media()`.
5164///
5165/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
5166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
5167#[non_exhaustive]
5168pub enum ForcedColors {
5169 /// Emulate active forced colors
5170 #[serde(rename = "active")]
5171 Active,
5172 /// Emulate no forced colors
5173 #[serde(rename = "none")]
5174 None_,
5175 /// Reset forced colors to browser default
5176 #[serde(rename = "no-override")]
5177 NoOverride,
5178}
5179
5180/// Options for `page.emulate_media()`.
5181///
5182/// All fields are optional. Fields that are `None` are omitted from the protocol
5183/// message (meaning they are not changed). To reset a field to browser default,
5184/// use the `NoOverride` variant.
5185///
5186/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
5187#[derive(Debug, Clone, Default)]
5188#[non_exhaustive]
5189pub struct EmulateMediaOptions {
5190 /// Media type to emulate (screen, print, or no-override)
5191 pub media: Option<Media>,
5192 /// Color scheme preference to emulate
5193 pub color_scheme: Option<ColorScheme>,
5194 /// Reduced motion preference to emulate
5195 pub reduced_motion: Option<ReducedMotion>,
5196 /// Forced colors preference to emulate
5197 pub forced_colors: Option<ForcedColors>,
5198}
5199
5200impl EmulateMediaOptions {
5201 /// Creates a new builder for EmulateMediaOptions
5202 pub fn builder() -> EmulateMediaOptionsBuilder {
5203 EmulateMediaOptionsBuilder::default()
5204 }
5205}
5206
5207/// Builder for EmulateMediaOptions
5208#[derive(Debug, Clone, Default)]
5209pub struct EmulateMediaOptionsBuilder {
5210 media: Option<Media>,
5211 color_scheme: Option<ColorScheme>,
5212 reduced_motion: Option<ReducedMotion>,
5213 forced_colors: Option<ForcedColors>,
5214}
5215
5216impl EmulateMediaOptionsBuilder {
5217 /// Sets the media type to emulate
5218 pub fn media(mut self, media: Media) -> Self {
5219 self.media = Some(media);
5220 self
5221 }
5222
5223 /// Sets the color scheme preference
5224 pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
5225 self.color_scheme = Some(color_scheme);
5226 self
5227 }
5228
5229 /// Sets the reduced motion preference
5230 pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
5231 self.reduced_motion = Some(reduced_motion);
5232 self
5233 }
5234
5235 /// Sets the forced colors preference
5236 pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
5237 self.forced_colors = Some(forced_colors);
5238 self
5239 }
5240
5241 /// Builds the EmulateMediaOptions
5242 pub fn build(self) -> EmulateMediaOptions {
5243 EmulateMediaOptions {
5244 media: self.media,
5245 color_scheme: self.color_scheme,
5246 reduced_motion: self.reduced_motion,
5247 forced_colors: self.forced_colors,
5248 }
5249 }
5250}
5251
5252// ============================================================================
5253// PdfOptions
5254// ============================================================================
5255
5256/// Margin options for PDF generation.
5257///
5258/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
5259#[derive(Debug, Clone, Default, Serialize)]
5260pub struct PdfMargin {
5261 /// Top margin (e.g. `"1in"`)
5262 #[serde(skip_serializing_if = "Option::is_none")]
5263 pub top: Option<String>,
5264 /// Right margin
5265 #[serde(skip_serializing_if = "Option::is_none")]
5266 pub right: Option<String>,
5267 /// Bottom margin
5268 #[serde(skip_serializing_if = "Option::is_none")]
5269 pub bottom: Option<String>,
5270 /// Left margin
5271 #[serde(skip_serializing_if = "Option::is_none")]
5272 pub left: Option<String>,
5273}
5274
5275/// Options for generating a PDF from a page.
5276///
5277/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
5278/// Firefox or WebKit will result in an error.
5279///
5280/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
5281#[derive(Debug, Clone, Default)]
5282#[non_exhaustive]
5283pub struct PdfOptions {
5284 /// If specified, the PDF will also be saved to this file path.
5285 pub path: Option<std::path::PathBuf>,
5286 /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
5287 pub scale: Option<f64>,
5288 /// Whether to display header and footer (default false).
5289 pub display_header_footer: Option<bool>,
5290 /// HTML template for the print header. Should be valid HTML.
5291 pub header_template: Option<String>,
5292 /// HTML template for the print footer.
5293 pub footer_template: Option<String>,
5294 /// Whether to print background graphics (default false).
5295 pub print_background: Option<bool>,
5296 /// Paper orientation — `true` for landscape (default false).
5297 pub landscape: Option<bool>,
5298 /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
5299 pub page_ranges: Option<String>,
5300 /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
5301 pub format: Option<String>,
5302 /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
5303 pub width: Option<String>,
5304 /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
5305 pub height: Option<String>,
5306 /// Whether or not to prefer page size as defined by CSS.
5307 pub prefer_css_page_size: Option<bool>,
5308 /// Paper margins, defaulting to none.
5309 pub margin: Option<PdfMargin>,
5310}
5311
5312impl PdfOptions {
5313 /// Creates a new builder for PdfOptions
5314 pub fn builder() -> PdfOptionsBuilder {
5315 PdfOptionsBuilder::default()
5316 }
5317}
5318
5319/// Builder for PdfOptions
5320#[derive(Debug, Clone, Default)]
5321pub struct PdfOptionsBuilder {
5322 path: Option<std::path::PathBuf>,
5323 scale: Option<f64>,
5324 display_header_footer: Option<bool>,
5325 header_template: Option<String>,
5326 footer_template: Option<String>,
5327 print_background: Option<bool>,
5328 landscape: Option<bool>,
5329 page_ranges: Option<String>,
5330 format: Option<String>,
5331 width: Option<String>,
5332 height: Option<String>,
5333 prefer_css_page_size: Option<bool>,
5334 margin: Option<PdfMargin>,
5335}
5336
5337impl PdfOptionsBuilder {
5338 /// Sets the file path for saving the PDF
5339 pub fn path(mut self, path: std::path::PathBuf) -> Self {
5340 self.path = Some(path);
5341 self
5342 }
5343
5344 /// Sets the scale of the webpage rendering
5345 pub fn scale(mut self, scale: f64) -> Self {
5346 self.scale = Some(scale);
5347 self
5348 }
5349
5350 /// Sets whether to display header and footer
5351 pub fn display_header_footer(mut self, display: bool) -> Self {
5352 self.display_header_footer = Some(display);
5353 self
5354 }
5355
5356 /// Sets the HTML template for the print header
5357 pub fn header_template(mut self, template: impl Into<String>) -> Self {
5358 self.header_template = Some(template.into());
5359 self
5360 }
5361
5362 /// Sets the HTML template for the print footer
5363 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
5364 self.footer_template = Some(template.into());
5365 self
5366 }
5367
5368 /// Sets whether to print background graphics
5369 pub fn print_background(mut self, print: bool) -> Self {
5370 self.print_background = Some(print);
5371 self
5372 }
5373
5374 /// Sets whether to use landscape orientation
5375 pub fn landscape(mut self, landscape: bool) -> Self {
5376 self.landscape = Some(landscape);
5377 self
5378 }
5379
5380 /// Sets the page ranges to print
5381 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
5382 self.page_ranges = Some(ranges.into());
5383 self
5384 }
5385
5386 /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
5387 pub fn format(mut self, format: impl Into<String>) -> Self {
5388 self.format = Some(format.into());
5389 self
5390 }
5391
5392 /// Sets the paper width
5393 pub fn width(mut self, width: impl Into<String>) -> Self {
5394 self.width = Some(width.into());
5395 self
5396 }
5397
5398 /// Sets the paper height
5399 pub fn height(mut self, height: impl Into<String>) -> Self {
5400 self.height = Some(height.into());
5401 self
5402 }
5403
5404 /// Sets whether to prefer page size as defined by CSS
5405 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
5406 self.prefer_css_page_size = Some(prefer);
5407 self
5408 }
5409
5410 /// Sets the paper margins
5411 pub fn margin(mut self, margin: PdfMargin) -> Self {
5412 self.margin = Some(margin);
5413 self
5414 }
5415
5416 /// Builds the PdfOptions
5417 pub fn build(self) -> PdfOptions {
5418 PdfOptions {
5419 path: self.path,
5420 scale: self.scale,
5421 display_header_footer: self.display_header_footer,
5422 header_template: self.header_template,
5423 footer_template: self.footer_template,
5424 print_background: self.print_background,
5425 landscape: self.landscape,
5426 page_ranges: self.page_ranges,
5427 format: self.format,
5428 width: self.width,
5429 height: self.height,
5430 prefer_css_page_size: self.prefer_css_page_size,
5431 margin: self.margin,
5432 }
5433 }
5434}
5435
5436/// Response from navigation operations.
5437///
5438/// Returned from `page.goto()`, `page.reload()`, `page.go_back()`, and similar
5439/// navigation methods. Provides access to the HTTP response status, headers, and body.
5440///
5441/// See: <https://playwright.dev/docs/api/class-response>
5442#[derive(Clone)]
5443pub struct Response {
5444 url: String,
5445 status: u16,
5446 status_text: String,
5447 ok: bool,
5448 headers: std::collections::HashMap<String, String>,
5449 /// Reference to the backing channel owner for RPC calls (body, rawHeaders, etc.)
5450 /// Stored as the generic trait object so it can be downcast to ResponseObject when needed.
5451 response_channel_owner: Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
5452}
5453
5454impl Response {
5455 /// Creates a new Response from protocol data.
5456 ///
5457 /// This is used internally when constructing a Response from the protocol
5458 /// initializer (e.g., after `goto` or `reload`).
5459 pub(crate) fn new(
5460 url: String,
5461 status: u16,
5462 status_text: String,
5463 headers: std::collections::HashMap<String, String>,
5464 response_channel_owner: Option<
5465 std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>,
5466 >,
5467 ) -> Self {
5468 Self {
5469 url,
5470 status,
5471 status_text,
5472 ok: (200..300).contains(&status),
5473 headers,
5474 response_channel_owner,
5475 }
5476 }
5477}
5478
5479impl Response {
5480 /// Returns the URL of the response.
5481 ///
5482 /// See: <https://playwright.dev/docs/api/class-response#response-url>
5483 pub fn url(&self) -> &str {
5484 &self.url
5485 }
5486
5487 /// Returns the HTTP status code.
5488 ///
5489 /// See: <https://playwright.dev/docs/api/class-response#response-status>
5490 pub fn status(&self) -> u16 {
5491 self.status
5492 }
5493
5494 /// Returns the HTTP status text.
5495 ///
5496 /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
5497 pub fn status_text(&self) -> &str {
5498 &self.status_text
5499 }
5500
5501 /// Returns whether the response was successful (status 200-299).
5502 ///
5503 /// See: <https://playwright.dev/docs/api/class-response#response-ok>
5504 pub fn ok(&self) -> bool {
5505 self.ok
5506 }
5507
5508 /// Returns the response headers as a HashMap.
5509 ///
5510 /// Note: these are the headers from the protocol initializer. For the full
5511 /// raw headers (including duplicates), use `headers_array()` or `all_headers()`.
5512 ///
5513 /// See: <https://playwright.dev/docs/api/class-response#response-headers>
5514 pub fn headers(&self) -> &std::collections::HashMap<String, String> {
5515 &self.headers
5516 }
5517
5518 /// Returns the [`Request`] that triggered this response.
5519 ///
5520 /// Navigates the protocol object hierarchy: ResponseObject → parent (Request).
5521 ///
5522 /// See: <https://playwright.dev/docs/api/class-response#response-request>
5523 pub fn request(&self) -> Option<crate::protocol::Request> {
5524 let owner = self.response_channel_owner.as_ref()?;
5525 downcast_parent::<crate::protocol::Request>(&**owner)
5526 }
5527
5528 /// Returns the [`Frame`](crate::protocol::Frame) that initiated the request for this response.
5529 ///
5530 /// Navigates the protocol object hierarchy: ResponseObject → Request → Frame.
5531 ///
5532 /// See: <https://playwright.dev/docs/api/class-response#response-frame>
5533 pub fn frame(&self) -> Option<crate::protocol::Frame> {
5534 let request = self.request()?;
5535 request.frame()
5536 }
5537
5538 /// Returns the backing `ResponseObject`, or an error if unavailable.
5539 pub(crate) fn response_object(&self) -> crate::error::Result<crate::protocol::ResponseObject> {
5540 let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
5541 crate::error::Error::ProtocolError(
5542 "Response has no backing protocol object".to_string(),
5543 )
5544 })?;
5545 arc.as_any()
5546 .downcast_ref::<crate::protocol::ResponseObject>()
5547 .cloned()
5548 .ok_or_else(|| crate::error::Error::TypeMismatch {
5549 guid: arc.guid().to_string(),
5550 expected: "ResponseObject".to_string(),
5551 actual: arc.type_name().to_string(),
5552 })
5553 }
5554
5555 /// Returns TLS/SSL security details for HTTPS connections, or `None` for HTTP.
5556 ///
5557 /// See: <https://playwright.dev/docs/api/class-response#response-security-details>
5558 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url()))]
5559 pub async fn security_details(
5560 &self,
5561 ) -> crate::error::Result<Option<crate::protocol::response::SecurityDetails>> {
5562 self.response_object()?.security_details().await
5563 }
5564
5565 /// Returns the server's IP address and port, or `None`.
5566 ///
5567 /// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
5568 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url()))]
5569 pub async fn server_addr(
5570 &self,
5571 ) -> crate::error::Result<Option<crate::protocol::response::RemoteAddr>> {
5572 self.response_object()?.server_addr().await
5573 }
5574
5575 /// Waits for this response to finish loading.
5576 ///
5577 /// For responses obtained from navigation methods (`goto`, `reload`), the response
5578 /// is already finished when returned. For responses from `on_response` handlers,
5579 /// the body may still be loading.
5580 ///
5581 /// See: <https://playwright.dev/docs/api/class-response#response-finished>
5582 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url()))]
5583 pub async fn finished(&self) -> crate::error::Result<()> {
5584 // The Playwright protocol dispatches `requestFinished` as a separate event
5585 // rather than exposing a `finished` RPC method on Response.
5586 // For responses from goto/reload, the response is already complete.
5587 // TODO: For on_response handlers, implement proper waiting via requestFinished event.
5588 Ok(())
5589 }
5590
5591 /// Returns the HTTP version used by this response (e.g. `"HTTP/1.1"` or `"HTTP/2.0"`).
5592 ///
5593 /// Makes an RPC call to the Playwright server.
5594 ///
5595 /// # Errors
5596 ///
5597 /// Returns an error if:
5598 /// - No backing protocol object is available (edge case)
5599 /// - The RPC call to the server fails
5600 ///
5601 /// See: <https://playwright.dev/docs/api/class-response#response-http-version>
5602 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url(), version = tracing::field::Empty))]
5603 pub async fn http_version(&self) -> crate::error::Result<String> {
5604 let v = self.response_object()?.http_version().await?;
5605 tracing::Span::current().record("version", &v);
5606 Ok(v)
5607 }
5608
5609 /// Returns the response body as raw bytes.
5610 ///
5611 /// Makes an RPC call to the Playwright server to fetch the response body.
5612 ///
5613 /// # Errors
5614 ///
5615 /// Returns an error if:
5616 /// - No backing protocol object is available (edge case)
5617 /// - The RPC call to the server fails
5618 /// - The base64 response cannot be decoded
5619 ///
5620 /// See: <https://playwright.dev/docs/api/class-response#response-body>
5621 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url(), bytes_len = tracing::field::Empty))]
5622 pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
5623 let bytes = self.response_object()?.body().await?;
5624 tracing::Span::current().record("bytes_len", bytes.len());
5625 Ok(bytes)
5626 }
5627
5628 /// Returns the response body as a UTF-8 string.
5629 ///
5630 /// Calls `body()` then converts bytes to a UTF-8 string.
5631 ///
5632 /// # Errors
5633 ///
5634 /// Returns an error if:
5635 /// - `body()` fails
5636 /// - The body is not valid UTF-8
5637 ///
5638 /// See: <https://playwright.dev/docs/api/class-response#response-text>
5639 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url()))]
5640 pub async fn text(&self) -> crate::error::Result<String> {
5641 let bytes = self.body().await?;
5642 String::from_utf8(bytes).map_err(|e| {
5643 crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
5644 })
5645 }
5646
5647 /// Parses the response body as JSON and deserializes it into type `T`.
5648 ///
5649 /// Calls `text()` then uses `serde_json` to deserialize the body.
5650 ///
5651 /// # Errors
5652 ///
5653 /// Returns an error if:
5654 /// - `text()` fails
5655 /// - The body is not valid JSON or doesn't match the expected type
5656 ///
5657 /// See: <https://playwright.dev/docs/api/class-response#response-json>
5658 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url()))]
5659 pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
5660 let text = self.text().await?;
5661 serde_json::from_str(&text).map_err(|e| {
5662 crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
5663 })
5664 }
5665
5666 /// Returns all response headers as name-value pairs, preserving duplicates.
5667 ///
5668 /// Makes an RPC call for `"rawHeaders"` which returns the complete header list.
5669 ///
5670 /// # Errors
5671 ///
5672 /// Returns an error if:
5673 /// - No backing protocol object is available (edge case)
5674 /// - The RPC call to the server fails
5675 ///
5676 /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
5677 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url()))]
5678 pub async fn headers_array(
5679 &self,
5680 ) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
5681 self.response_object()?.raw_headers().await
5682 }
5683
5684 /// Returns all response headers merged into a HashMap with lowercase keys.
5685 ///
5686 /// When multiple headers have the same name, their values are joined with `, `.
5687 /// This matches the behavior of `response.allHeaders()` in other Playwright bindings.
5688 ///
5689 /// # Errors
5690 ///
5691 /// Returns an error if:
5692 /// - No backing protocol object is available (edge case)
5693 /// - The RPC call to the server fails
5694 ///
5695 /// See: <https://playwright.dev/docs/api/class-response#response-all-headers>
5696 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url()))]
5697 pub async fn all_headers(
5698 &self,
5699 ) -> crate::error::Result<std::collections::HashMap<String, String>> {
5700 let entries = self.headers_array().await?;
5701 let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
5702 for entry in entries {
5703 let key = entry.name.to_lowercase();
5704 map.entry(key)
5705 .and_modify(|v| {
5706 v.push_str(", ");
5707 v.push_str(&entry.value);
5708 })
5709 .or_insert(entry.value);
5710 }
5711 Ok(map)
5712 }
5713
5714 /// Returns the value for a single response header, or `None` if not present.
5715 ///
5716 /// The lookup is case-insensitive.
5717 ///
5718 /// # Errors
5719 ///
5720 /// Returns an error if:
5721 /// - No backing protocol object is available (edge case)
5722 /// - The RPC call to the server fails
5723 ///
5724 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
5725 /// Returns the value for a single response header, or `None` if not present.
5726 ///
5727 /// The lookup is case-insensitive. When multiple headers share the same name,
5728 /// their values are joined with `, ` (matching Playwright's behavior).
5729 ///
5730 /// Uses the raw headers from the server for accurate results.
5731 ///
5732 /// # Errors
5733 ///
5734 /// Returns an error if the underlying `headers_array()` RPC call fails.
5735 ///
5736 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
5737 #[tracing::instrument(level = "debug", skip_all, fields(url = %self.url(), name = %name))]
5738 pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
5739 let entries = self.headers_array().await?;
5740 let name_lower = name.to_lowercase();
5741 let mut values: Vec<String> = entries
5742 .into_iter()
5743 .filter(|h| h.name.to_lowercase() == name_lower)
5744 .map(|h| h.value)
5745 .collect();
5746
5747 if values.is_empty() {
5748 Ok(None)
5749 } else if values.len() == 1 {
5750 Ok(Some(values.remove(0)))
5751 } else {
5752 Ok(Some(values.join(", ")))
5753 }
5754 }
5755}
5756
5757impl std::fmt::Debug for Response {
5758 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5759 f.debug_struct("Response")
5760 .field("url", &self.url)
5761 .field("status", &self.status)
5762 .field("status_text", &self.status_text)
5763 .field("ok", &self.ok)
5764 .finish_non_exhaustive()
5765 }
5766}
5767
5768/// Options for `page.route_from_har()` and `context.route_from_har()`.
5769///
5770/// See: <https://playwright.dev/docs/api/class-page#page-route-from-har>
5771#[derive(Debug, Clone, Default)]
5772#[non_exhaustive]
5773pub struct RouteFromHarOptions {
5774 /// URL glob pattern — only requests matching this pattern are served from
5775 /// the HAR file. All requests are intercepted when omitted.
5776 pub url: Option<String>,
5777
5778 /// Policy for requests not found in the HAR file.
5779 ///
5780 /// - `"abort"` (default) — terminate the request with a network error.
5781 /// - `"fallback"` — pass the request through to the next handler (or network).
5782 pub not_found: Option<String>,
5783
5784 /// When `true`, record new network activity into the HAR file instead of
5785 /// replaying existing entries. Defaults to `false`.
5786 pub update: Option<bool>,
5787
5788 /// Content storage strategy used when `update` is `true`.
5789 ///
5790 /// - `"embed"` (default) — inline base64-encoded content in the HAR.
5791 /// - `"attach"` — store content as separate files alongside the HAR.
5792 pub update_content: Option<String>,
5793
5794 /// Recording detail level used when `update` is `true`.
5795 ///
5796 /// - `"minimal"` (default) — omit timing, cookies, and security info.
5797 /// - `"full"` — record everything.
5798 pub update_mode: Option<String>,
5799}
5800
5801impl RouteFromHarOptions {
5802 /// Only serve requests matching this URL glob from the HAR.
5803 pub fn url(mut self, url: impl Into<String>) -> Self {
5804 self.url = Some(url.into());
5805 self
5806 }
5807 /// Behavior for requests not found in the HAR ("abort" or "fallback").
5808 pub fn not_found(mut self, not_found: impl Into<String>) -> Self {
5809 self.not_found = Some(not_found.into());
5810 self
5811 }
5812 /// Record new entries into the HAR instead of serving from it.
5813 pub fn update(mut self, update: bool) -> Self {
5814 self.update = Some(update);
5815 self
5816 }
5817}
5818
5819/// Options for `page.add_locator_handler()`.
5820///
5821/// See: <https://playwright.dev/docs/api/class-page#page-add-locator-handler>
5822#[derive(Debug, Clone, Default)]
5823#[non_exhaustive]
5824pub struct AddLocatorHandlerOptions {
5825 /// Whether to keep the page frozen after the handler has been called.
5826 ///
5827 /// When `false` (default), Playwright resumes normal page operation after
5828 /// the handler completes. When `true`, the page stays paused.
5829 pub no_wait_after: Option<bool>,
5830
5831 /// Maximum number of times to invoke this handler.
5832 ///
5833 /// Once exhausted, the handler is automatically unregistered.
5834 /// `None` (default) means the handler runs indefinitely.
5835 pub times: Option<u32>,
5836}
5837
5838/// Shared helper: store timeout locally and notify the Playwright server.
5839/// Used by both Page and BrowserContext timeout setters.
5840pub(crate) async fn set_timeout_and_notify(
5841 channel: &crate::server::channel::Channel,
5842 method: &str,
5843 timeout: f64,
5844) {
5845 if let Err(e) = channel
5846 .send_no_result(method, serde_json::json!({ "timeout": timeout }))
5847 .await
5848 {
5849 tracing::warn!("{} send error: {}", method, e);
5850 }
5851}