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