Skip to main content

playwright_rs/protocol/
page.rs

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