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    /// Current viewport size (None when no_viewport is set).
204    /// Updated by set_viewport_size().
205    viewport: Arc<RwLock<Option<Viewport>>>,
206    /// Whether this page has been closed.
207    /// Set to true when close() is called or a "close" event is received.
208    is_closed: Arc<AtomicBool>,
209    /// Default timeout for actions (milliseconds), stored as f64 bits.
210    default_timeout_ms: Arc<AtomicU64>,
211    /// Default timeout for navigation operations (milliseconds), stored as f64 bits.
212    default_navigation_timeout_ms: Arc<AtomicU64>,
213    /// Page-level binding callbacks registered via expose_function / expose_binding
214    binding_callbacks: Arc<Mutex<HashMap<String, PageBindingCallback>>>,
215    /// Console event handlers
216    console_handlers: Arc<Mutex<Vec<ConsoleHandler>>>,
217    /// FileChooser event handlers
218    filechooser_handlers: Arc<Mutex<Vec<FileChooserHandler>>>,
219    /// One-shot senders waiting for the next "fileChooser" event (expect_file_chooser)
220    filechooser_waiters:
221        Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::FileChooser>>>>,
222    /// One-shot senders waiting for the next "popup" event (expect_popup)
223    popup_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Page>>>>,
224    /// One-shot senders waiting for the next "download" event (expect_download)
225    download_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Download>>>>,
226    /// One-shot senders waiting for the next "response" event (expect_response)
227    response_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<ResponseObject>>>>,
228    /// One-shot senders waiting for the next "request" event (expect_request)
229    request_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Request>>>>,
230    /// One-shot senders waiting for the next "console" event (expect_console_message)
231    console_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::ConsoleMessage>>>>,
232    /// close event handlers (fires when page is closed)
233    close_handlers: Arc<Mutex<Vec<CloseHandler>>>,
234    /// load event handlers (fires when page fully loads)
235    load_handlers: Arc<Mutex<Vec<LoadHandler>>>,
236    /// crash event handlers (fires when page crashes)
237    crash_handlers: Arc<Mutex<Vec<CrashHandler>>>,
238    /// pageError event handlers (fires on uncaught JS exceptions)
239    pageerror_handlers: Arc<Mutex<Vec<PageErrorHandler>>>,
240    /// popup event handlers (fires when a popup window opens)
241    popup_handlers: Arc<Mutex<Vec<PopupHandler>>>,
242    /// frameAttached event handlers
243    frameattached_handlers: Arc<Mutex<Vec<FrameAttachedHandler>>>,
244    /// frameDetached event handlers
245    framedetached_handlers: Arc<Mutex<Vec<FrameDetachedHandler>>>,
246    /// frameNavigated event handlers
247    framenavigated_handlers: Arc<Mutex<Vec<FrameNavigatedHandler>>>,
248    /// worker event handlers (fires when a web worker is created in the page)
249    worker_handlers: Arc<Mutex<Vec<WorkerHandler>>>,
250}
251
252/// Type alias for boxed route handler future
253type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
254
255/// Type alias for boxed download handler future
256type DownloadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
257
258/// Type alias for boxed dialog handler future
259type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
260
261/// Type alias for boxed request handler future
262type RequestHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
263
264/// Type alias for boxed response handler future
265type ResponseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
266
267/// Type alias for boxed websocket handler future
268type WebSocketHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
269
270/// Storage for a single route handler
271#[derive(Clone)]
272struct RouteHandlerEntry {
273    pattern: String,
274    handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
275}
276
277/// Download event handler
278type DownloadHandler = Arc<dyn Fn(Download) -> DownloadHandlerFuture + Send + Sync>;
279
280/// Dialog event handler
281type DialogHandler = Arc<dyn Fn(Dialog) -> DialogHandlerFuture + Send + Sync>;
282
283/// Request event handler
284type RequestHandler = Arc<dyn Fn(Request) -> RequestHandlerFuture + Send + Sync>;
285
286/// Response event handler
287type ResponseHandler = Arc<dyn Fn(ResponseObject) -> ResponseHandlerFuture + Send + Sync>;
288
289/// WebSocket event handler
290type WebSocketHandler = Arc<dyn Fn(WebSocket) -> WebSocketHandlerFuture + Send + Sync>;
291
292/// Type alias for boxed console handler future
293type ConsoleHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
294
295/// Console event handler
296type ConsoleHandler =
297    Arc<dyn Fn(crate::protocol::ConsoleMessage) -> ConsoleHandlerFuture + Send + Sync>;
298
299/// Type alias for boxed filechooser handler future
300type FileChooserHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
301
302/// FileChooser event handler
303type FileChooserHandler =
304    Arc<dyn Fn(crate::protocol::FileChooser) -> FileChooserHandlerFuture + Send + Sync>;
305
306/// Type alias for boxed close handler future
307type CloseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
308
309/// close event handler (no arguments)
310type CloseHandler = Arc<dyn Fn() -> CloseHandlerFuture + Send + Sync>;
311
312/// Type alias for boxed load handler future
313type LoadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
314
315/// load event handler (no arguments)
316type LoadHandler = Arc<dyn Fn() -> LoadHandlerFuture + Send + Sync>;
317
318/// Type alias for boxed crash handler future
319type CrashHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
320
321/// crash event handler (no arguments)
322type CrashHandler = Arc<dyn Fn() -> CrashHandlerFuture + Send + Sync>;
323
324/// Type alias for boxed pageError handler future
325type PageErrorHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
326
327/// pageError event handler — receives the error message as a String
328type PageErrorHandler = Arc<dyn Fn(String) -> PageErrorHandlerFuture + Send + Sync>;
329
330/// Type alias for boxed popup handler future
331type PopupHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
332
333/// popup event handler — receives the new popup Page
334type PopupHandler = Arc<dyn Fn(Page) -> PopupHandlerFuture + Send + Sync>;
335
336/// Type alias for boxed frameAttached/Detached/Navigated handler future
337type FrameEventHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
338
339/// frameAttached event handler
340type FrameAttachedHandler =
341    Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
342
343/// frameDetached event handler
344type FrameDetachedHandler =
345    Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
346
347/// frameNavigated event handler
348type FrameNavigatedHandler =
349    Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
350
351/// Type alias for boxed worker handler future
352type WorkerHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
353
354/// worker event handler — receives the new Worker
355type WorkerHandler = Arc<dyn Fn(crate::protocol::Worker) -> WorkerHandlerFuture + Send + Sync>;
356
357/// Type alias for boxed page-level binding callback future
358type PageBindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
359
360/// Page-level binding callback: receives deserialized JS args, returns a JSON value
361type PageBindingCallback =
362    Arc<dyn Fn(Vec<serde_json::Value>) -> PageBindingCallbackFuture + Send + Sync>;
363
364impl Page {
365    /// Creates a new Page from protocol initialization
366    ///
367    /// This is called by the object factory when the server sends a `__create__` message
368    /// for a Page object.
369    ///
370    /// # Arguments
371    ///
372    /// * `parent` - The parent BrowserContext object
373    /// * `type_name` - The protocol type name ("Page")
374    /// * `guid` - The unique identifier for this page
375    /// * `initializer` - The initialization data from the server
376    ///
377    /// # Errors
378    ///
379    /// Returns error if initializer is malformed
380    pub fn new(
381        parent: Arc<dyn ChannelOwner>,
382        type_name: String,
383        guid: Arc<str>,
384        initializer: Value,
385    ) -> Result<Self> {
386        // Extract mainFrame GUID from initializer
387        let main_frame_guid: Arc<str> =
388            Arc::from(initializer["mainFrame"]["guid"].as_str().ok_or_else(|| {
389                crate::error::Error::ProtocolError(
390                    "Page initializer missing 'mainFrame.guid' field".to_string(),
391                )
392            })?);
393
394        let base = ChannelOwnerImpl::new(
395            ParentOrConnection::Parent(parent),
396            type_name,
397            guid,
398            initializer,
399        );
400
401        // Initialize URL to about:blank
402        let url = Arc::new(RwLock::new("about:blank".to_string()));
403
404        // Initialize empty route handlers
405        let route_handlers = Arc::new(Mutex::new(Vec::new()));
406
407        // Initialize empty event handlers
408        let download_handlers = Arc::new(Mutex::new(Vec::new()));
409        let dialog_handlers = Arc::new(Mutex::new(Vec::new()));
410        let websocket_handlers = Arc::new(Mutex::new(Vec::new()));
411
412        // Initialize cached main frame as empty (will be populated on first access)
413        let cached_main_frame = Arc::new(Mutex::new(None));
414
415        // Extract viewport from initializer (may be null for no_viewport contexts)
416        let initial_viewport: Option<Viewport> =
417            base.initializer().get("viewportSize").and_then(|v| {
418                if v.is_null() {
419                    None
420                } else {
421                    serde_json::from_value(v.clone()).ok()
422                }
423            });
424        let viewport = Arc::new(RwLock::new(initial_viewport));
425
426        Ok(Self {
427            base,
428            url,
429            main_frame_guid,
430            cached_main_frame,
431            route_handlers,
432            download_handlers,
433            dialog_handlers,
434            request_handlers: Default::default(),
435            request_finished_handlers: Default::default(),
436            request_failed_handlers: Default::default(),
437            response_handlers: Default::default(),
438            websocket_handlers,
439            viewport,
440            is_closed: Arc::new(AtomicBool::new(false)),
441            default_timeout_ms: Arc::new(AtomicU64::new(crate::DEFAULT_TIMEOUT_MS.to_bits())),
442            default_navigation_timeout_ms: Arc::new(AtomicU64::new(
443                crate::DEFAULT_TIMEOUT_MS.to_bits(),
444            )),
445            binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
446            console_handlers: Arc::new(Mutex::new(Vec::new())),
447            filechooser_handlers: Arc::new(Mutex::new(Vec::new())),
448            filechooser_waiters: Arc::new(Mutex::new(Vec::new())),
449            popup_waiters: Arc::new(Mutex::new(Vec::new())),
450            download_waiters: Arc::new(Mutex::new(Vec::new())),
451            response_waiters: Arc::new(Mutex::new(Vec::new())),
452            request_waiters: Arc::new(Mutex::new(Vec::new())),
453            console_waiters: Arc::new(Mutex::new(Vec::new())),
454            close_handlers: Arc::new(Mutex::new(Vec::new())),
455            load_handlers: Arc::new(Mutex::new(Vec::new())),
456            crash_handlers: Arc::new(Mutex::new(Vec::new())),
457            pageerror_handlers: Arc::new(Mutex::new(Vec::new())),
458            popup_handlers: Arc::new(Mutex::new(Vec::new())),
459            frameattached_handlers: Arc::new(Mutex::new(Vec::new())),
460            framedetached_handlers: Arc::new(Mutex::new(Vec::new())),
461            framenavigated_handlers: Arc::new(Mutex::new(Vec::new())),
462            worker_handlers: Arc::new(Mutex::new(Vec::new())),
463        })
464    }
465
466    /// Returns the channel for sending protocol messages
467    ///
468    /// Used internally for sending RPC calls to the page.
469    fn channel(&self) -> &Channel {
470        self.base.channel()
471    }
472
473    /// Returns the main frame of the page.
474    ///
475    /// The main frame is where navigation and DOM operations actually happen.
476    ///
477    /// This method also wires up the back-reference from the frame to the page so that
478    /// `frame.page()`, `frame.locator()`, and `frame.get_by_*()` work correctly.
479    pub async fn main_frame(&self) -> Result<crate::protocol::Frame> {
480        // Get and downcast the Frame object from the connection's object registry
481        let frame: crate::protocol::Frame = self
482            .connection()
483            .get_typed::<crate::protocol::Frame>(&self.main_frame_guid)
484            .await?;
485
486        // Wire up the back-reference so frame.page() / frame.locator() work.
487        // This is safe to call multiple times (subsequent calls are no-ops once set).
488        frame.set_page(self.clone());
489
490        // Cache the frame for synchronous access in url()
491        if let Ok(mut cached) = self.cached_main_frame.lock() {
492            *cached = Some(frame.clone());
493        }
494
495        Ok(frame)
496    }
497
498    /// Returns the current URL of the page.
499    ///
500    /// This returns the last committed URL, including hash fragments from anchor navigation.
501    /// Initially, pages are at "about:blank".
502    ///
503    /// See: <https://playwright.dev/docs/api/class-page#page-url>
504    pub fn url(&self) -> String {
505        // Try to get URL from the cached main frame (source of truth for navigation including hashes)
506        if let Ok(cached) = self.cached_main_frame.lock()
507            && let Some(frame) = cached.as_ref()
508        {
509            return frame.url();
510        }
511
512        // Fallback to cached URL if frame not yet loaded
513        self.url.read().unwrap().clone()
514    }
515
516    /// Closes the page.
517    ///
518    /// This is a graceful operation that sends a close command to the page
519    /// and waits for it to shut down properly.
520    ///
521    /// # Errors
522    ///
523    /// Returns error if:
524    /// - Page has already been closed
525    /// - Communication with browser process fails
526    ///
527    /// See: <https://playwright.dev/docs/api/class-page#page-close>
528    pub async fn close(&self) -> Result<()> {
529        // Send close RPC to server
530        let result = self
531            .channel()
532            .send_no_result("close", serde_json::json!({}))
533            .await;
534        // Mark as closed regardless of error (best-effort)
535        self.is_closed.store(true, Ordering::Relaxed);
536        result
537    }
538
539    /// Returns whether the page has been closed.
540    ///
541    /// Returns `true` after `close()` has been called on this page, or after the
542    /// page receives a close event from the server (e.g. when the browser context
543    /// is closed).
544    ///
545    /// See: <https://playwright.dev/docs/api/class-page#page-is-closed>
546    pub fn is_closed(&self) -> bool {
547        self.is_closed.load(Ordering::Relaxed)
548    }
549
550    /// Sets the default timeout for all operations on this page.
551    ///
552    /// The timeout applies to actions such as `click`, `fill`, `locator.wait_for`, etc.
553    /// Pass `0` to disable timeouts.
554    ///
555    /// This stores the value locally so that subsequent action calls use it when
556    /// no explicit timeout is provided, and also notifies the Playwright server
557    /// so it can apply the same default on its side.
558    ///
559    /// # Arguments
560    ///
561    /// * `timeout` - Timeout in milliseconds
562    ///
563    /// See: <https://playwright.dev/docs/api/class-page#page-set-default-timeout>
564    pub async fn set_default_timeout(&self, timeout: f64) {
565        self.default_timeout_ms
566            .store(timeout.to_bits(), Ordering::Relaxed);
567        set_timeout_and_notify(self.channel(), "setDefaultTimeoutNoReply", timeout).await;
568    }
569
570    /// Sets the default timeout for navigation operations on this page.
571    ///
572    /// The timeout applies to navigation actions such as `goto`, `reload`,
573    /// `go_back`, and `go_forward`. Pass `0` to disable timeouts.
574    ///
575    /// # Arguments
576    ///
577    /// * `timeout` - Timeout in milliseconds
578    ///
579    /// See: <https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout>
580    pub async fn set_default_navigation_timeout(&self, timeout: f64) {
581        self.default_navigation_timeout_ms
582            .store(timeout.to_bits(), Ordering::Relaxed);
583        set_timeout_and_notify(
584            self.channel(),
585            "setDefaultNavigationTimeoutNoReply",
586            timeout,
587        )
588        .await;
589    }
590
591    /// Returns the current default action timeout in milliseconds.
592    pub fn default_timeout_ms(&self) -> f64 {
593        f64::from_bits(self.default_timeout_ms.load(Ordering::Relaxed))
594    }
595
596    /// Returns the current default navigation timeout in milliseconds.
597    pub fn default_navigation_timeout_ms(&self) -> f64 {
598        f64::from_bits(self.default_navigation_timeout_ms.load(Ordering::Relaxed))
599    }
600
601    /// Returns GotoOptions with the navigation timeout filled in if not already set.
602    ///
603    /// Used internally to ensure the page's configured default navigation timeout
604    /// is used when the caller does not provide an explicit timeout.
605    fn with_navigation_timeout(&self, options: Option<GotoOptions>) -> GotoOptions {
606        let nav_timeout = self.default_navigation_timeout_ms();
607        match options {
608            Some(opts) if opts.timeout.is_some() => opts,
609            Some(mut opts) => {
610                opts.timeout = Some(std::time::Duration::from_millis(nav_timeout as u64));
611                opts
612            }
613            None => GotoOptions {
614                timeout: Some(std::time::Duration::from_millis(nav_timeout as u64)),
615                wait_until: None,
616            },
617        }
618    }
619
620    /// Returns all frames in the page, including the main frame.
621    ///
622    /// Currently returns only the main (top-level) frame. Iframe enumeration
623    /// is not yet implemented and will be added in a future release.
624    ///
625    /// # Errors
626    ///
627    /// Returns error if:
628    /// - Page has been closed
629    /// - Communication with browser process fails
630    ///
631    /// See: <https://playwright.dev/docs/api/class-page#page-frames>
632    pub async fn frames(&self) -> Result<Vec<crate::protocol::Frame>> {
633        // Start with the main frame
634        let main = self.main_frame().await?;
635        Ok(vec![main])
636    }
637
638    /// Navigates to the specified URL.
639    ///
640    /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
641    /// about:blank). This matches Playwright's behavior across all language bindings.
642    ///
643    /// # Arguments
644    ///
645    /// * `url` - The URL to navigate to
646    /// * `options` - Optional navigation options (timeout, wait_until)
647    ///
648    /// # Errors
649    ///
650    /// Returns error if:
651    /// - URL is invalid
652    /// - Navigation timeout (default 30s)
653    /// - Network error
654    ///
655    /// See: <https://playwright.dev/docs/api/class-page#page-goto>
656    pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
657        // Inject the page-level navigation timeout when no explicit timeout is given
658        let options = self.with_navigation_timeout(options);
659
660        // Delegate to main frame
661        let frame = self.main_frame().await.map_err(|e| match e {
662            Error::TargetClosed { context, .. } => Error::TargetClosed {
663                target_type: "Page".to_string(),
664                context,
665            },
666            other => other,
667        })?;
668
669        let response = frame.goto(url, Some(options)).await.map_err(|e| match e {
670            Error::TargetClosed { context, .. } => Error::TargetClosed {
671                target_type: "Page".to_string(),
672                context,
673            },
674            other => other,
675        })?;
676
677        // Update the page's URL if we got a response
678        if let Some(ref resp) = response
679            && let Ok(mut page_url) = self.url.write()
680        {
681            *page_url = resp.url().to_string();
682        }
683
684        Ok(response)
685    }
686
687    /// Returns the browser context that the page belongs to.
688    pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
689        downcast_parent::<crate::protocol::BrowserContext>(self)
690            .ok_or_else(|| Error::ProtocolError("Page parent is not a BrowserContext".to_string()))
691    }
692
693    /// Pauses script execution.
694    ///
695    /// Playwright will stop executing the script and wait for the user to either press
696    /// "Resume" in the page overlay or in the debugger.
697    ///
698    /// See: <https://playwright.dev/docs/api/class-page#page-pause>
699    pub async fn pause(&self) -> Result<()> {
700        self.context()?.pause().await
701    }
702
703    /// Returns the page's title.
704    ///
705    /// See: <https://playwright.dev/docs/api/class-page#page-title>
706    pub async fn title(&self) -> Result<String> {
707        // Delegate to main frame
708        let frame = self.main_frame().await?;
709        frame.title().await
710    }
711
712    /// Returns the full HTML content of the page, including the DOCTYPE.
713    ///
714    /// This method retrieves the complete HTML markup of the page,
715    /// including the doctype declaration and all DOM elements.
716    ///
717    /// See: <https://playwright.dev/docs/api/class-page#page-content>
718    pub async fn content(&self) -> Result<String> {
719        // Delegate to main frame
720        let frame = self.main_frame().await?;
721        frame.content().await
722    }
723
724    /// Sets the content of the page.
725    ///
726    /// See: <https://playwright.dev/docs/api/class-page#page-set-content>
727    pub async fn set_content(&self, html: &str, options: Option<GotoOptions>) -> Result<()> {
728        let frame = self.main_frame().await?;
729        frame.set_content(html, options).await
730    }
731
732    /// Waits for the required load state to be reached.
733    ///
734    /// This resolves when the page reaches a required load state, `load` by default.
735    /// The navigation must have been committed when this method is called. If the current
736    /// document has already reached the required state, resolves immediately.
737    ///
738    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-load-state>
739    pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
740        let frame = self.main_frame().await?;
741        frame.wait_for_load_state(state).await
742    }
743
744    /// Waits for the main frame to navigate to a URL matching the given string or glob pattern.
745    ///
746    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-url>
747    pub async fn wait_for_url(&self, url: &str, options: Option<GotoOptions>) -> Result<()> {
748        let frame = self.main_frame().await?;
749        frame.wait_for_url(url, options).await
750    }
751
752    /// Creates a locator for finding elements on the page.
753    ///
754    /// Locators are the central piece of Playwright's auto-waiting and retry-ability.
755    /// They don't execute queries until an action is performed.
756    ///
757    /// # Arguments
758    ///
759    /// * `selector` - CSS selector or other locating strategy
760    ///
761    /// See: <https://playwright.dev/docs/api/class-page#page-locator>
762    pub async fn locator(&self, selector: &str) -> crate::protocol::Locator {
763        // Get the main frame
764        let frame = self.main_frame().await.expect("Main frame should exist");
765
766        crate::protocol::Locator::new(Arc::new(frame), selector.to_string(), self.clone())
767    }
768
769    /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) for an iframe on this page.
770    ///
771    /// The `selector` identifies the iframe element (e.g., `"iframe[name='content']"`).
772    ///
773    /// See: <https://playwright.dev/docs/api/class-page#page-frame-locator>
774    pub async fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
775        let frame = self.main_frame().await.expect("Main frame should exist");
776        crate::protocol::FrameLocator::new(Arc::new(frame), selector.to_string(), self.clone())
777    }
778
779    /// Returns a locator that matches elements containing the given text.
780    ///
781    /// By default, matching is case-insensitive and searches for a substring.
782    /// Set `exact` to `true` for case-sensitive exact matching.
783    ///
784    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-text>
785    pub async fn get_by_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
786        self.locator(&crate::protocol::locator::get_by_text_selector(text, exact))
787            .await
788    }
789
790    /// Returns a locator that matches elements by their associated label text.
791    ///
792    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-label>
793    pub async fn get_by_label(&self, text: &str, exact: bool) -> crate::protocol::Locator {
794        self.locator(&crate::protocol::locator::get_by_label_selector(
795            text, exact,
796        ))
797        .await
798    }
799
800    /// Returns a locator that matches elements by their placeholder text.
801    ///
802    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-placeholder>
803    pub async fn get_by_placeholder(&self, text: &str, exact: bool) -> crate::protocol::Locator {
804        self.locator(&crate::protocol::locator::get_by_placeholder_selector(
805            text, exact,
806        ))
807        .await
808    }
809
810    /// Returns a locator that matches elements by their alt text.
811    ///
812    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-alt-text>
813    pub async fn get_by_alt_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
814        self.locator(&crate::protocol::locator::get_by_alt_text_selector(
815            text, exact,
816        ))
817        .await
818    }
819
820    /// Returns a locator that matches elements by their title attribute.
821    ///
822    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-title>
823    pub async fn get_by_title(&self, text: &str, exact: bool) -> crate::protocol::Locator {
824        self.locator(&crate::protocol::locator::get_by_title_selector(
825            text, exact,
826        ))
827        .await
828    }
829
830    /// Returns a locator that matches elements by their test ID attribute.
831    ///
832    /// By default, uses the `data-testid` attribute. Call
833    /// [`playwright.selectors().set_test_id_attribute()`](crate::protocol::Selectors::set_test_id_attribute)
834    /// to change the attribute name.
835    ///
836    /// Always uses exact matching (case-sensitive).
837    ///
838    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-test-id>
839    pub async fn get_by_test_id(&self, test_id: &str) -> crate::protocol::Locator {
840        let attr = self.connection().selectors().test_id_attribute();
841        self.locator(&crate::protocol::locator::get_by_test_id_selector_with_attr(test_id, &attr))
842            .await
843    }
844
845    /// Returns a locator that matches elements by their ARIA role.
846    ///
847    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
848    pub async fn get_by_role(
849        &self,
850        role: crate::protocol::locator::AriaRole,
851        options: Option<crate::protocol::locator::GetByRoleOptions>,
852    ) -> crate::protocol::Locator {
853        self.locator(&crate::protocol::locator::get_by_role_selector(
854            role, options,
855        ))
856        .await
857    }
858
859    /// Returns the keyboard instance for low-level keyboard control.
860    ///
861    /// See: <https://playwright.dev/docs/api/class-page#page-keyboard>
862    pub fn keyboard(&self) -> crate::protocol::Keyboard {
863        crate::protocol::Keyboard::new(self.clone())
864    }
865
866    /// Returns the mouse instance for low-level mouse control.
867    ///
868    /// See: <https://playwright.dev/docs/api/class-page#page-mouse>
869    pub fn mouse(&self) -> crate::protocol::Mouse {
870        crate::protocol::Mouse::new(self.clone())
871    }
872
873    // Internal keyboard methods (called by Keyboard struct)
874
875    pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
876        self.channel()
877            .send_no_result(
878                "keyboardDown",
879                serde_json::json!({
880                    "key": key
881                }),
882            )
883            .await
884    }
885
886    pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
887        self.channel()
888            .send_no_result(
889                "keyboardUp",
890                serde_json::json!({
891                    "key": key
892                }),
893            )
894            .await
895    }
896
897    pub(crate) async fn keyboard_press(
898        &self,
899        key: &str,
900        options: Option<crate::protocol::KeyboardOptions>,
901    ) -> Result<()> {
902        let mut params = serde_json::json!({
903            "key": key
904        });
905
906        if let Some(opts) = options {
907            let opts_json = opts.to_json();
908            if let Some(obj) = params.as_object_mut()
909                && let Some(opts_obj) = opts_json.as_object()
910            {
911                obj.extend(opts_obj.clone());
912            }
913        }
914
915        self.channel().send_no_result("keyboardPress", params).await
916    }
917
918    pub(crate) async fn keyboard_type(
919        &self,
920        text: &str,
921        options: Option<crate::protocol::KeyboardOptions>,
922    ) -> Result<()> {
923        let mut params = serde_json::json!({
924            "text": text
925        });
926
927        if let Some(opts) = options {
928            let opts_json = opts.to_json();
929            if let Some(obj) = params.as_object_mut()
930                && let Some(opts_obj) = opts_json.as_object()
931            {
932                obj.extend(opts_obj.clone());
933            }
934        }
935
936        self.channel().send_no_result("keyboardType", params).await
937    }
938
939    pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
940        self.channel()
941            .send_no_result(
942                "keyboardInsertText",
943                serde_json::json!({
944                    "text": text
945                }),
946            )
947            .await
948    }
949
950    // Internal mouse methods (called by Mouse struct)
951
952    pub(crate) async fn mouse_move(
953        &self,
954        x: i32,
955        y: i32,
956        options: Option<crate::protocol::MouseOptions>,
957    ) -> Result<()> {
958        let mut params = serde_json::json!({
959            "x": x,
960            "y": y
961        });
962
963        if let Some(opts) = options {
964            let opts_json = opts.to_json();
965            if let Some(obj) = params.as_object_mut()
966                && let Some(opts_obj) = opts_json.as_object()
967            {
968                obj.extend(opts_obj.clone());
969            }
970        }
971
972        self.channel().send_no_result("mouseMove", params).await
973    }
974
975    pub(crate) async fn mouse_click(
976        &self,
977        x: i32,
978        y: i32,
979        options: Option<crate::protocol::MouseOptions>,
980    ) -> Result<()> {
981        let mut params = serde_json::json!({
982            "x": x,
983            "y": y
984        });
985
986        if let Some(opts) = options {
987            let opts_json = opts.to_json();
988            if let Some(obj) = params.as_object_mut()
989                && let Some(opts_obj) = opts_json.as_object()
990            {
991                obj.extend(opts_obj.clone());
992            }
993        }
994
995        self.channel().send_no_result("mouseClick", params).await
996    }
997
998    pub(crate) async fn mouse_dblclick(
999        &self,
1000        x: i32,
1001        y: i32,
1002        options: Option<crate::protocol::MouseOptions>,
1003    ) -> Result<()> {
1004        let mut params = serde_json::json!({
1005            "x": x,
1006            "y": y,
1007            "clickCount": 2
1008        });
1009
1010        if let Some(opts) = options {
1011            let opts_json = opts.to_json();
1012            if let Some(obj) = params.as_object_mut()
1013                && let Some(opts_obj) = opts_json.as_object()
1014            {
1015                obj.extend(opts_obj.clone());
1016            }
1017        }
1018
1019        self.channel().send_no_result("mouseClick", params).await
1020    }
1021
1022    pub(crate) async fn mouse_down(
1023        &self,
1024        options: Option<crate::protocol::MouseOptions>,
1025    ) -> Result<()> {
1026        let mut params = serde_json::json!({});
1027
1028        if let Some(opts) = options {
1029            let opts_json = opts.to_json();
1030            if let Some(obj) = params.as_object_mut()
1031                && let Some(opts_obj) = opts_json.as_object()
1032            {
1033                obj.extend(opts_obj.clone());
1034            }
1035        }
1036
1037        self.channel().send_no_result("mouseDown", params).await
1038    }
1039
1040    pub(crate) async fn mouse_up(
1041        &self,
1042        options: Option<crate::protocol::MouseOptions>,
1043    ) -> Result<()> {
1044        let mut params = serde_json::json!({});
1045
1046        if let Some(opts) = options {
1047            let opts_json = opts.to_json();
1048            if let Some(obj) = params.as_object_mut()
1049                && let Some(opts_obj) = opts_json.as_object()
1050            {
1051                obj.extend(opts_obj.clone());
1052            }
1053        }
1054
1055        self.channel().send_no_result("mouseUp", params).await
1056    }
1057
1058    pub(crate) async fn mouse_wheel(&self, delta_x: i32, delta_y: i32) -> Result<()> {
1059        self.channel()
1060            .send_no_result(
1061                "mouseWheel",
1062                serde_json::json!({
1063                    "deltaX": delta_x,
1064                    "deltaY": delta_y
1065                }),
1066            )
1067            .await
1068    }
1069
1070    /// Reloads the current page.
1071    ///
1072    /// # Arguments
1073    ///
1074    /// * `options` - Optional reload options (timeout, wait_until)
1075    ///
1076    /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
1077    /// about:blank). This matches Playwright's behavior across all language bindings.
1078    ///
1079    /// See: <https://playwright.dev/docs/api/class-page#page-reload>
1080    pub async fn reload(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1081        self.navigate_history("reload", options).await
1082    }
1083
1084    /// Navigates to the previous page in history.
1085    ///
1086    /// Returns the main resource response. In case of multiple server redirects, the navigation
1087    /// will resolve with the response of the last redirect. If can not go back, returns `None`.
1088    ///
1089    /// See: <https://playwright.dev/docs/api/class-page#page-go-back>
1090    pub async fn go_back(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1091        self.navigate_history("goBack", options).await
1092    }
1093
1094    /// Navigates to the next page in history.
1095    ///
1096    /// Returns the main resource response. In case of multiple server redirects, the navigation
1097    /// will resolve with the response of the last redirect. If can not go forward, returns `None`.
1098    ///
1099    /// See: <https://playwright.dev/docs/api/class-page#page-go-forward>
1100    pub async fn go_forward(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1101        self.navigate_history("goForward", options).await
1102    }
1103
1104    /// Shared implementation for reload, go_back and go_forward.
1105    async fn navigate_history(
1106        &self,
1107        method: &str,
1108        options: Option<GotoOptions>,
1109    ) -> Result<Option<Response>> {
1110        // Inject the page-level navigation timeout when no explicit timeout is given
1111        let opts = self.with_navigation_timeout(options);
1112        let mut params = serde_json::json!({});
1113
1114        // opts.timeout is always Some(...) because with_navigation_timeout guarantees it
1115        if let Some(timeout) = opts.timeout {
1116            params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
1117        } else {
1118            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1119        }
1120        if let Some(wait_until) = opts.wait_until {
1121            params["waitUntil"] = serde_json::json!(wait_until.as_str());
1122        }
1123
1124        #[derive(Deserialize)]
1125        struct NavigationResponse {
1126            response: Option<ResponseReference>,
1127        }
1128
1129        #[derive(Deserialize)]
1130        struct ResponseReference {
1131            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
1132            guid: Arc<str>,
1133        }
1134
1135        let result: NavigationResponse = self.channel().send(method, params).await?;
1136
1137        if let Some(response_ref) = result.response {
1138            let response_arc = {
1139                let mut attempts = 0;
1140                let max_attempts = 20;
1141                loop {
1142                    match self.connection().get_object(&response_ref.guid).await {
1143                        Ok(obj) => break obj,
1144                        Err(_) if attempts < max_attempts => {
1145                            attempts += 1;
1146                            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1147                        }
1148                        Err(e) => return Err(e),
1149                    }
1150                }
1151            };
1152
1153            let initializer = response_arc.initializer();
1154
1155            let status = initializer["status"].as_u64().ok_or_else(|| {
1156                crate::error::Error::ProtocolError("Response missing status".to_string())
1157            })? as u16;
1158
1159            let headers = initializer["headers"]
1160                .as_array()
1161                .ok_or_else(|| {
1162                    crate::error::Error::ProtocolError("Response missing headers".to_string())
1163                })?
1164                .iter()
1165                .filter_map(|h| {
1166                    let name = h["name"].as_str()?;
1167                    let value = h["value"].as_str()?;
1168                    Some((name.to_string(), value.to_string()))
1169                })
1170                .collect();
1171
1172            let response = Response::new(
1173                initializer["url"]
1174                    .as_str()
1175                    .ok_or_else(|| {
1176                        crate::error::Error::ProtocolError("Response missing url".to_string())
1177                    })?
1178                    .to_string(),
1179                status,
1180                initializer["statusText"].as_str().unwrap_or("").to_string(),
1181                headers,
1182                Some(response_arc),
1183            );
1184
1185            if let Ok(mut page_url) = self.url.write() {
1186                *page_url = response.url().to_string();
1187            }
1188
1189            Ok(Some(response))
1190        } else {
1191            Ok(None)
1192        }
1193    }
1194
1195    /// Returns the first element matching the selector, or None if not found.
1196    ///
1197    /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
1198    pub async fn query_selector(
1199        &self,
1200        selector: &str,
1201    ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
1202        let frame = self.main_frame().await?;
1203        frame.query_selector(selector).await
1204    }
1205
1206    /// Returns all elements matching the selector.
1207    ///
1208    /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
1209    pub async fn query_selector_all(
1210        &self,
1211        selector: &str,
1212    ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
1213        let frame = self.main_frame().await?;
1214        frame.query_selector_all(selector).await
1215    }
1216
1217    /// Takes a screenshot of the page and returns the image bytes.
1218    ///
1219    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1220    pub async fn screenshot(
1221        &self,
1222        options: Option<crate::protocol::ScreenshotOptions>,
1223    ) -> Result<Vec<u8>> {
1224        let params = if let Some(opts) = options {
1225            opts.to_json()
1226        } else {
1227            // Default to PNG with required timeout
1228            serde_json::json!({
1229                "type": "png",
1230                "timeout": crate::DEFAULT_TIMEOUT_MS
1231            })
1232        };
1233
1234        #[derive(Deserialize)]
1235        struct ScreenshotResponse {
1236            binary: String,
1237        }
1238
1239        let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
1240
1241        // Decode base64 to bytes
1242        let bytes = base64::prelude::BASE64_STANDARD
1243            .decode(&response.binary)
1244            .map_err(|e| {
1245                crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
1246            })?;
1247
1248        Ok(bytes)
1249    }
1250
1251    /// Takes a screenshot and saves it to a file, also returning the bytes.
1252    ///
1253    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1254    pub async fn screenshot_to_file(
1255        &self,
1256        path: &std::path::Path,
1257        options: Option<crate::protocol::ScreenshotOptions>,
1258    ) -> Result<Vec<u8>> {
1259        // Get the screenshot bytes
1260        let bytes = self.screenshot(options).await?;
1261
1262        // Write to file
1263        tokio::fs::write(path, &bytes).await.map_err(|e| {
1264            crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
1265        })?;
1266
1267        Ok(bytes)
1268    }
1269
1270    /// Evaluates JavaScript in the page context (without return value).
1271    ///
1272    /// Executes the provided JavaScript expression or function within the page's
1273    /// context without returning a value.
1274    ///
1275    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1276    pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
1277        // Delegate to the main frame
1278        let frame = self.main_frame().await?;
1279        frame.frame_evaluate_expression(expression).await
1280    }
1281
1282    /// Evaluates JavaScript in the page context with optional arguments.
1283    ///
1284    /// Executes the provided JavaScript expression or function within the page's
1285    /// context and returns the result. The return value must be JSON-serializable.
1286    ///
1287    /// # Arguments
1288    ///
1289    /// * `expression` - JavaScript code to evaluate
1290    /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1291    ///
1292    /// # Returns
1293    ///
1294    /// The result as a `serde_json::Value`
1295    ///
1296    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1297    pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
1298        &self,
1299        expression: &str,
1300        arg: Option<&T>,
1301    ) -> Result<U> {
1302        // Delegate to the main frame
1303        let frame = self.main_frame().await?;
1304        let result = frame.evaluate(expression, arg).await?;
1305        serde_json::from_value(result).map_err(Error::from)
1306    }
1307
1308    /// Evaluates a JavaScript expression and returns the result as a String.
1309    ///
1310    /// # Arguments
1311    ///
1312    /// * `expression` - JavaScript code to evaluate
1313    ///
1314    /// # Returns
1315    ///
1316    /// The result converted to a String
1317    ///
1318    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1319    pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
1320        let frame = self.main_frame().await?;
1321        frame.frame_evaluate_expression_value(expression).await
1322    }
1323
1324    /// Registers a route handler for network interception.
1325    ///
1326    /// When a request matches the specified pattern, the handler will be called
1327    /// with a Route object that can abort, continue, or fulfill the request.
1328    ///
1329    /// # Arguments
1330    ///
1331    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
1332    /// * `handler` - Async closure that handles the route
1333    ///
1334    /// See: <https://playwright.dev/docs/api/class-page#page-route>
1335    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1336    where
1337        F: Fn(Route) -> Fut + Send + Sync + 'static,
1338        Fut: Future<Output = Result<()>> + Send + 'static,
1339    {
1340        // 1. Wrap handler in Arc with type erasure
1341        let handler =
1342            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1343
1344        // 2. Store in handlers list
1345        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1346            pattern: pattern.to_string(),
1347            handler,
1348        });
1349
1350        // 3. Enable network interception via protocol
1351        self.enable_network_interception().await?;
1352
1353        Ok(())
1354    }
1355
1356    /// Updates network interception patterns for this page
1357    async fn enable_network_interception(&self) -> Result<()> {
1358        // Collect all patterns from registered handlers
1359        // Each pattern must be an object with "glob" field
1360        let patterns: Vec<serde_json::Value> = self
1361            .route_handlers
1362            .lock()
1363            .unwrap()
1364            .iter()
1365            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1366            .collect();
1367
1368        // Send protocol command to update network interception patterns
1369        // Follows playwright-python's approach
1370        self.channel()
1371            .send_no_result(
1372                "setNetworkInterceptionPatterns",
1373                serde_json::json!({
1374                    "patterns": patterns
1375                }),
1376            )
1377            .await
1378    }
1379
1380    /// Removes route handler(s) matching the given URL pattern.
1381    ///
1382    /// # Arguments
1383    ///
1384    /// * `pattern` - URL pattern to remove handlers for
1385    ///
1386    /// See: <https://playwright.dev/docs/api/class-page#page-unroute>
1387    pub async fn unroute(&self, pattern: &str) -> Result<()> {
1388        self.route_handlers
1389            .lock()
1390            .unwrap()
1391            .retain(|entry| entry.pattern != pattern);
1392        self.enable_network_interception().await
1393    }
1394
1395    /// Removes all registered route handlers.
1396    ///
1397    /// # Arguments
1398    ///
1399    /// * `behavior` - Optional behavior for in-flight handlers
1400    ///
1401    /// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
1402    pub async fn unroute_all(
1403        &self,
1404        _behavior: Option<crate::protocol::route::UnrouteBehavior>,
1405    ) -> Result<()> {
1406        self.route_handlers.lock().unwrap().clear();
1407        self.enable_network_interception().await
1408    }
1409
1410    /// Handles a route event from the protocol
1411    ///
1412    /// Called by on_event when a "route" event is received.
1413    /// Supports handler chaining via `route.fallback()` — if a handler calls
1414    /// `fallback()` instead of `continue_()`, `abort()`, or `fulfill()`, the
1415    /// next matching handler in the chain is tried.
1416    async fn on_route_event(&self, route: Route) {
1417        let handlers = self.route_handlers.lock().unwrap().clone();
1418        let url = route.request().url().to_string();
1419
1420        // Find matching handler (last registered wins, with fallback chaining)
1421        for entry in handlers.iter().rev() {
1422            if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
1423                let handler = entry.handler.clone();
1424                if let Err(e) = handler(route.clone()).await {
1425                    tracing::warn!("Route handler error: {}", e);
1426                    break;
1427                }
1428                // If handler called fallback(), try the next matching handler
1429                if !route.was_handled() {
1430                    continue;
1431                }
1432                break;
1433            }
1434        }
1435    }
1436
1437    /// Registers a download event handler.
1438    ///
1439    /// The handler will be called when a download is triggered by the page.
1440    /// Downloads occur when the page initiates a file download (e.g., clicking a link
1441    /// with the download attribute, or a server response with Content-Disposition: attachment).
1442    ///
1443    /// # Arguments
1444    ///
1445    /// * `handler` - Async closure that receives the Download object
1446    ///
1447    /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
1448    pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1449    where
1450        F: Fn(Download) -> Fut + Send + Sync + 'static,
1451        Fut: Future<Output = Result<()>> + Send + 'static,
1452    {
1453        // Wrap handler with type erasure
1454        let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
1455            Box::pin(handler(download))
1456        });
1457
1458        // Store handler
1459        self.download_handlers.lock().unwrap().push(handler);
1460
1461        Ok(())
1462    }
1463
1464    /// Registers a dialog event handler.
1465    ///
1466    /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
1467    /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
1468    ///
1469    /// # Arguments
1470    ///
1471    /// * `handler` - Async closure that receives the Dialog object
1472    ///
1473    /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
1474    pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1475    where
1476        F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1477        Fut: Future<Output = Result<()>> + Send + 'static,
1478    {
1479        // Wrap handler with type erasure
1480        let handler =
1481            Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
1482
1483        // Store handler
1484        self.dialog_handlers.lock().unwrap().push(handler);
1485
1486        // Dialog events are auto-emitted (no subscription needed)
1487
1488        Ok(())
1489    }
1490
1491    /// Registers a console event handler.
1492    ///
1493    /// The handler is called whenever the page emits a JavaScript console message
1494    /// (e.g. `console.log`, `console.error`, `console.warn`, etc.).
1495    ///
1496    /// The server only sends console events after the first handler is registered
1497    /// (subscription is managed automatically).
1498    ///
1499    /// # Arguments
1500    ///
1501    /// * `handler` - Async closure that receives the [`ConsoleMessage`](crate::protocol::ConsoleMessage)
1502    ///
1503    /// See: <https://playwright.dev/docs/api/class-page#page-event-console>
1504    pub async fn on_console<F, Fut>(&self, handler: F) -> Result<()>
1505    where
1506        F: Fn(crate::protocol::ConsoleMessage) -> Fut + Send + Sync + 'static,
1507        Fut: Future<Output = Result<()>> + Send + 'static,
1508    {
1509        let handler = Arc::new(
1510            move |msg: crate::protocol::ConsoleMessage| -> ConsoleHandlerFuture {
1511                Box::pin(handler(msg))
1512            },
1513        );
1514
1515        let needs_subscription = {
1516            let handlers = self.console_handlers.lock().unwrap();
1517            let waiters = self.console_waiters.lock().unwrap();
1518            handlers.is_empty() && waiters.is_empty()
1519        };
1520        if needs_subscription {
1521            _ = self.channel().update_subscription("console", true).await;
1522        }
1523        self.console_handlers.lock().unwrap().push(handler);
1524
1525        Ok(())
1526    }
1527
1528    /// Registers a handler for file chooser events.
1529    ///
1530    /// The handler is called whenever the page opens a file chooser dialog
1531    /// (e.g. when the user clicks an `<input type="file">` element).
1532    ///
1533    /// Use [`FileChooser::set_files`](crate::protocol::FileChooser::set_files) inside
1534    /// the handler to satisfy the file chooser without OS-level interaction.
1535    ///
1536    /// The server only sends `"fileChooser"` events after the first handler is
1537    /// registered (subscription is managed automatically via `updateSubscription`).
1538    ///
1539    /// # Arguments
1540    ///
1541    /// * `handler` - Async closure that receives a [`FileChooser`](crate::protocol::FileChooser)
1542    ///
1543    /// # Example
1544    ///
1545    /// ```ignore
1546    /// page.on_filechooser(|chooser| async move {
1547    ///     chooser.set_files(&[std::path::PathBuf::from("/tmp/file.txt")]).await
1548    /// }).await?;
1549    /// ```
1550    ///
1551    /// See: <https://playwright.dev/docs/api/class-page#page-event-file-chooser>
1552    pub async fn on_filechooser<F, Fut>(&self, handler: F) -> Result<()>
1553    where
1554        F: Fn(crate::protocol::FileChooser) -> Fut + Send + Sync + 'static,
1555        Fut: Future<Output = Result<()>> + Send + 'static,
1556    {
1557        let handler = Arc::new(
1558            move |chooser: crate::protocol::FileChooser| -> FileChooserHandlerFuture {
1559                Box::pin(handler(chooser))
1560            },
1561        );
1562
1563        let needs_subscription = {
1564            let handlers = self.filechooser_handlers.lock().unwrap();
1565            let waiters = self.filechooser_waiters.lock().unwrap();
1566            handlers.is_empty() && waiters.is_empty()
1567        };
1568        if needs_subscription {
1569            _ = self
1570                .channel()
1571                .update_subscription("fileChooser", true)
1572                .await;
1573        }
1574        self.filechooser_handlers.lock().unwrap().push(handler);
1575
1576        Ok(())
1577    }
1578
1579    /// Creates a one-shot waiter that resolves when the next file chooser opens.
1580    ///
1581    /// The waiter **must** be created before the action that triggers the file
1582    /// chooser to avoid a race condition.
1583    ///
1584    /// # Arguments
1585    ///
1586    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1587    ///
1588    /// # Errors
1589    ///
1590    /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if the file chooser
1591    /// does not open within the timeout.
1592    ///
1593    /// # Example
1594    ///
1595    /// ```ignore
1596    /// // Set up waiter BEFORE triggering the file chooser
1597    /// let waiter = page.expect_file_chooser(None).await?;
1598    /// page.locator("input[type=file]").await.click(None).await?;
1599    /// let chooser = waiter.wait().await?;
1600    /// chooser.set_files(&[PathBuf::from("/tmp/file.txt")]).await?;
1601    /// ```
1602    ///
1603    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1604    pub async fn expect_file_chooser(
1605        &self,
1606        timeout: Option<f64>,
1607    ) -> Result<crate::protocol::EventWaiter<crate::protocol::FileChooser>> {
1608        let (tx, rx) = tokio::sync::oneshot::channel();
1609
1610        let needs_subscription = {
1611            let handlers = self.filechooser_handlers.lock().unwrap();
1612            let waiters = self.filechooser_waiters.lock().unwrap();
1613            handlers.is_empty() && waiters.is_empty()
1614        };
1615        if needs_subscription {
1616            _ = self
1617                .channel()
1618                .update_subscription("fileChooser", true)
1619                .await;
1620        }
1621        self.filechooser_waiters.lock().unwrap().push(tx);
1622
1623        Ok(crate::protocol::EventWaiter::new(
1624            rx,
1625            timeout.or(Some(30_000.0)),
1626        ))
1627    }
1628
1629    /// Creates a one-shot waiter that resolves when the next popup window opens.
1630    ///
1631    /// The waiter **must** be created before the action that opens the popup to
1632    /// avoid a race condition.
1633    ///
1634    /// # Arguments
1635    ///
1636    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1637    ///
1638    /// # Errors
1639    ///
1640    /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no popup
1641    /// opens within the timeout.
1642    ///
1643    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1644    pub async fn expect_popup(
1645        &self,
1646        timeout: Option<f64>,
1647    ) -> Result<crate::protocol::EventWaiter<Page>> {
1648        let (tx, rx) = tokio::sync::oneshot::channel();
1649        self.popup_waiters.lock().unwrap().push(tx);
1650        Ok(crate::protocol::EventWaiter::new(
1651            rx,
1652            timeout.or(Some(30_000.0)),
1653        ))
1654    }
1655
1656    /// Creates a one-shot waiter that resolves when the next download starts.
1657    ///
1658    /// The waiter **must** be created before the action that triggers the download
1659    /// to avoid a race condition.
1660    ///
1661    /// # Arguments
1662    ///
1663    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1664    ///
1665    /// # Errors
1666    ///
1667    /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no download
1668    /// starts within the timeout.
1669    ///
1670    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1671    pub async fn expect_download(
1672        &self,
1673        timeout: Option<f64>,
1674    ) -> Result<crate::protocol::EventWaiter<Download>> {
1675        let (tx, rx) = tokio::sync::oneshot::channel();
1676        self.download_waiters.lock().unwrap().push(tx);
1677        Ok(crate::protocol::EventWaiter::new(
1678            rx,
1679            timeout.or(Some(30_000.0)),
1680        ))
1681    }
1682
1683    /// Creates a one-shot waiter that resolves when the next network response is received.
1684    ///
1685    /// The waiter **must** be created before the action that triggers the response
1686    /// to avoid a race condition.
1687    ///
1688    /// # Arguments
1689    ///
1690    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1691    ///
1692    /// # Errors
1693    ///
1694    /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no response
1695    /// arrives within the timeout.
1696    ///
1697    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1698    pub async fn expect_response(
1699        &self,
1700        timeout: Option<f64>,
1701    ) -> Result<crate::protocol::EventWaiter<ResponseObject>> {
1702        let (tx, rx) = tokio::sync::oneshot::channel();
1703
1704        let needs_subscription = {
1705            let handlers = self.response_handlers.lock().unwrap();
1706            let waiters = self.response_waiters.lock().unwrap();
1707            handlers.is_empty() && waiters.is_empty()
1708        };
1709        if needs_subscription {
1710            _ = self.channel().update_subscription("response", true).await;
1711        }
1712        self.response_waiters.lock().unwrap().push(tx);
1713
1714        Ok(crate::protocol::EventWaiter::new(
1715            rx,
1716            timeout.or(Some(30_000.0)),
1717        ))
1718    }
1719
1720    /// Creates a one-shot waiter that resolves when the next network request is issued.
1721    ///
1722    /// The waiter **must** be created before the action that issues the request
1723    /// to avoid a race condition.
1724    ///
1725    /// # Arguments
1726    ///
1727    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1728    ///
1729    /// # Errors
1730    ///
1731    /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no request
1732    /// is issued within the timeout.
1733    ///
1734    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1735    pub async fn expect_request(
1736        &self,
1737        timeout: Option<f64>,
1738    ) -> Result<crate::protocol::EventWaiter<Request>> {
1739        let (tx, rx) = tokio::sync::oneshot::channel();
1740
1741        let needs_subscription = {
1742            let handlers = self.request_handlers.lock().unwrap();
1743            let waiters = self.request_waiters.lock().unwrap();
1744            handlers.is_empty() && waiters.is_empty()
1745        };
1746        if needs_subscription {
1747            _ = self.channel().update_subscription("request", true).await;
1748        }
1749        self.request_waiters.lock().unwrap().push(tx);
1750
1751        Ok(crate::protocol::EventWaiter::new(
1752            rx,
1753            timeout.or(Some(30_000.0)),
1754        ))
1755    }
1756
1757    /// Creates a one-shot waiter that resolves when the next console message is produced.
1758    ///
1759    /// The waiter **must** be created before the action that produces the console
1760    /// message to avoid a race condition.
1761    ///
1762    /// # Arguments
1763    ///
1764    /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1765    ///
1766    /// # Errors
1767    ///
1768    /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no console
1769    /// message is produced within the timeout.
1770    ///
1771    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1772    pub async fn expect_console_message(
1773        &self,
1774        timeout: Option<f64>,
1775    ) -> Result<crate::protocol::EventWaiter<crate::protocol::ConsoleMessage>> {
1776        let (tx, rx) = tokio::sync::oneshot::channel();
1777
1778        let needs_subscription = {
1779            let handlers = self.console_handlers.lock().unwrap();
1780            let waiters = self.console_waiters.lock().unwrap();
1781            handlers.is_empty() && waiters.is_empty()
1782        };
1783        if needs_subscription {
1784            _ = self.channel().update_subscription("console", true).await;
1785        }
1786        self.console_waiters.lock().unwrap().push(tx);
1787
1788        Ok(crate::protocol::EventWaiter::new(
1789            rx,
1790            timeout.or(Some(30_000.0)),
1791        ))
1792    }
1793
1794    /// See: <https://playwright.dev/docs/api/class-page#page-event-request>
1795    pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
1796    where
1797        F: Fn(Request) -> Fut + Send + Sync + 'static,
1798        Fut: Future<Output = Result<()>> + Send + 'static,
1799    {
1800        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1801            Box::pin(handler(request))
1802        });
1803
1804        let needs_subscription = {
1805            let handlers = self.request_handlers.lock().unwrap();
1806            let waiters = self.request_waiters.lock().unwrap();
1807            handlers.is_empty() && waiters.is_empty()
1808        };
1809        if needs_subscription {
1810            _ = self.channel().update_subscription("request", true).await;
1811        }
1812        self.request_handlers.lock().unwrap().push(handler);
1813
1814        Ok(())
1815    }
1816
1817    /// See: <https://playwright.dev/docs/api/class-page#page-event-request-finished>
1818    pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
1819    where
1820        F: Fn(Request) -> Fut + Send + Sync + 'static,
1821        Fut: Future<Output = Result<()>> + Send + 'static,
1822    {
1823        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1824            Box::pin(handler(request))
1825        });
1826
1827        let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
1828        if needs_subscription {
1829            _ = self
1830                .channel()
1831                .update_subscription("requestFinished", true)
1832                .await;
1833        }
1834        self.request_finished_handlers.lock().unwrap().push(handler);
1835
1836        Ok(())
1837    }
1838
1839    /// See: <https://playwright.dev/docs/api/class-page#page-event-request-failed>
1840    pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
1841    where
1842        F: Fn(Request) -> Fut + Send + Sync + 'static,
1843        Fut: Future<Output = Result<()>> + Send + 'static,
1844    {
1845        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1846            Box::pin(handler(request))
1847        });
1848
1849        let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
1850        if needs_subscription {
1851            _ = self
1852                .channel()
1853                .update_subscription("requestFailed", true)
1854                .await;
1855        }
1856        self.request_failed_handlers.lock().unwrap().push(handler);
1857
1858        Ok(())
1859    }
1860
1861    /// See: <https://playwright.dev/docs/api/class-page#page-event-response>
1862    pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
1863    where
1864        F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
1865        Fut: Future<Output = Result<()>> + Send + 'static,
1866    {
1867        let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
1868            Box::pin(handler(response))
1869        });
1870
1871        let needs_subscription = {
1872            let handlers = self.response_handlers.lock().unwrap();
1873            let waiters = self.response_waiters.lock().unwrap();
1874            handlers.is_empty() && waiters.is_empty()
1875        };
1876        if needs_subscription {
1877            _ = self.channel().update_subscription("response", true).await;
1878        }
1879        self.response_handlers.lock().unwrap().push(handler);
1880
1881        Ok(())
1882    }
1883
1884    /// Adds a listener for the `websocket` event.
1885    ///
1886    /// The handler will be called when a WebSocket request is dispatched.
1887    ///
1888    /// # Arguments
1889    ///
1890    /// * `handler` - The function to call when the event occurs
1891    ///
1892    /// See: <https://playwright.dev/docs/api/class-page#page-on-websocket>
1893    pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
1894    where
1895        F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1896        Fut: Future<Output = Result<()>> + Send + 'static,
1897    {
1898        let handler =
1899            Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
1900        self.websocket_handlers.lock().unwrap().push(handler);
1901        Ok(())
1902    }
1903
1904    /// Registers a handler for the `worker` event.
1905    ///
1906    /// The handler is called when a new Web Worker is created in the page.
1907    ///
1908    /// # Arguments
1909    ///
1910    /// * `handler` - Async closure called with the new [`Worker`] object
1911    ///
1912    /// See: <https://playwright.dev/docs/api/class-page#page-event-worker>
1913    pub async fn on_worker<F, Fut>(&self, handler: F) -> Result<()>
1914    where
1915        F: Fn(Worker) -> Fut + Send + Sync + 'static,
1916        Fut: Future<Output = Result<()>> + Send + 'static,
1917    {
1918        let handler = Arc::new(move |w: Worker| -> WorkerHandlerFuture { Box::pin(handler(w)) });
1919        self.worker_handlers.lock().unwrap().push(handler);
1920        Ok(())
1921    }
1922
1923    /// Registers a handler for the `close` event.
1924    ///
1925    /// The handler is called when the page is closed, either by calling `page.close()`,
1926    /// by the browser context being closed, or when the browser process exits.
1927    ///
1928    /// # Arguments
1929    ///
1930    /// * `handler` - Async closure called with no arguments when the page closes
1931    ///
1932    /// See: <https://playwright.dev/docs/api/class-page#page-event-close>
1933    pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
1934    where
1935        F: Fn() -> Fut + Send + Sync + 'static,
1936        Fut: Future<Output = Result<()>> + Send + 'static,
1937    {
1938        let handler = Arc::new(move || -> CloseHandlerFuture { Box::pin(handler()) });
1939        self.close_handlers.lock().unwrap().push(handler);
1940        Ok(())
1941    }
1942
1943    /// Registers a handler for the `load` event.
1944    ///
1945    /// The handler is called when the page's `load` event fires, i.e. after
1946    /// all resources including stylesheets and images have finished loading.
1947    ///
1948    /// The server only sends `"load"` events after the first handler is registered
1949    /// (subscription is managed automatically).
1950    ///
1951    /// # Arguments
1952    ///
1953    /// * `handler` - Async closure called with no arguments when the page loads
1954    ///
1955    /// See: <https://playwright.dev/docs/api/class-page#page-event-load>
1956    pub async fn on_load<F, Fut>(&self, handler: F) -> Result<()>
1957    where
1958        F: Fn() -> Fut + Send + Sync + 'static,
1959        Fut: Future<Output = Result<()>> + Send + 'static,
1960    {
1961        let handler = Arc::new(move || -> LoadHandlerFuture { Box::pin(handler()) });
1962        // "load" events come via Frame's "loadstate" event, no subscription needed.
1963        self.load_handlers.lock().unwrap().push(handler);
1964        Ok(())
1965    }
1966
1967    /// Registers a handler for the `crash` event.
1968    ///
1969    /// The handler is called when the page crashes (e.g. runs out of memory).
1970    ///
1971    /// # Arguments
1972    ///
1973    /// * `handler` - Async closure called with no arguments when the page crashes
1974    ///
1975    /// See: <https://playwright.dev/docs/api/class-page#page-event-crash>
1976    pub async fn on_crash<F, Fut>(&self, handler: F) -> Result<()>
1977    where
1978        F: Fn() -> Fut + Send + Sync + 'static,
1979        Fut: Future<Output = Result<()>> + Send + 'static,
1980    {
1981        let handler = Arc::new(move || -> CrashHandlerFuture { Box::pin(handler()) });
1982        self.crash_handlers.lock().unwrap().push(handler);
1983        Ok(())
1984    }
1985
1986    /// Registers a handler for the `pageError` event.
1987    ///
1988    /// The handler is called when an uncaught JavaScript exception is thrown in the page.
1989    /// The handler receives the error message as a `String`.
1990    ///
1991    /// The server only sends `"pageError"` events after the first handler is registered
1992    /// (subscription is managed automatically).
1993    ///
1994    /// # Arguments
1995    ///
1996    /// * `handler` - Async closure that receives the error message string
1997    ///
1998    /// See: <https://playwright.dev/docs/api/class-page#page-event-page-error>
1999    pub async fn on_pageerror<F, Fut>(&self, handler: F) -> Result<()>
2000    where
2001        F: Fn(String) -> Fut + Send + Sync + 'static,
2002        Fut: Future<Output = Result<()>> + Send + 'static,
2003    {
2004        let handler =
2005            Arc::new(move |msg: String| -> PageErrorHandlerFuture { Box::pin(handler(msg)) });
2006        // "pageError" events come via BrowserContext, no subscription needed.
2007        self.pageerror_handlers.lock().unwrap().push(handler);
2008        Ok(())
2009    }
2010
2011    /// Registers a handler for the `popup` event.
2012    ///
2013    /// The handler is called when the page opens a popup window (e.g. via `window.open()`).
2014    /// The handler receives the new popup [`Page`] object.
2015    ///
2016    /// The server only sends `"popup"` events after the first handler is registered
2017    /// (subscription is managed automatically).
2018    ///
2019    /// # Arguments
2020    ///
2021    /// * `handler` - Async closure that receives the popup Page
2022    ///
2023    /// See: <https://playwright.dev/docs/api/class-page#page-event-popup>
2024    pub async fn on_popup<F, Fut>(&self, handler: F) -> Result<()>
2025    where
2026        F: Fn(Page) -> Fut + Send + Sync + 'static,
2027        Fut: Future<Output = Result<()>> + Send + 'static,
2028    {
2029        let handler = Arc::new(move |page: Page| -> PopupHandlerFuture { Box::pin(handler(page)) });
2030        // "popup" events arrive via BrowserContext's "page" event when a page has an opener.
2031        self.popup_handlers.lock().unwrap().push(handler);
2032        Ok(())
2033    }
2034
2035    /// Registers a handler for the `frameAttached` event.
2036    ///
2037    /// The handler is called when a new frame (iframe) is attached to the page.
2038    /// The handler receives the attached [`Frame`](crate::protocol::Frame) object.
2039    ///
2040    /// # Arguments
2041    ///
2042    /// * `handler` - Async closure that receives the attached Frame
2043    ///
2044    /// See: <https://playwright.dev/docs/api/class-page#page-event-frameattached>
2045    pub async fn on_frameattached<F, Fut>(&self, handler: F) -> Result<()>
2046    where
2047        F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2048        Fut: Future<Output = Result<()>> + Send + 'static,
2049    {
2050        let handler = Arc::new(
2051            move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2052                Box::pin(handler(frame))
2053            },
2054        );
2055        self.frameattached_handlers.lock().unwrap().push(handler);
2056        Ok(())
2057    }
2058
2059    /// Registers a handler for the `frameDetached` event.
2060    ///
2061    /// The handler is called when a frame (iframe) is detached from the page.
2062    /// The handler receives the detached [`Frame`](crate::protocol::Frame) object.
2063    ///
2064    /// # Arguments
2065    ///
2066    /// * `handler` - Async closure that receives the detached Frame
2067    ///
2068    /// See: <https://playwright.dev/docs/api/class-page#page-event-framedetached>
2069    pub async fn on_framedetached<F, Fut>(&self, handler: F) -> Result<()>
2070    where
2071        F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2072        Fut: Future<Output = Result<()>> + Send + 'static,
2073    {
2074        let handler = Arc::new(
2075            move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2076                Box::pin(handler(frame))
2077            },
2078        );
2079        self.framedetached_handlers.lock().unwrap().push(handler);
2080        Ok(())
2081    }
2082
2083    /// Registers a handler for the `frameNavigated` event.
2084    ///
2085    /// The handler is called when a frame navigates to a new URL.
2086    /// The handler receives the navigated [`Frame`](crate::protocol::Frame) object.
2087    ///
2088    /// # Arguments
2089    ///
2090    /// * `handler` - Async closure that receives the navigated Frame
2091    ///
2092    /// See: <https://playwright.dev/docs/api/class-page#page-event-framenavigated>
2093    pub async fn on_framenavigated<F, Fut>(&self, handler: F) -> Result<()>
2094    where
2095        F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2096        Fut: Future<Output = Result<()>> + Send + 'static,
2097    {
2098        let handler = Arc::new(
2099            move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2100                Box::pin(handler(frame))
2101            },
2102        );
2103        self.framenavigated_handlers.lock().unwrap().push(handler);
2104        Ok(())
2105    }
2106
2107    /// Exposes a Rust function to this page as `window[name]` in JavaScript.
2108    ///
2109    /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
2110    /// server fires a `bindingCall` event on the **page** channel that invokes
2111    /// `callback` with the deserialized arguments. The return value is sent back
2112    /// to JS so the `await window[name](…)` expression resolves with it.
2113    ///
2114    /// The binding is page-scoped and not visible to other pages in the same context.
2115    ///
2116    /// # Arguments
2117    ///
2118    /// * `name`     – JavaScript identifier that will be available as `window[name]`.
2119    /// * `callback` – Async closure called with `Vec<serde_json::Value>` (JS arguments)
2120    ///   returning `serde_json::Value` (the result).
2121    ///
2122    /// # Errors
2123    ///
2124    /// Returns error if:
2125    /// - The page has been closed.
2126    /// - Communication with the browser process fails.
2127    ///
2128    /// See: <https://playwright.dev/docs/api/class-page#page-expose-function>
2129    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
2130    where
2131        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2132        Fut: Future<Output = serde_json::Value> + Send + 'static,
2133    {
2134        self.expose_binding_internal(name, false, callback).await
2135    }
2136
2137    /// Exposes a Rust function to this page as `window[name]` in JavaScript,
2138    /// with `needsHandle: true`.
2139    ///
2140    /// Identical to [`expose_function`](Self::expose_function) but the Playwright
2141    /// server passes the first argument as a `JSHandle` object rather than a plain
2142    /// value.
2143    ///
2144    /// # Arguments
2145    ///
2146    /// * `name`     – JavaScript identifier.
2147    /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
2148    ///
2149    /// # Errors
2150    ///
2151    /// Returns error if:
2152    /// - The page has been closed.
2153    /// - Communication with the browser process fails.
2154    ///
2155    /// See: <https://playwright.dev/docs/api/class-page#page-expose-binding>
2156    pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
2157    where
2158        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2159        Fut: Future<Output = serde_json::Value> + Send + 'static,
2160    {
2161        self.expose_binding_internal(name, true, callback).await
2162    }
2163
2164    /// Internal implementation shared by page-level expose_function and expose_binding.
2165    ///
2166    /// Both `expose_function` and `expose_binding` use `needsHandle: false` because
2167    /// the current implementation does not support JSHandle objects. Using
2168    /// `needsHandle: true` would cause the Playwright server to wrap the first
2169    /// argument as a `JSHandle`, which requires a JSHandle protocol object that
2170    /// is not yet implemented.
2171    async fn expose_binding_internal<F, Fut>(
2172        &self,
2173        name: &str,
2174        _needs_handle: bool,
2175        callback: F,
2176    ) -> Result<()>
2177    where
2178        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2179        Fut: Future<Output = serde_json::Value> + Send + 'static,
2180    {
2181        let callback: PageBindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
2182            Box::pin(callback(args)) as PageBindingCallbackFuture
2183        });
2184
2185        // Store callback before sending RPC (avoids race with early bindingCall events)
2186        self.binding_callbacks
2187            .lock()
2188            .unwrap()
2189            .insert(name.to_string(), callback);
2190
2191        // Tell the Playwright server to inject window[name] into this page.
2192        // Always use needsHandle: false — see note above.
2193        self.channel()
2194            .send_no_result(
2195                "exposeBinding",
2196                serde_json::json!({ "name": name, "needsHandle": false }),
2197            )
2198            .await
2199    }
2200
2201    /// Handles a download event from the protocol
2202    async fn on_download_event(&self, download: Download) {
2203        let handlers = self.download_handlers.lock().unwrap().clone();
2204
2205        for handler in handlers {
2206            if let Err(e) = handler(download.clone()).await {
2207                tracing::warn!("Download handler error: {}", e);
2208            }
2209        }
2210        // Notify the first expect_download() waiter (FIFO order)
2211        if let Some(tx) = self.download_waiters.lock().unwrap().pop() {
2212            let _ = tx.send(download);
2213        }
2214    }
2215
2216    /// Handles a dialog event from the protocol
2217    async fn on_dialog_event(&self, dialog: Dialog) {
2218        let handlers = self.dialog_handlers.lock().unwrap().clone();
2219
2220        for handler in handlers {
2221            if let Err(e) = handler(dialog.clone()).await {
2222                tracing::warn!("Dialog handler error: {}", e);
2223            }
2224        }
2225    }
2226
2227    async fn on_request_event(&self, request: Request) {
2228        let handlers = self.request_handlers.lock().unwrap().clone();
2229
2230        for handler in handlers {
2231            if let Err(e) = handler(request.clone()).await {
2232                tracing::warn!("Request handler error: {}", e);
2233            }
2234        }
2235        // Notify the first expect_request() waiter (FIFO order)
2236        if let Some(tx) = self.request_waiters.lock().unwrap().pop() {
2237            let _ = tx.send(request);
2238        }
2239    }
2240
2241    async fn on_request_failed_event(&self, request: Request) {
2242        let handlers = self.request_failed_handlers.lock().unwrap().clone();
2243
2244        for handler in handlers {
2245            if let Err(e) = handler(request.clone()).await {
2246                tracing::warn!("RequestFailed handler error: {}", e);
2247            }
2248        }
2249    }
2250
2251    async fn on_request_finished_event(&self, request: Request) {
2252        let handlers = self.request_finished_handlers.lock().unwrap().clone();
2253
2254        for handler in handlers {
2255            if let Err(e) = handler(request.clone()).await {
2256                tracing::warn!("RequestFinished handler error: {}", e);
2257            }
2258        }
2259    }
2260
2261    async fn on_response_event(&self, response: ResponseObject) {
2262        let handlers = self.response_handlers.lock().unwrap().clone();
2263
2264        for handler in handlers {
2265            if let Err(e) = handler(response.clone()).await {
2266                tracing::warn!("Response handler error: {}", e);
2267            }
2268        }
2269        // Notify the first expect_response() waiter (FIFO order)
2270        if let Some(tx) = self.response_waiters.lock().unwrap().pop() {
2271            let _ = tx.send(response);
2272        }
2273    }
2274
2275    /// Triggers dialog event (called by BrowserContext when dialog events arrive)
2276    ///
2277    /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
2278    /// This method is public so BrowserContext can forward dialog events.
2279    pub async fn trigger_dialog_event(&self, dialog: Dialog) {
2280        self.on_dialog_event(dialog).await;
2281    }
2282
2283    /// Triggers request event (called by BrowserContext when request events arrive)
2284    pub(crate) async fn trigger_request_event(&self, request: Request) {
2285        self.on_request_event(request).await;
2286    }
2287
2288    pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
2289        self.on_request_finished_event(request).await;
2290    }
2291
2292    pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
2293        self.on_request_failed_event(request).await;
2294    }
2295
2296    /// Triggers response event (called by BrowserContext when response events arrive)
2297    pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
2298        self.on_response_event(response).await;
2299    }
2300
2301    /// Triggers console event (called by BrowserContext when console events arrive).
2302    ///
2303    /// The BrowserContext receives all `"console"` events, constructs the
2304    /// [`ConsoleMessage`](crate::protocol::ConsoleMessage), dispatches to
2305    /// context-level handlers, then calls this method to forward to page-level handlers.
2306    pub(crate) async fn trigger_console_event(&self, msg: crate::protocol::ConsoleMessage) {
2307        self.on_console_event(msg).await;
2308    }
2309
2310    async fn on_console_event(&self, msg: crate::protocol::ConsoleMessage) {
2311        // Notify the first expect_console_message() waiter (FIFO order)
2312        if let Some(tx) = self.console_waiters.lock().unwrap().pop() {
2313            let _ = tx.send(msg.clone());
2314        }
2315        let handlers = self.console_handlers.lock().unwrap().clone();
2316        for handler in handlers {
2317            if let Err(e) = handler(msg.clone()).await {
2318                tracing::warn!("Console handler error: {}", e);
2319            }
2320        }
2321    }
2322
2323    /// Dispatches a FileChooser event to registered handlers and one-shot waiters.
2324    async fn on_filechooser_event(&self, chooser: crate::protocol::FileChooser) {
2325        // Dispatch to persistent handlers
2326        let handlers = self.filechooser_handlers.lock().unwrap().clone();
2327        for handler in handlers {
2328            if let Err(e) = handler(chooser.clone()).await {
2329                tracing::warn!("FileChooser handler error: {}", e);
2330            }
2331        }
2332
2333        // Notify the first expect_file_chooser() waiter (FIFO order)
2334        if let Some(tx) = self.filechooser_waiters.lock().unwrap().pop() {
2335            let _ = tx.send(chooser);
2336        }
2337    }
2338
2339    /// Triggers load event (called by Frame when loadstate "load" is added)
2340    pub(crate) async fn trigger_load_event(&self) {
2341        self.on_load_event().await;
2342    }
2343
2344    /// Triggers pageError event (called by BrowserContext when pageError arrives)
2345    pub(crate) async fn trigger_pageerror_event(&self, message: String) {
2346        self.on_pageerror_event(message).await;
2347    }
2348
2349    /// Triggers popup event (called by BrowserContext when a page is opened with an opener)
2350    pub(crate) async fn trigger_popup_event(&self, popup: Page) {
2351        self.on_popup_event(popup).await;
2352    }
2353
2354    /// Triggers frameNavigated event (called by Frame when "navigated" is received)
2355    pub(crate) async fn trigger_framenavigated_event(&self, frame: crate::protocol::Frame) {
2356        self.on_framenavigated_event(frame).await;
2357    }
2358
2359    async fn on_close_event(&self) {
2360        let handlers = self.close_handlers.lock().unwrap().clone();
2361        for handler in handlers {
2362            if let Err(e) = handler().await {
2363                tracing::warn!("Close handler error: {}", e);
2364            }
2365        }
2366    }
2367
2368    async fn on_load_event(&self) {
2369        let handlers = self.load_handlers.lock().unwrap().clone();
2370        for handler in handlers {
2371            if let Err(e) = handler().await {
2372                tracing::warn!("Load handler error: {}", e);
2373            }
2374        }
2375    }
2376
2377    async fn on_crash_event(&self) {
2378        let handlers = self.crash_handlers.lock().unwrap().clone();
2379        for handler in handlers {
2380            if let Err(e) = handler().await {
2381                tracing::warn!("Crash handler error: {}", e);
2382            }
2383        }
2384    }
2385
2386    async fn on_pageerror_event(&self, message: String) {
2387        let handlers = self.pageerror_handlers.lock().unwrap().clone();
2388        for handler in handlers {
2389            if let Err(e) = handler(message.clone()).await {
2390                tracing::warn!("PageError handler error: {}", e);
2391            }
2392        }
2393    }
2394
2395    async fn on_popup_event(&self, popup: Page) {
2396        let handlers = self.popup_handlers.lock().unwrap().clone();
2397        for handler in handlers {
2398            if let Err(e) = handler(popup.clone()).await {
2399                tracing::warn!("Popup handler error: {}", e);
2400            }
2401        }
2402        // Notify the first expect_popup() waiter (FIFO order)
2403        if let Some(tx) = self.popup_waiters.lock().unwrap().pop() {
2404            let _ = tx.send(popup);
2405        }
2406    }
2407
2408    async fn on_frameattached_event(&self, frame: crate::protocol::Frame) {
2409        let handlers = self.frameattached_handlers.lock().unwrap().clone();
2410        for handler in handlers {
2411            if let Err(e) = handler(frame.clone()).await {
2412                tracing::warn!("FrameAttached handler error: {}", e);
2413            }
2414        }
2415    }
2416
2417    async fn on_framedetached_event(&self, frame: crate::protocol::Frame) {
2418        let handlers = self.framedetached_handlers.lock().unwrap().clone();
2419        for handler in handlers {
2420            if let Err(e) = handler(frame.clone()).await {
2421                tracing::warn!("FrameDetached handler error: {}", e);
2422            }
2423        }
2424    }
2425
2426    async fn on_framenavigated_event(&self, frame: crate::protocol::Frame) {
2427        let handlers = self.framenavigated_handlers.lock().unwrap().clone();
2428        for handler in handlers {
2429            if let Err(e) = handler(frame.clone()).await {
2430                tracing::warn!("FrameNavigated handler error: {}", e);
2431            }
2432        }
2433    }
2434
2435    /// Adds a `<style>` tag into the page with the desired content.
2436    ///
2437    /// # Arguments
2438    ///
2439    /// * `options` - Style tag options (content, url, or path)
2440    ///
2441    /// # Returns
2442    ///
2443    /// Returns an ElementHandle pointing to the injected `<style>` tag
2444    ///
2445    /// # Example
2446    ///
2447    /// ```no_run
2448    /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
2449    /// # #[tokio::main]
2450    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2451    /// # let playwright = Playwright::launch().await?;
2452    /// # let browser = playwright.chromium().launch().await?;
2453    /// # let context = browser.new_context().await?;
2454    /// # let page = context.new_page().await?;
2455    /// use playwright_rs::protocol::AddStyleTagOptions;
2456    ///
2457    /// // With inline CSS
2458    /// page.add_style_tag(
2459    ///     AddStyleTagOptions::builder()
2460    ///         .content("body { background-color: red; }")
2461    ///         .build()
2462    /// ).await?;
2463    ///
2464    /// // With external URL
2465    /// page.add_style_tag(
2466    ///     AddStyleTagOptions::builder()
2467    ///         .url("https://example.com/style.css")
2468    ///         .build()
2469    /// ).await?;
2470    ///
2471    /// // From file
2472    /// page.add_style_tag(
2473    ///     AddStyleTagOptions::builder()
2474    ///         .path("./styles/custom.css")
2475    ///         .build()
2476    /// ).await?;
2477    /// # Ok(())
2478    /// # }
2479    /// ```
2480    ///
2481    /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
2482    pub async fn add_style_tag(
2483        &self,
2484        options: AddStyleTagOptions,
2485    ) -> Result<Arc<crate::protocol::ElementHandle>> {
2486        let frame = self.main_frame().await?;
2487        frame.add_style_tag(options).await
2488    }
2489
2490    /// Adds a script which would be evaluated in one of the following scenarios:
2491    /// - Whenever the page is navigated
2492    /// - Whenever a child frame is attached or navigated
2493    ///
2494    /// The script is evaluated after the document was created but before any of its scripts were run.
2495    ///
2496    /// # Arguments
2497    ///
2498    /// * `script` - JavaScript code to be injected into the page
2499    ///
2500    /// # Example
2501    ///
2502    /// ```no_run
2503    /// # use playwright_rs::protocol::Playwright;
2504    /// # #[tokio::main]
2505    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2506    /// # let playwright = Playwright::launch().await?;
2507    /// # let browser = playwright.chromium().launch().await?;
2508    /// # let context = browser.new_context().await?;
2509    /// # let page = context.new_page().await?;
2510    /// page.add_init_script("window.injected = 123;").await?;
2511    /// # Ok(())
2512    /// # }
2513    /// ```
2514    ///
2515    /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
2516    pub async fn add_init_script(&self, script: &str) -> Result<()> {
2517        self.channel()
2518            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
2519            .await
2520    }
2521
2522    /// Sets the viewport size for the page.
2523    ///
2524    /// This method allows dynamic resizing of the viewport after page creation,
2525    /// useful for testing responsive layouts at different screen sizes.
2526    ///
2527    /// # Arguments
2528    ///
2529    /// * `viewport` - The viewport dimensions (width and height in pixels)
2530    ///
2531    /// # Example
2532    ///
2533    /// ```no_run
2534    /// # use playwright_rs::protocol::{Playwright, Viewport};
2535    /// # #[tokio::main]
2536    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2537    /// # let playwright = Playwright::launch().await?;
2538    /// # let browser = playwright.chromium().launch().await?;
2539    /// # let page = browser.new_page().await?;
2540    /// // Set viewport to mobile size
2541    /// let mobile = Viewport {
2542    ///     width: 375,
2543    ///     height: 667,
2544    /// };
2545    /// page.set_viewport_size(mobile).await?;
2546    ///
2547    /// // Later, test desktop layout
2548    /// let desktop = Viewport {
2549    ///     width: 1920,
2550    ///     height: 1080,
2551    /// };
2552    /// page.set_viewport_size(desktop).await?;
2553    /// # Ok(())
2554    /// # }
2555    /// ```
2556    ///
2557    /// # Errors
2558    ///
2559    /// Returns error if:
2560    /// - Page has been closed
2561    /// - Communication with browser process fails
2562    ///
2563    /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
2564    pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
2565        // Store the new viewport locally so viewport_size() can reflect the change
2566        if let Ok(mut guard) = self.viewport.write() {
2567            *guard = Some(viewport.clone());
2568        }
2569        self.channel()
2570            .send_no_result(
2571                "setViewportSize",
2572                serde_json::json!({ "viewportSize": viewport }),
2573            )
2574            .await
2575    }
2576
2577    /// Brings this page to the front (activates the tab).
2578    ///
2579    /// Activates the page in the browser, making it the focused tab. This is
2580    /// useful in multi-page tests to ensure actions target the correct page.
2581    ///
2582    /// # Errors
2583    ///
2584    /// Returns error if:
2585    /// - Page has been closed
2586    /// - Communication with browser process fails
2587    ///
2588    /// See: <https://playwright.dev/docs/api/class-page#page-bring-to-front>
2589    pub async fn bring_to_front(&self) -> Result<()> {
2590        self.channel()
2591            .send_no_result("bringToFront", serde_json::json!({}))
2592            .await
2593    }
2594
2595    /// Sets extra HTTP headers that will be sent with every request from this page.
2596    ///
2597    /// These headers are sent in addition to headers set on the browser context via
2598    /// `BrowserContext::set_extra_http_headers()`. Page-level headers take precedence
2599    /// over context-level headers when names conflict.
2600    ///
2601    /// # Arguments
2602    ///
2603    /// * `headers` - Map of header names to values.
2604    ///
2605    /// # Errors
2606    ///
2607    /// Returns error if:
2608    /// - Page has been closed
2609    /// - Communication with browser process fails
2610    ///
2611    /// See: <https://playwright.dev/docs/api/class-page#page-set-extra-http-headers>
2612    pub async fn set_extra_http_headers(
2613        &self,
2614        headers: std::collections::HashMap<String, String>,
2615    ) -> Result<()> {
2616        // Playwright protocol expects an array of {name, value} objects
2617        // This RPC is sent on the Page channel (not the Frame channel)
2618        let headers_array: Vec<serde_json::Value> = headers
2619            .into_iter()
2620            .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
2621            .collect();
2622        self.channel()
2623            .send_no_result(
2624                "setExtraHTTPHeaders",
2625                serde_json::json!({ "headers": headers_array }),
2626            )
2627            .await
2628    }
2629
2630    /// Emulates media features for the page.
2631    ///
2632    /// This method allows emulating CSS media features such as `media`, `color-scheme`,
2633    /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
2634    ///
2635    /// To reset a specific feature to the browser default, use the `NoOverride` variant.
2636    ///
2637    /// # Arguments
2638    ///
2639    /// * `options` - Optional emulation options. If `None`, this is a no-op.
2640    ///
2641    /// # Example
2642    ///
2643    /// ```no_run
2644    /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
2645    /// # #[tokio::main]
2646    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2647    /// # let playwright = Playwright::launch().await?;
2648    /// # let browser = playwright.chromium().launch().await?;
2649    /// # let page = browser.new_page().await?;
2650    /// // Emulate print media
2651    /// page.emulate_media(Some(
2652    ///     EmulateMediaOptions::builder()
2653    ///         .media(Media::Print)
2654    ///         .build()
2655    /// )).await?;
2656    ///
2657    /// // Emulate dark color scheme
2658    /// page.emulate_media(Some(
2659    ///     EmulateMediaOptions::builder()
2660    ///         .color_scheme(ColorScheme::Dark)
2661    ///         .build()
2662    /// )).await?;
2663    /// # Ok(())
2664    /// # }
2665    /// ```
2666    ///
2667    /// # Errors
2668    ///
2669    /// Returns error if:
2670    /// - Page has been closed
2671    /// - Communication with browser process fails
2672    ///
2673    /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2674    pub async fn emulate_media(&self, options: Option<EmulateMediaOptions>) -> Result<()> {
2675        let mut params = serde_json::json!({});
2676
2677        if let Some(opts) = options {
2678            if let Some(media) = opts.media {
2679                params["media"] = serde_json::to_value(media).map_err(|e| {
2680                    crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
2681                })?;
2682            }
2683            if let Some(color_scheme) = opts.color_scheme {
2684                params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
2685                    crate::error::Error::ProtocolError(format!(
2686                        "Failed to serialize colorScheme: {}",
2687                        e
2688                    ))
2689                })?;
2690            }
2691            if let Some(reduced_motion) = opts.reduced_motion {
2692                params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
2693                    crate::error::Error::ProtocolError(format!(
2694                        "Failed to serialize reducedMotion: {}",
2695                        e
2696                    ))
2697                })?;
2698            }
2699            if let Some(forced_colors) = opts.forced_colors {
2700                params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
2701                    crate::error::Error::ProtocolError(format!(
2702                        "Failed to serialize forcedColors: {}",
2703                        e
2704                    ))
2705                })?;
2706            }
2707        }
2708
2709        self.channel().send_no_result("emulateMedia", params).await
2710    }
2711
2712    /// Generates a PDF of the page and returns it as bytes.
2713    ///
2714    /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
2715    /// not supported in Firefox or WebKit.
2716    ///
2717    /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
2718    /// saved to that file.
2719    ///
2720    /// # Arguments
2721    ///
2722    /// * `options` - Optional PDF generation options
2723    ///
2724    /// # Example
2725    ///
2726    /// ```no_run
2727    /// # use playwright_rs::protocol::Playwright;
2728    /// # #[tokio::main]
2729    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2730    /// # let playwright = Playwright::launch().await?;
2731    /// # let browser = playwright.chromium().launch().await?;
2732    /// # let page = browser.new_page().await?;
2733    /// let pdf_bytes = page.pdf(None).await?;
2734    /// assert!(!pdf_bytes.is_empty());
2735    /// # Ok(())
2736    /// # }
2737    /// ```
2738    ///
2739    /// # Errors
2740    ///
2741    /// Returns error if:
2742    /// - The browser is not Chromium (PDF only supported in Chromium)
2743    /// - Page has been closed
2744    /// - Communication with browser process fails
2745    ///
2746    /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2747    pub async fn pdf(&self, options: Option<PdfOptions>) -> Result<Vec<u8>> {
2748        let mut params = serde_json::json!({});
2749        let mut save_path: Option<std::path::PathBuf> = None;
2750
2751        if let Some(opts) = options {
2752            // Capture the file path before consuming opts
2753            save_path = opts.path;
2754
2755            if let Some(scale) = opts.scale {
2756                params["scale"] = serde_json::json!(scale);
2757            }
2758            if let Some(v) = opts.display_header_footer {
2759                params["displayHeaderFooter"] = serde_json::json!(v);
2760            }
2761            if let Some(v) = opts.header_template {
2762                params["headerTemplate"] = serde_json::json!(v);
2763            }
2764            if let Some(v) = opts.footer_template {
2765                params["footerTemplate"] = serde_json::json!(v);
2766            }
2767            if let Some(v) = opts.print_background {
2768                params["printBackground"] = serde_json::json!(v);
2769            }
2770            if let Some(v) = opts.landscape {
2771                params["landscape"] = serde_json::json!(v);
2772            }
2773            if let Some(v) = opts.page_ranges {
2774                params["pageRanges"] = serde_json::json!(v);
2775            }
2776            if let Some(v) = opts.format {
2777                params["format"] = serde_json::json!(v);
2778            }
2779            if let Some(v) = opts.width {
2780                params["width"] = serde_json::json!(v);
2781            }
2782            if let Some(v) = opts.height {
2783                params["height"] = serde_json::json!(v);
2784            }
2785            if let Some(v) = opts.prefer_css_page_size {
2786                params["preferCSSPageSize"] = serde_json::json!(v);
2787            }
2788            if let Some(margin) = opts.margin {
2789                params["margin"] = serde_json::to_value(margin).map_err(|e| {
2790                    crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
2791                })?;
2792            }
2793        }
2794
2795        #[derive(Deserialize)]
2796        struct PdfResponse {
2797            pdf: String,
2798        }
2799
2800        let response: PdfResponse = self.channel().send("pdf", params).await?;
2801
2802        // Decode base64 to bytes
2803        let pdf_bytes = base64::engine::general_purpose::STANDARD
2804            .decode(&response.pdf)
2805            .map_err(|e| {
2806                crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
2807            })?;
2808
2809        // If a path was specified, save the PDF to disk as well
2810        if let Some(path) = save_path {
2811            tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
2812                crate::error::Error::InvalidArgument(format!(
2813                    "Failed to write PDF to '{}': {}",
2814                    path.display(),
2815                    e
2816                ))
2817            })?;
2818        }
2819
2820        Ok(pdf_bytes)
2821    }
2822
2823    /// Adds a `<script>` tag into the page with the desired URL or content.
2824    ///
2825    /// # Arguments
2826    ///
2827    /// * `options` - Optional script tag options (content, url, or path).
2828    ///   If `None`, returns an error because no source is specified.
2829    ///
2830    /// At least one of `content`, `url`, or `path` must be provided.
2831    ///
2832    /// # Example
2833    ///
2834    /// ```no_run
2835    /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
2836    /// # #[tokio::main]
2837    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2838    /// # let playwright = Playwright::launch().await?;
2839    /// # let browser = playwright.chromium().launch().await?;
2840    /// # let context = browser.new_context().await?;
2841    /// # let page = context.new_page().await?;
2842    /// // With inline JavaScript
2843    /// page.add_script_tag(Some(
2844    ///     AddScriptTagOptions::builder()
2845    ///         .content("window.myVar = 42;")
2846    ///         .build()
2847    /// )).await?;
2848    ///
2849    /// // With external URL
2850    /// page.add_script_tag(Some(
2851    ///     AddScriptTagOptions::builder()
2852    ///         .url("https://example.com/script.js")
2853    ///         .build()
2854    /// )).await?;
2855    /// # Ok(())
2856    /// # }
2857    /// ```
2858    ///
2859    /// # Errors
2860    ///
2861    /// Returns error if:
2862    /// - `options` is `None` or no content/url/path is specified
2863    /// - Page has been closed
2864    /// - Script loading fails (e.g., invalid URL)
2865    ///
2866    /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
2867    pub async fn add_script_tag(
2868        &self,
2869        options: Option<AddScriptTagOptions>,
2870    ) -> Result<Arc<crate::protocol::ElementHandle>> {
2871        let opts = options.ok_or_else(|| {
2872            Error::InvalidArgument(
2873                "At least one of content, url, or path must be specified".to_string(),
2874            )
2875        })?;
2876        let frame = self.main_frame().await?;
2877        frame.add_script_tag(opts).await
2878    }
2879
2880    /// Returns the current viewport size of the page, or `None` if no viewport is set.
2881    ///
2882    /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
2883    /// returns the dimensions configured at context creation time or updated via
2884    /// `set_viewport_size()`.
2885    ///
2886    /// # Example
2887    ///
2888    /// ```ignore
2889    /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
2890    /// # #[tokio::main]
2891    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2892    /// # let playwright = Playwright::launch().await?;
2893    /// # let browser = playwright.chromium().launch().await?;
2894    /// let context = browser.new_context_with_options(
2895    ///     BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
2896    /// ).await?;
2897    /// let page = context.new_page().await?;
2898    /// let size = page.viewport_size().expect("Viewport should be set");
2899    /// assert_eq!(size.width, 1280);
2900    /// assert_eq!(size.height, 720);
2901    /// # Ok(())
2902    /// # }
2903    /// ```
2904    ///
2905    /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
2906    pub fn viewport_size(&self) -> Option<Viewport> {
2907        self.viewport.read().ok()?.clone()
2908    }
2909}
2910
2911impl ChannelOwner for Page {
2912    fn guid(&self) -> &str {
2913        self.base.guid()
2914    }
2915
2916    fn type_name(&self) -> &str {
2917        self.base.type_name()
2918    }
2919
2920    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2921        self.base.parent()
2922    }
2923
2924    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2925        self.base.connection()
2926    }
2927
2928    fn initializer(&self) -> &Value {
2929        self.base.initializer()
2930    }
2931
2932    fn channel(&self) -> &Channel {
2933        self.base.channel()
2934    }
2935
2936    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2937        self.base.dispose(reason)
2938    }
2939
2940    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2941        self.base.adopt(child)
2942    }
2943
2944    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2945        self.base.add_child(guid, child)
2946    }
2947
2948    fn remove_child(&self, guid: &str) {
2949        self.base.remove_child(guid)
2950    }
2951
2952    fn on_event(&self, method: &str, params: Value) {
2953        match method {
2954            "navigated" => {
2955                // Update URL when page navigates
2956                if let Some(url_value) = params.get("url")
2957                    && let Some(url_str) = url_value.as_str()
2958                    && let Ok(mut url) = self.url.write()
2959                {
2960                    *url = url_str.to_string();
2961                }
2962            }
2963            "route" => {
2964                // Handle network routing event
2965                if let Some(route_guid) = params
2966                    .get("route")
2967                    .and_then(|v| v.get("guid"))
2968                    .and_then(|v| v.as_str())
2969                {
2970                    // Get the Route object from connection's registry
2971                    let connection = self.connection();
2972                    let route_guid_owned = route_guid.to_string();
2973                    let self_clone = self.clone();
2974
2975                    tokio::spawn(async move {
2976                        // Get and downcast Route object
2977                        let route: Route =
2978                            match connection.get_typed::<Route>(&route_guid_owned).await {
2979                                Ok(r) => r,
2980                                Err(e) => {
2981                                    tracing::warn!("Failed to get route object: {}", e);
2982                                    return;
2983                                }
2984                            };
2985
2986                        // Set APIRequestContext on the route for fetch() support.
2987                        // Page's parent is BrowserContext, which has the request context.
2988                        if let Some(ctx) =
2989                            downcast_parent::<crate::protocol::BrowserContext>(&self_clone)
2990                            && let Ok(api_ctx) = ctx.request().await
2991                        {
2992                            route.set_api_request_context(api_ctx);
2993                        }
2994
2995                        // Call the route handler and wait for completion
2996                        self_clone.on_route_event(route).await;
2997                    });
2998                }
2999            }
3000            "download" => {
3001                // Handle download event
3002                // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
3003                let url = params
3004                    .get("url")
3005                    .and_then(|v| v.as_str())
3006                    .unwrap_or("")
3007                    .to_string();
3008
3009                let suggested_filename = params
3010                    .get("suggestedFilename")
3011                    .and_then(|v| v.as_str())
3012                    .unwrap_or("")
3013                    .to_string();
3014
3015                if let Some(artifact_guid) = params
3016                    .get("artifact")
3017                    .and_then(|v| v.get("guid"))
3018                    .and_then(|v| v.as_str())
3019                {
3020                    let connection = self.connection();
3021                    let artifact_guid_owned = artifact_guid.to_string();
3022                    let self_clone = self.clone();
3023
3024                    tokio::spawn(async move {
3025                        // Wait for Artifact object to be created
3026                        let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
3027                            Ok(obj) => obj,
3028                            Err(e) => {
3029                                tracing::warn!("Failed to get artifact object: {}", e);
3030                                return;
3031                            }
3032                        };
3033
3034                        // Create Download wrapper from Artifact + event params
3035                        let download = Download::from_artifact(
3036                            artifact_arc,
3037                            url,
3038                            suggested_filename,
3039                            self_clone.clone(),
3040                        );
3041
3042                        // Call the download handlers
3043                        self_clone.on_download_event(download).await;
3044                    });
3045                }
3046            }
3047            "dialog" => {
3048                // Dialog events are handled by BrowserContext and forwarded to Page
3049                // This case should not be reached, but keeping for completeness
3050            }
3051            "webSocket" => {
3052                if let Some(ws_guid) = params
3053                    .get("webSocket")
3054                    .and_then(|v| v.get("guid"))
3055                    .and_then(|v| v.as_str())
3056                {
3057                    let connection = self.connection();
3058                    let ws_guid_owned = ws_guid.to_string();
3059                    let self_clone = self.clone();
3060
3061                    tokio::spawn(async move {
3062                        // Get and downcast WebSocket object
3063                        let ws: WebSocket =
3064                            match connection.get_typed::<WebSocket>(&ws_guid_owned).await {
3065                                Ok(ws) => ws,
3066                                Err(e) => {
3067                                    tracing::warn!("Failed to get WebSocket object: {}", e);
3068                                    return;
3069                                }
3070                            };
3071
3072                        // Call handlers
3073                        let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
3074                        for handler in handlers {
3075                            let ws_clone = ws.clone();
3076                            tokio::spawn(async move {
3077                                if let Err(e) = handler(ws_clone).await {
3078                                    tracing::error!("Error in websocket handler: {}", e);
3079                                }
3080                            });
3081                        }
3082                    });
3083                }
3084            }
3085            "worker" => {
3086                // A new Web Worker was created in the page.
3087                // Event format: {worker: {guid: "Worker@..."}}
3088                if let Some(worker_guid) = params
3089                    .get("worker")
3090                    .and_then(|v| v.get("guid"))
3091                    .and_then(|v| v.as_str())
3092                {
3093                    let connection = self.connection();
3094                    let worker_guid_owned = worker_guid.to_string();
3095                    let self_clone = self.clone();
3096
3097                    tokio::spawn(async move {
3098                        let worker: Worker =
3099                            match connection.get_typed::<Worker>(&worker_guid_owned).await {
3100                                Ok(w) => w,
3101                                Err(e) => {
3102                                    tracing::warn!("Failed to get Worker object: {}", e);
3103                                    return;
3104                                }
3105                            };
3106
3107                        let handlers = self_clone.worker_handlers.lock().unwrap().clone();
3108                        for handler in handlers {
3109                            let worker_clone = worker.clone();
3110                            tokio::spawn(async move {
3111                                if let Err(e) = handler(worker_clone).await {
3112                                    tracing::error!("Error in worker handler: {}", e);
3113                                }
3114                            });
3115                        }
3116                    });
3117                }
3118            }
3119            "bindingCall" => {
3120                // A JS caller on this page invoked a page-level exposed function.
3121                // Event format: {binding: {guid: "..."}}
3122                if let Some(binding_guid) = params
3123                    .get("binding")
3124                    .and_then(|v| v.get("guid"))
3125                    .and_then(|v| v.as_str())
3126                {
3127                    let connection = self.connection();
3128                    let binding_guid_owned = binding_guid.to_string();
3129                    let binding_callbacks = self.binding_callbacks.clone();
3130
3131                    tokio::spawn(async move {
3132                        let binding_call: crate::protocol::BindingCall = match connection
3133                            .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
3134                            .await
3135                        {
3136                            Ok(bc) => bc,
3137                            Err(e) => {
3138                                tracing::warn!("Failed to get BindingCall object: {}", e);
3139                                return;
3140                            }
3141                        };
3142
3143                        let name = binding_call.name().to_string();
3144
3145                        // Look up page-level callback
3146                        let callback = {
3147                            let callbacks = binding_callbacks.lock().unwrap();
3148                            callbacks.get(&name).cloned()
3149                        };
3150
3151                        let Some(callback) = callback else {
3152                            // No page-level handler — the context-level handler on
3153                            // BrowserContext::on_event("bindingCall") will handle it.
3154                            return;
3155                        };
3156
3157                        // Deserialize args from Playwright protocol format
3158                        let raw_args = binding_call.args();
3159                        let args = crate::protocol::browser_context::BrowserContext::deserialize_binding_args_pub(raw_args);
3160
3161                        // Call callback and serialize result
3162                        let result_value = callback(args).await;
3163                        let serialized =
3164                            crate::protocol::evaluate_conversion::serialize_argument(&result_value);
3165
3166                        if let Err(e) = binding_call.resolve(serialized).await {
3167                            tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
3168                        }
3169                    });
3170                }
3171            }
3172            "fileChooser" => {
3173                // FileChooser event: sent when an <input type="file"> is interacted with.
3174                // Event params: {element: {guid: "..."}, isMultiple: bool}
3175                let is_multiple = params
3176                    .get("isMultiple")
3177                    .and_then(|v| v.as_bool())
3178                    .unwrap_or(false);
3179
3180                if let Some(element_guid) = params
3181                    .get("element")
3182                    .and_then(|v| v.get("guid"))
3183                    .and_then(|v| v.as_str())
3184                {
3185                    let connection = self.connection();
3186                    let element_guid_owned = element_guid.to_string();
3187                    let self_clone = self.clone();
3188
3189                    tokio::spawn(async move {
3190                        let element: crate::protocol::ElementHandle = match connection
3191                            .get_typed::<crate::protocol::ElementHandle>(&element_guid_owned)
3192                            .await
3193                        {
3194                            Ok(e) => e,
3195                            Err(err) => {
3196                                tracing::warn!(
3197                                    "Failed to get ElementHandle for fileChooser: {}",
3198                                    err
3199                                );
3200                                return;
3201                            }
3202                        };
3203
3204                        let chooser = crate::protocol::FileChooser::new(
3205                            self_clone.clone(),
3206                            std::sync::Arc::new(element),
3207                            is_multiple,
3208                        );
3209
3210                        self_clone.on_filechooser_event(chooser).await;
3211                    });
3212                }
3213            }
3214            "close" => {
3215                // Server-initiated close (e.g. context was closed)
3216                self.is_closed.store(true, Ordering::Relaxed);
3217                // Dispatch close handlers
3218                let self_clone = self.clone();
3219                tokio::spawn(async move {
3220                    self_clone.on_close_event().await;
3221                });
3222            }
3223            "load" => {
3224                let self_clone = self.clone();
3225                tokio::spawn(async move {
3226                    self_clone.on_load_event().await;
3227                });
3228            }
3229            "crash" => {
3230                let self_clone = self.clone();
3231                tokio::spawn(async move {
3232                    self_clone.on_crash_event().await;
3233                });
3234            }
3235            "pageError" => {
3236                // params: {"error": {"message": "...", "stack": "..."}}
3237                let message = params
3238                    .get("error")
3239                    .and_then(|e| e.get("message"))
3240                    .and_then(|m| m.as_str())
3241                    .unwrap_or("")
3242                    .to_string();
3243                let self_clone = self.clone();
3244                tokio::spawn(async move {
3245                    self_clone.on_pageerror_event(message).await;
3246                });
3247            }
3248            // "popup" is forwarded from BrowserContext::on_event when a "page" event
3249            // is received for a page that has an opener. No direct "popup" event on Page.
3250            "frameAttached" => {
3251                // params: {"frame": {"guid": "..."}}
3252                if let Some(frame_guid) = params
3253                    .get("frame")
3254                    .and_then(|v| v.get("guid"))
3255                    .and_then(|v| v.as_str())
3256                {
3257                    let connection = self.connection();
3258                    let frame_guid_owned = frame_guid.to_string();
3259                    let self_clone = self.clone();
3260
3261                    tokio::spawn(async move {
3262                        let frame: crate::protocol::Frame = match connection
3263                            .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
3264                            .await
3265                        {
3266                            Ok(f) => f,
3267                            Err(e) => {
3268                                tracing::warn!("Failed to get Frame for frameAttached: {}", e);
3269                                return;
3270                            }
3271                        };
3272                        self_clone.on_frameattached_event(frame).await;
3273                    });
3274                }
3275            }
3276            "frameDetached" => {
3277                // params: {"frame": {"guid": "..."}}
3278                if let Some(frame_guid) = params
3279                    .get("frame")
3280                    .and_then(|v| v.get("guid"))
3281                    .and_then(|v| v.as_str())
3282                {
3283                    let connection = self.connection();
3284                    let frame_guid_owned = frame_guid.to_string();
3285                    let self_clone = self.clone();
3286
3287                    tokio::spawn(async move {
3288                        let frame: crate::protocol::Frame = match connection
3289                            .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
3290                            .await
3291                        {
3292                            Ok(f) => f,
3293                            Err(e) => {
3294                                tracing::warn!("Failed to get Frame for frameDetached: {}", e);
3295                                return;
3296                            }
3297                        };
3298                        self_clone.on_framedetached_event(frame).await;
3299                    });
3300                }
3301            }
3302            "frameNavigated" => {
3303                // params: {"frame": {"guid": "..."}}
3304                // Note: frameNavigated may also contain url, name, etc. at top level
3305                // The frame guid is in the "frame" field (same as attached/detached)
3306                if let Some(frame_guid) = params
3307                    .get("frame")
3308                    .and_then(|v| v.get("guid"))
3309                    .and_then(|v| v.as_str())
3310                {
3311                    let connection = self.connection();
3312                    let frame_guid_owned = frame_guid.to_string();
3313                    let self_clone = self.clone();
3314
3315                    tokio::spawn(async move {
3316                        let frame: crate::protocol::Frame = match connection
3317                            .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
3318                            .await
3319                        {
3320                            Ok(f) => f,
3321                            Err(e) => {
3322                                tracing::warn!("Failed to get Frame for frameNavigated: {}", e);
3323                                return;
3324                            }
3325                        };
3326                        self_clone.on_framenavigated_event(frame).await;
3327                    });
3328                }
3329            }
3330            _ => {
3331                // Other events not yet handled
3332            }
3333        }
3334    }
3335
3336    fn was_collected(&self) -> bool {
3337        self.base.was_collected()
3338    }
3339
3340    fn as_any(&self) -> &dyn Any {
3341        self
3342    }
3343}
3344
3345impl std::fmt::Debug for Page {
3346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3347        f.debug_struct("Page")
3348            .field("guid", &self.guid())
3349            .field("url", &self.url())
3350            .finish()
3351    }
3352}
3353
3354/// Options for page.goto() and page.reload()
3355#[derive(Debug, Clone)]
3356pub struct GotoOptions {
3357    /// Maximum operation time in milliseconds
3358    pub timeout: Option<std::time::Duration>,
3359    /// When to consider operation succeeded
3360    pub wait_until: Option<WaitUntil>,
3361}
3362
3363impl GotoOptions {
3364    /// Creates new GotoOptions with default values
3365    pub fn new() -> Self {
3366        Self {
3367            timeout: None,
3368            wait_until: None,
3369        }
3370    }
3371
3372    /// Sets the timeout
3373    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
3374        self.timeout = Some(timeout);
3375        self
3376    }
3377
3378    /// Sets the wait_until option
3379    pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
3380        self.wait_until = Some(wait_until);
3381        self
3382    }
3383}
3384
3385impl Default for GotoOptions {
3386    fn default() -> Self {
3387        Self::new()
3388    }
3389}
3390
3391/// When to consider navigation succeeded
3392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3393pub enum WaitUntil {
3394    /// Consider operation to be finished when the `load` event is fired
3395    Load,
3396    /// Consider operation to be finished when the `DOMContentLoaded` event is fired
3397    DomContentLoaded,
3398    /// Consider operation to be finished when there are no network connections for at least 500ms
3399    NetworkIdle,
3400    /// Consider operation to be finished when the commit event is fired
3401    Commit,
3402}
3403
3404impl WaitUntil {
3405    pub(crate) fn as_str(&self) -> &'static str {
3406        match self {
3407            WaitUntil::Load => "load",
3408            WaitUntil::DomContentLoaded => "domcontentloaded",
3409            WaitUntil::NetworkIdle => "networkidle",
3410            WaitUntil::Commit => "commit",
3411        }
3412    }
3413}
3414
3415/// Options for adding a style tag to the page
3416///
3417/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
3418#[derive(Debug, Clone, Default)]
3419pub struct AddStyleTagOptions {
3420    /// Raw CSS content to inject
3421    pub content: Option<String>,
3422    /// URL of the `<link>` tag to add
3423    pub url: Option<String>,
3424    /// Path to a CSS file to inject
3425    pub path: Option<String>,
3426}
3427
3428impl AddStyleTagOptions {
3429    /// Creates a new builder for AddStyleTagOptions
3430    pub fn builder() -> AddStyleTagOptionsBuilder {
3431        AddStyleTagOptionsBuilder::default()
3432    }
3433
3434    /// Validates that at least one option is specified
3435    pub(crate) fn validate(&self) -> Result<()> {
3436        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
3437            return Err(Error::InvalidArgument(
3438                "At least one of content, url, or path must be specified".to_string(),
3439            ));
3440        }
3441        Ok(())
3442    }
3443}
3444
3445/// Builder for AddStyleTagOptions
3446#[derive(Debug, Clone, Default)]
3447pub struct AddStyleTagOptionsBuilder {
3448    content: Option<String>,
3449    url: Option<String>,
3450    path: Option<String>,
3451}
3452
3453impl AddStyleTagOptionsBuilder {
3454    /// Sets the CSS content to inject
3455    pub fn content(mut self, content: impl Into<String>) -> Self {
3456        self.content = Some(content.into());
3457        self
3458    }
3459
3460    /// Sets the URL of the stylesheet
3461    pub fn url(mut self, url: impl Into<String>) -> Self {
3462        self.url = Some(url.into());
3463        self
3464    }
3465
3466    /// Sets the path to a CSS file
3467    pub fn path(mut self, path: impl Into<String>) -> Self {
3468        self.path = Some(path.into());
3469        self
3470    }
3471
3472    /// Builds the AddStyleTagOptions
3473    pub fn build(self) -> AddStyleTagOptions {
3474        AddStyleTagOptions {
3475            content: self.content,
3476            url: self.url,
3477            path: self.path,
3478        }
3479    }
3480}
3481
3482// ============================================================================
3483// AddScriptTagOptions
3484// ============================================================================
3485
3486/// Options for adding a `<script>` tag to the page.
3487///
3488/// At least one of `content`, `url`, or `path` must be specified.
3489///
3490/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
3491#[derive(Debug, Clone, Default)]
3492pub struct AddScriptTagOptions {
3493    /// Raw JavaScript content to inject
3494    pub content: Option<String>,
3495    /// URL of the `<script>` tag to add
3496    pub url: Option<String>,
3497    /// Path to a JavaScript file to inject (file contents will be read and sent as content)
3498    pub path: Option<String>,
3499    /// Script type attribute (e.g., `"module"`)
3500    pub type_: Option<String>,
3501}
3502
3503impl AddScriptTagOptions {
3504    /// Creates a new builder for AddScriptTagOptions
3505    pub fn builder() -> AddScriptTagOptionsBuilder {
3506        AddScriptTagOptionsBuilder::default()
3507    }
3508
3509    /// Validates that at least one option is specified
3510    pub(crate) fn validate(&self) -> Result<()> {
3511        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
3512            return Err(Error::InvalidArgument(
3513                "At least one of content, url, or path must be specified".to_string(),
3514            ));
3515        }
3516        Ok(())
3517    }
3518}
3519
3520/// Builder for AddScriptTagOptions
3521#[derive(Debug, Clone, Default)]
3522pub struct AddScriptTagOptionsBuilder {
3523    content: Option<String>,
3524    url: Option<String>,
3525    path: Option<String>,
3526    type_: Option<String>,
3527}
3528
3529impl AddScriptTagOptionsBuilder {
3530    /// Sets the JavaScript content to inject
3531    pub fn content(mut self, content: impl Into<String>) -> Self {
3532        self.content = Some(content.into());
3533        self
3534    }
3535
3536    /// Sets the URL of the script to load
3537    pub fn url(mut self, url: impl Into<String>) -> Self {
3538        self.url = Some(url.into());
3539        self
3540    }
3541
3542    /// Sets the path to a JavaScript file to inject
3543    pub fn path(mut self, path: impl Into<String>) -> Self {
3544        self.path = Some(path.into());
3545        self
3546    }
3547
3548    /// Sets the script type attribute (e.g., `"module"`)
3549    pub fn type_(mut self, type_: impl Into<String>) -> Self {
3550        self.type_ = Some(type_.into());
3551        self
3552    }
3553
3554    /// Builds the AddScriptTagOptions
3555    pub fn build(self) -> AddScriptTagOptions {
3556        AddScriptTagOptions {
3557            content: self.content,
3558            url: self.url,
3559            path: self.path,
3560            type_: self.type_,
3561        }
3562    }
3563}
3564
3565// ============================================================================
3566// EmulateMediaOptions and related enums
3567// ============================================================================
3568
3569/// Media type for `page.emulate_media()`.
3570///
3571/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3572#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3573#[serde(rename_all = "lowercase")]
3574pub enum Media {
3575    /// Emulate screen media type
3576    Screen,
3577    /// Emulate print media type
3578    Print,
3579    /// Reset media emulation to browser default (sends `"no-override"` to protocol)
3580    #[serde(rename = "no-override")]
3581    NoOverride,
3582}
3583
3584/// Preferred color scheme for `page.emulate_media()`.
3585///
3586/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3587#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3588pub enum ColorScheme {
3589    /// Emulate light color scheme
3590    #[serde(rename = "light")]
3591    Light,
3592    /// Emulate dark color scheme
3593    #[serde(rename = "dark")]
3594    Dark,
3595    /// Emulate no preference for color scheme
3596    #[serde(rename = "no-preference")]
3597    NoPreference,
3598    /// Reset color scheme to browser default
3599    #[serde(rename = "no-override")]
3600    NoOverride,
3601}
3602
3603/// Reduced motion preference for `page.emulate_media()`.
3604///
3605/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3607pub enum ReducedMotion {
3608    /// Emulate reduced motion preference
3609    #[serde(rename = "reduce")]
3610    Reduce,
3611    /// Emulate no preference for reduced motion
3612    #[serde(rename = "no-preference")]
3613    NoPreference,
3614    /// Reset reduced motion to browser default
3615    #[serde(rename = "no-override")]
3616    NoOverride,
3617}
3618
3619/// Forced colors preference for `page.emulate_media()`.
3620///
3621/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3622#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3623pub enum ForcedColors {
3624    /// Emulate active forced colors
3625    #[serde(rename = "active")]
3626    Active,
3627    /// Emulate no forced colors
3628    #[serde(rename = "none")]
3629    None_,
3630    /// Reset forced colors to browser default
3631    #[serde(rename = "no-override")]
3632    NoOverride,
3633}
3634
3635/// Options for `page.emulate_media()`.
3636///
3637/// All fields are optional. Fields that are `None` are omitted from the protocol
3638/// message (meaning they are not changed). To reset a field to browser default,
3639/// use the `NoOverride` variant.
3640///
3641/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3642#[derive(Debug, Clone, Default)]
3643pub struct EmulateMediaOptions {
3644    /// Media type to emulate (screen, print, or no-override)
3645    pub media: Option<Media>,
3646    /// Color scheme preference to emulate
3647    pub color_scheme: Option<ColorScheme>,
3648    /// Reduced motion preference to emulate
3649    pub reduced_motion: Option<ReducedMotion>,
3650    /// Forced colors preference to emulate
3651    pub forced_colors: Option<ForcedColors>,
3652}
3653
3654impl EmulateMediaOptions {
3655    /// Creates a new builder for EmulateMediaOptions
3656    pub fn builder() -> EmulateMediaOptionsBuilder {
3657        EmulateMediaOptionsBuilder::default()
3658    }
3659}
3660
3661/// Builder for EmulateMediaOptions
3662#[derive(Debug, Clone, Default)]
3663pub struct EmulateMediaOptionsBuilder {
3664    media: Option<Media>,
3665    color_scheme: Option<ColorScheme>,
3666    reduced_motion: Option<ReducedMotion>,
3667    forced_colors: Option<ForcedColors>,
3668}
3669
3670impl EmulateMediaOptionsBuilder {
3671    /// Sets the media type to emulate
3672    pub fn media(mut self, media: Media) -> Self {
3673        self.media = Some(media);
3674        self
3675    }
3676
3677    /// Sets the color scheme preference
3678    pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
3679        self.color_scheme = Some(color_scheme);
3680        self
3681    }
3682
3683    /// Sets the reduced motion preference
3684    pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
3685        self.reduced_motion = Some(reduced_motion);
3686        self
3687    }
3688
3689    /// Sets the forced colors preference
3690    pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
3691        self.forced_colors = Some(forced_colors);
3692        self
3693    }
3694
3695    /// Builds the EmulateMediaOptions
3696    pub fn build(self) -> EmulateMediaOptions {
3697        EmulateMediaOptions {
3698            media: self.media,
3699            color_scheme: self.color_scheme,
3700            reduced_motion: self.reduced_motion,
3701            forced_colors: self.forced_colors,
3702        }
3703    }
3704}
3705
3706// ============================================================================
3707// PdfOptions
3708// ============================================================================
3709
3710/// Margin options for PDF generation.
3711///
3712/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
3713#[derive(Debug, Clone, Default, Serialize)]
3714pub struct PdfMargin {
3715    /// Top margin (e.g. `"1in"`)
3716    #[serde(skip_serializing_if = "Option::is_none")]
3717    pub top: Option<String>,
3718    /// Right margin
3719    #[serde(skip_serializing_if = "Option::is_none")]
3720    pub right: Option<String>,
3721    /// Bottom margin
3722    #[serde(skip_serializing_if = "Option::is_none")]
3723    pub bottom: Option<String>,
3724    /// Left margin
3725    #[serde(skip_serializing_if = "Option::is_none")]
3726    pub left: Option<String>,
3727}
3728
3729/// Options for generating a PDF from a page.
3730///
3731/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
3732/// Firefox or WebKit will result in an error.
3733///
3734/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
3735#[derive(Debug, Clone, Default)]
3736pub struct PdfOptions {
3737    /// If specified, the PDF will also be saved to this file path.
3738    pub path: Option<std::path::PathBuf>,
3739    /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
3740    pub scale: Option<f64>,
3741    /// Whether to display header and footer (default false).
3742    pub display_header_footer: Option<bool>,
3743    /// HTML template for the print header. Should be valid HTML.
3744    pub header_template: Option<String>,
3745    /// HTML template for the print footer.
3746    pub footer_template: Option<String>,
3747    /// Whether to print background graphics (default false).
3748    pub print_background: Option<bool>,
3749    /// Paper orientation — `true` for landscape (default false).
3750    pub landscape: Option<bool>,
3751    /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
3752    pub page_ranges: Option<String>,
3753    /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
3754    pub format: Option<String>,
3755    /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
3756    pub width: Option<String>,
3757    /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
3758    pub height: Option<String>,
3759    /// Whether or not to prefer page size as defined by CSS.
3760    pub prefer_css_page_size: Option<bool>,
3761    /// Paper margins, defaulting to none.
3762    pub margin: Option<PdfMargin>,
3763}
3764
3765impl PdfOptions {
3766    /// Creates a new builder for PdfOptions
3767    pub fn builder() -> PdfOptionsBuilder {
3768        PdfOptionsBuilder::default()
3769    }
3770}
3771
3772/// Builder for PdfOptions
3773#[derive(Debug, Clone, Default)]
3774pub struct PdfOptionsBuilder {
3775    path: Option<std::path::PathBuf>,
3776    scale: Option<f64>,
3777    display_header_footer: Option<bool>,
3778    header_template: Option<String>,
3779    footer_template: Option<String>,
3780    print_background: Option<bool>,
3781    landscape: Option<bool>,
3782    page_ranges: Option<String>,
3783    format: Option<String>,
3784    width: Option<String>,
3785    height: Option<String>,
3786    prefer_css_page_size: Option<bool>,
3787    margin: Option<PdfMargin>,
3788}
3789
3790impl PdfOptionsBuilder {
3791    /// Sets the file path for saving the PDF
3792    pub fn path(mut self, path: std::path::PathBuf) -> Self {
3793        self.path = Some(path);
3794        self
3795    }
3796
3797    /// Sets the scale of the webpage rendering
3798    pub fn scale(mut self, scale: f64) -> Self {
3799        self.scale = Some(scale);
3800        self
3801    }
3802
3803    /// Sets whether to display header and footer
3804    pub fn display_header_footer(mut self, display: bool) -> Self {
3805        self.display_header_footer = Some(display);
3806        self
3807    }
3808
3809    /// Sets the HTML template for the print header
3810    pub fn header_template(mut self, template: impl Into<String>) -> Self {
3811        self.header_template = Some(template.into());
3812        self
3813    }
3814
3815    /// Sets the HTML template for the print footer
3816    pub fn footer_template(mut self, template: impl Into<String>) -> Self {
3817        self.footer_template = Some(template.into());
3818        self
3819    }
3820
3821    /// Sets whether to print background graphics
3822    pub fn print_background(mut self, print: bool) -> Self {
3823        self.print_background = Some(print);
3824        self
3825    }
3826
3827    /// Sets whether to use landscape orientation
3828    pub fn landscape(mut self, landscape: bool) -> Self {
3829        self.landscape = Some(landscape);
3830        self
3831    }
3832
3833    /// Sets the page ranges to print
3834    pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
3835        self.page_ranges = Some(ranges.into());
3836        self
3837    }
3838
3839    /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
3840    pub fn format(mut self, format: impl Into<String>) -> Self {
3841        self.format = Some(format.into());
3842        self
3843    }
3844
3845    /// Sets the paper width
3846    pub fn width(mut self, width: impl Into<String>) -> Self {
3847        self.width = Some(width.into());
3848        self
3849    }
3850
3851    /// Sets the paper height
3852    pub fn height(mut self, height: impl Into<String>) -> Self {
3853        self.height = Some(height.into());
3854        self
3855    }
3856
3857    /// Sets whether to prefer page size as defined by CSS
3858    pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
3859        self.prefer_css_page_size = Some(prefer);
3860        self
3861    }
3862
3863    /// Sets the paper margins
3864    pub fn margin(mut self, margin: PdfMargin) -> Self {
3865        self.margin = Some(margin);
3866        self
3867    }
3868
3869    /// Builds the PdfOptions
3870    pub fn build(self) -> PdfOptions {
3871        PdfOptions {
3872            path: self.path,
3873            scale: self.scale,
3874            display_header_footer: self.display_header_footer,
3875            header_template: self.header_template,
3876            footer_template: self.footer_template,
3877            print_background: self.print_background,
3878            landscape: self.landscape,
3879            page_ranges: self.page_ranges,
3880            format: self.format,
3881            width: self.width,
3882            height: self.height,
3883            prefer_css_page_size: self.prefer_css_page_size,
3884            margin: self.margin,
3885        }
3886    }
3887}
3888
3889/// Response from navigation operations.
3890///
3891/// Returned from `page.goto()`, `page.reload()`, `page.go_back()`, and similar
3892/// navigation methods. Provides access to the HTTP response status, headers, and body.
3893///
3894/// See: <https://playwright.dev/docs/api/class-response>
3895#[derive(Clone)]
3896pub struct Response {
3897    url: String,
3898    status: u16,
3899    status_text: String,
3900    ok: bool,
3901    headers: std::collections::HashMap<String, String>,
3902    /// Reference to the backing channel owner for RPC calls (body, rawHeaders, etc.)
3903    /// Stored as the generic trait object so it can be downcast to ResponseObject when needed.
3904    response_channel_owner: Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
3905}
3906
3907impl Response {
3908    /// Creates a new Response from protocol data.
3909    ///
3910    /// This is used internally when constructing a Response from the protocol
3911    /// initializer (e.g., after `goto` or `reload`).
3912    pub(crate) fn new(
3913        url: String,
3914        status: u16,
3915        status_text: String,
3916        headers: std::collections::HashMap<String, String>,
3917        response_channel_owner: Option<
3918            std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>,
3919        >,
3920    ) -> Self {
3921        Self {
3922            url,
3923            status,
3924            status_text,
3925            ok: (200..300).contains(&status),
3926            headers,
3927            response_channel_owner,
3928        }
3929    }
3930}
3931
3932impl Response {
3933    /// Returns the URL of the response.
3934    ///
3935    /// See: <https://playwright.dev/docs/api/class-response#response-url>
3936    pub fn url(&self) -> &str {
3937        &self.url
3938    }
3939
3940    /// Returns the HTTP status code.
3941    ///
3942    /// See: <https://playwright.dev/docs/api/class-response#response-status>
3943    pub fn status(&self) -> u16 {
3944        self.status
3945    }
3946
3947    /// Returns the HTTP status text.
3948    ///
3949    /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
3950    pub fn status_text(&self) -> &str {
3951        &self.status_text
3952    }
3953
3954    /// Returns whether the response was successful (status 200-299).
3955    ///
3956    /// See: <https://playwright.dev/docs/api/class-response#response-ok>
3957    pub fn ok(&self) -> bool {
3958        self.ok
3959    }
3960
3961    /// Returns the response headers as a HashMap.
3962    ///
3963    /// Note: these are the headers from the protocol initializer. For the full
3964    /// raw headers (including duplicates), use `headers_array()` or `all_headers()`.
3965    ///
3966    /// See: <https://playwright.dev/docs/api/class-response#response-headers>
3967    pub fn headers(&self) -> &std::collections::HashMap<String, String> {
3968        &self.headers
3969    }
3970
3971    /// Returns the [`Request`] that triggered this response.
3972    ///
3973    /// Navigates the protocol object hierarchy: ResponseObject → parent (Request).
3974    ///
3975    /// See: <https://playwright.dev/docs/api/class-response#response-request>
3976    pub fn request(&self) -> Option<crate::protocol::Request> {
3977        let owner = self.response_channel_owner.as_ref()?;
3978        downcast_parent::<crate::protocol::Request>(&**owner)
3979    }
3980
3981    /// Returns the [`Frame`](crate::protocol::Frame) that initiated the request for this response.
3982    ///
3983    /// Navigates the protocol object hierarchy: ResponseObject → Request → Frame.
3984    ///
3985    /// See: <https://playwright.dev/docs/api/class-response#response-frame>
3986    pub fn frame(&self) -> Option<crate::protocol::Frame> {
3987        let request = self.request()?;
3988        request.frame()
3989    }
3990
3991    /// Returns the backing `ResponseObject`, or an error if unavailable.
3992    pub(crate) fn response_object(&self) -> crate::error::Result<crate::protocol::ResponseObject> {
3993        let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
3994            crate::error::Error::ProtocolError(
3995                "Response has no backing protocol object".to_string(),
3996            )
3997        })?;
3998        arc.as_any()
3999            .downcast_ref::<crate::protocol::ResponseObject>()
4000            .cloned()
4001            .ok_or_else(|| crate::error::Error::TypeMismatch {
4002                guid: arc.guid().to_string(),
4003                expected: "ResponseObject".to_string(),
4004                actual: arc.type_name().to_string(),
4005            })
4006    }
4007
4008    /// Returns TLS/SSL security details for HTTPS connections, or `None` for HTTP.
4009    ///
4010    /// See: <https://playwright.dev/docs/api/class-response#response-security-details>
4011    pub async fn security_details(
4012        &self,
4013    ) -> crate::error::Result<Option<crate::protocol::response::SecurityDetails>> {
4014        self.response_object()?.security_details().await
4015    }
4016
4017    /// Returns the server's IP address and port, or `None`.
4018    ///
4019    /// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
4020    pub async fn server_addr(
4021        &self,
4022    ) -> crate::error::Result<Option<crate::protocol::response::RemoteAddr>> {
4023        self.response_object()?.server_addr().await
4024    }
4025
4026    /// Waits for this response to finish loading.
4027    ///
4028    /// For responses obtained from navigation methods (`goto`, `reload`), the response
4029    /// is already finished when returned. For responses from `on_response` handlers,
4030    /// the body may still be loading.
4031    ///
4032    /// See: <https://playwright.dev/docs/api/class-response#response-finished>
4033    pub async fn finished(&self) -> crate::error::Result<()> {
4034        // The Playwright protocol dispatches `requestFinished` as a separate event
4035        // rather than exposing a `finished` RPC method on Response.
4036        // For responses from goto/reload, the response is already complete.
4037        // TODO: For on_response handlers, implement proper waiting via requestFinished event.
4038        Ok(())
4039    }
4040
4041    /// Returns the response body as raw bytes.
4042    ///
4043    /// Makes an RPC call to the Playwright server to fetch the response body.
4044    ///
4045    /// # Errors
4046    ///
4047    /// Returns an error if:
4048    /// - No backing protocol object is available (edge case)
4049    /// - The RPC call to the server fails
4050    /// - The base64 response cannot be decoded
4051    ///
4052    /// See: <https://playwright.dev/docs/api/class-response#response-body>
4053    pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
4054        self.response_object()?.body().await
4055    }
4056
4057    /// Returns the response body as a UTF-8 string.
4058    ///
4059    /// Calls `body()` then converts bytes to a UTF-8 string.
4060    ///
4061    /// # Errors
4062    ///
4063    /// Returns an error if:
4064    /// - `body()` fails
4065    /// - The body is not valid UTF-8
4066    ///
4067    /// See: <https://playwright.dev/docs/api/class-response#response-text>
4068    pub async fn text(&self) -> crate::error::Result<String> {
4069        let bytes = self.body().await?;
4070        String::from_utf8(bytes).map_err(|e| {
4071            crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
4072        })
4073    }
4074
4075    /// Parses the response body as JSON and deserializes it into type `T`.
4076    ///
4077    /// Calls `text()` then uses `serde_json` to deserialize the body.
4078    ///
4079    /// # Errors
4080    ///
4081    /// Returns an error if:
4082    /// - `text()` fails
4083    /// - The body is not valid JSON or doesn't match the expected type
4084    ///
4085    /// See: <https://playwright.dev/docs/api/class-response#response-json>
4086    pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
4087        let text = self.text().await?;
4088        serde_json::from_str(&text).map_err(|e| {
4089            crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
4090        })
4091    }
4092
4093    /// Returns all response headers as name-value pairs, preserving duplicates.
4094    ///
4095    /// Makes an RPC call for `"rawHeaders"` which returns the complete header list.
4096    ///
4097    /// # Errors
4098    ///
4099    /// Returns an error if:
4100    /// - No backing protocol object is available (edge case)
4101    /// - The RPC call to the server fails
4102    ///
4103    /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
4104    pub async fn headers_array(
4105        &self,
4106    ) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
4107        self.response_object()?.raw_headers().await
4108    }
4109
4110    /// Returns all response headers merged into a HashMap with lowercase keys.
4111    ///
4112    /// When multiple headers have the same name, their values are joined with `, `.
4113    /// This matches the behavior of `response.allHeaders()` in other Playwright bindings.
4114    ///
4115    /// # Errors
4116    ///
4117    /// Returns an error if:
4118    /// - No backing protocol object is available (edge case)
4119    /// - The RPC call to the server fails
4120    ///
4121    /// See: <https://playwright.dev/docs/api/class-response#response-all-headers>
4122    pub async fn all_headers(
4123        &self,
4124    ) -> crate::error::Result<std::collections::HashMap<String, String>> {
4125        let entries = self.headers_array().await?;
4126        let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
4127        for entry in entries {
4128            let key = entry.name.to_lowercase();
4129            map.entry(key)
4130                .and_modify(|v| {
4131                    v.push_str(", ");
4132                    v.push_str(&entry.value);
4133                })
4134                .or_insert(entry.value);
4135        }
4136        Ok(map)
4137    }
4138
4139    /// Returns the value for a single response header, or `None` if not present.
4140    ///
4141    /// The lookup is case-insensitive.
4142    ///
4143    /// # Errors
4144    ///
4145    /// Returns an error if:
4146    /// - No backing protocol object is available (edge case)
4147    /// - The RPC call to the server fails
4148    ///
4149    /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
4150    /// Returns the value for a single response header, or `None` if not present.
4151    ///
4152    /// The lookup is case-insensitive. When multiple headers share the same name,
4153    /// their values are joined with `, ` (matching Playwright's behavior).
4154    ///
4155    /// Uses the raw headers from the server for accurate results.
4156    ///
4157    /// # Errors
4158    ///
4159    /// Returns an error if the underlying `headers_array()` RPC call fails.
4160    ///
4161    /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
4162    pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
4163        let entries = self.headers_array().await?;
4164        let name_lower = name.to_lowercase();
4165        let mut values: Vec<String> = entries
4166            .into_iter()
4167            .filter(|h| h.name.to_lowercase() == name_lower)
4168            .map(|h| h.value)
4169            .collect();
4170
4171        if values.is_empty() {
4172            Ok(None)
4173        } else if values.len() == 1 {
4174            Ok(Some(values.remove(0)))
4175        } else {
4176            Ok(Some(values.join(", ")))
4177        }
4178    }
4179}
4180
4181impl std::fmt::Debug for Response {
4182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4183        f.debug_struct("Response")
4184            .field("url", &self.url)
4185            .field("status", &self.status)
4186            .field("status_text", &self.status_text)
4187            .field("ok", &self.ok)
4188            .finish_non_exhaustive()
4189    }
4190}
4191
4192/// Shared helper: store timeout locally and notify the Playwright server.
4193/// Used by both Page and BrowserContext timeout setters.
4194pub(crate) async fn set_timeout_and_notify(
4195    channel: &crate::server::channel::Channel,
4196    method: &str,
4197    timeout: f64,
4198) {
4199    if let Err(e) = channel
4200        .send_no_result(method, serde_json::json!({ "timeout": timeout }))
4201        .await
4202    {
4203        tracing::warn!("{} send error: {}", method, e);
4204    }
4205}