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