Skip to main content

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}