Skip to main content

playwright_rs/protocol/
page.rs

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