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