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};
9use crate::server::channel::Channel;
10use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
11use crate::server::connection::{ConnectionExt, downcast_parent};
12use base64::Engine;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::any::Any;
16use std::collections::HashMap;
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::sync::{Arc, Mutex, RwLock};
21
22/// Page represents a web page within a browser context.
23///
24/// A Page is created when you call `BrowserContext::new_page()` or `Browser::new_page()`.
25/// Each page is an isolated tab/window within its parent context.
26///
27/// Initially, pages are navigated to "about:blank". Use navigation methods
28/// Use navigation methods to navigate to URLs.
29///
30/// # Example
31///
32/// ```ignore
33/// use playwright_rs::protocol::{
34///     Playwright, ScreenshotOptions, ScreenshotType, AddStyleTagOptions, AddScriptTagOptions,
35///     EmulateMediaOptions, Media, ColorScheme, Viewport,
36/// };
37/// use std::path::PathBuf;
38///
39/// #[tokio::main]
40/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
41///     let playwright = Playwright::launch().await?;
42///     let browser = playwright.chromium().launch().await?;
43///     let page = browser.new_page().await?;
44///
45///     // Demonstrate url() - initially at about:blank
46///     assert_eq!(page.url(), "about:blank");
47///
48///     // Demonstrate goto() - navigate to a page
49///     let html = r#"<!DOCTYPE html>
50///         <html>
51///             <head><title>Test Page</title></head>
52///             <body>
53///                 <h1 id="heading">Hello World</h1>
54///                 <p>First paragraph</p>
55///                 <p>Second paragraph</p>
56///                 <button onclick="alert('Alert!')">Alert</button>
57///                 <a href="data:text/plain,file" download="test.txt">Download</a>
58///             </body>
59///         </html>
60///     "#;
61///     // Data URLs may not return a response (this is normal)
62///     let _response = page.goto(&format!("data:text/html,{}", html), None).await?;
63///
64///     // Demonstrate title()
65///     let title = page.title().await?;
66///     assert_eq!(title, "Test Page");
67///
68///     // Demonstrate content() - returns full HTML including DOCTYPE
69///     let content = page.content().await?;
70///     assert!(content.contains("<!DOCTYPE html>") || content.to_lowercase().contains("<!doctype html>"));
71///     assert!(content.contains("<title>Test Page</title>"));
72///     assert!(content.contains("Hello World"));
73///
74///     // Demonstrate locator()
75///     let heading = page.locator("#heading").await;
76///     let text = heading.text_content().await?;
77///     assert_eq!(text, Some("Hello World".to_string()));
78///
79///     // Demonstrate query_selector()
80///     let element = page.query_selector("h1").await?;
81///     assert!(element.is_some(), "Should find the h1 element");
82///
83///     // Demonstrate query_selector_all()
84///     let paragraphs = page.query_selector_all("p").await?;
85///     assert_eq!(paragraphs.len(), 2);
86///
87///     // Demonstrate evaluate()
88///     page.evaluate::<(), ()>("console.log('Hello from Playwright!')", None).await?;
89///
90///     // Demonstrate evaluate_value()
91///     let result = page.evaluate_value("1 + 1").await?;
92///     assert_eq!(result, "2");
93///
94///     // Demonstrate screenshot()
95///     let bytes = page.screenshot(None).await?;
96///     assert!(!bytes.is_empty());
97///
98///     // Demonstrate screenshot_to_file()
99///     let temp_dir = std::env::temp_dir();
100///     let path = temp_dir.join("playwright_doctest_screenshot.png");
101///     let bytes = page.screenshot_to_file(&path, Some(
102///         ScreenshotOptions::builder()
103///             .screenshot_type(ScreenshotType::Png)
104///             .build()
105///     )).await?;
106///     assert!(!bytes.is_empty());
107///
108///     // Demonstrate reload()
109///     // Data URLs may not return a response on reload (this is normal)
110///     let _response = page.reload(None).await?;
111///
112///     // Demonstrate route() - network interception
113///     page.route("**/*.png", |route| async move {
114///         route.abort(None).await
115///     }).await?;
116///
117///     // Demonstrate on_download() - download handler
118///     page.on_download(|download| async move {
119///         println!("Download started: {}", download.url());
120///         Ok(())
121///     }).await?;
122///
123///     // Demonstrate on_dialog() - dialog handler
124///     page.on_dialog(|dialog| async move {
125///         println!("Dialog: {} - {}", dialog.type_(), dialog.message());
126///         dialog.accept(None).await
127///     }).await?;
128///
129///     // Demonstrate add_style_tag() - inject CSS
130///     page.add_style_tag(
131///         AddStyleTagOptions::builder()
132///             .content("body { background-color: blue; }")
133///             .build()
134///     ).await?;
135///
136///     // Demonstrate set_extra_http_headers() - set page-level headers
137///     let mut headers = std::collections::HashMap::new();
138///     headers.insert("x-custom-header".to_string(), "value".to_string());
139///     page.set_extra_http_headers(headers).await?;
140///
141///     // Demonstrate emulate_media() - emulate print media type
142///     page.emulate_media(Some(
143///         EmulateMediaOptions::builder()
144///             .media(Media::Print)
145///             .color_scheme(ColorScheme::Dark)
146///             .build()
147///     )).await?;
148///
149///     // Demonstrate add_script_tag() - inject a script
150///     page.add_script_tag(Some(
151///         AddScriptTagOptions::builder()
152///             .content("window.injectedByScriptTag = true;")
153///             .build()
154///     )).await?;
155///
156///     // Demonstrate pdf() - generate PDF (Chromium only)
157///     let pdf_bytes = page.pdf(None).await?;
158///     assert!(!pdf_bytes.is_empty());
159///
160///     // Demonstrate set_viewport_size() - responsive testing
161///     let mobile_viewport = Viewport {
162///         width: 375,
163///         height: 667,
164///     };
165///     page.set_viewport_size(mobile_viewport).await?;
166///
167///     // Demonstrate close()
168///     page.close().await?;
169///
170///     browser.close().await?;
171///     Ok(())
172/// }
173/// ```
174///
175/// See: <https://playwright.dev/docs/api/class-page>
176#[derive(Clone)]
177pub struct Page {
178    base: ChannelOwnerImpl,
179    /// Current URL of the page
180    /// Wrapped in RwLock to allow updates from events
181    url: Arc<RwLock<String>>,
182    /// GUID of the main frame
183    main_frame_guid: Arc<str>,
184    /// Cached reference to the main frame for synchronous URL access
185    /// This is populated after the first call to main_frame()
186    cached_main_frame: Arc<Mutex<Option<crate::protocol::Frame>>>,
187    /// Route handlers for network interception
188    route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
189    /// Download event handlers
190    download_handlers: Arc<Mutex<Vec<DownloadHandler>>>,
191    /// Dialog event handlers
192    dialog_handlers: Arc<Mutex<Vec<DialogHandler>>>,
193    /// Request event handlers
194    request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
195    /// Request finished event handlers
196    request_finished_handlers: Arc<Mutex<Vec<RequestHandler>>>,
197    /// Request failed event handlers
198    request_failed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
199    /// Response event handlers
200    response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
201    /// WebSocket event handlers
202    websocket_handlers: Arc<Mutex<Vec<WebSocketHandler>>>,
203    /// Current viewport size (None when no_viewport is set).
204    /// Updated by set_viewport_size().
205    viewport: Arc<RwLock<Option<Viewport>>>,
206    /// Whether this page has been closed.
207    /// Set to true when close() is called or a "close" event is received.
208    is_closed: Arc<AtomicBool>,
209    /// Default timeout for actions (milliseconds), stored as f64 bits.
210    default_timeout_ms: Arc<AtomicU64>,
211    /// Default timeout for navigation operations (milliseconds), stored as f64 bits.
212    default_navigation_timeout_ms: Arc<AtomicU64>,
213    /// Page-level binding callbacks registered via expose_function / expose_binding
214    binding_callbacks: Arc<Mutex<HashMap<String, PageBindingCallback>>>,
215}
216
217/// Type alias for boxed route handler future
218type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
219
220/// Type alias for boxed download handler future
221type DownloadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
222
223/// Type alias for boxed dialog handler future
224type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
225
226/// Type alias for boxed request handler future
227type RequestHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
228
229/// Type alias for boxed response handler future
230type ResponseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
231
232/// Type alias for boxed websocket handler future
233type WebSocketHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
234
235/// Storage for a single route handler
236#[derive(Clone)]
237struct RouteHandlerEntry {
238    pattern: String,
239    handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
240}
241
242/// Download event handler
243type DownloadHandler = Arc<dyn Fn(Download) -> DownloadHandlerFuture + Send + Sync>;
244
245/// Dialog event handler
246type DialogHandler = Arc<dyn Fn(Dialog) -> DialogHandlerFuture + Send + Sync>;
247
248/// Request event handler
249type RequestHandler = Arc<dyn Fn(Request) -> RequestHandlerFuture + Send + Sync>;
250
251/// Response event handler
252type ResponseHandler = Arc<dyn Fn(ResponseObject) -> ResponseHandlerFuture + Send + Sync>;
253
254/// WebSocket event handler
255type WebSocketHandler = Arc<dyn Fn(WebSocket) -> WebSocketHandlerFuture + Send + Sync>;
256
257/// Type alias for boxed page-level binding callback future
258type PageBindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
259
260/// Page-level binding callback: receives deserialized JS args, returns a JSON value
261type PageBindingCallback =
262    Arc<dyn Fn(Vec<serde_json::Value>) -> PageBindingCallbackFuture + Send + Sync>;
263
264impl Page {
265    /// Creates a new Page from protocol initialization
266    ///
267    /// This is called by the object factory when the server sends a `__create__` message
268    /// for a Page object.
269    ///
270    /// # Arguments
271    ///
272    /// * `parent` - The parent BrowserContext object
273    /// * `type_name` - The protocol type name ("Page")
274    /// * `guid` - The unique identifier for this page
275    /// * `initializer` - The initialization data from the server
276    ///
277    /// # Errors
278    ///
279    /// Returns error if initializer is malformed
280    pub fn new(
281        parent: Arc<dyn ChannelOwner>,
282        type_name: String,
283        guid: Arc<str>,
284        initializer: Value,
285    ) -> Result<Self> {
286        // Extract mainFrame GUID from initializer
287        let main_frame_guid: Arc<str> =
288            Arc::from(initializer["mainFrame"]["guid"].as_str().ok_or_else(|| {
289                crate::error::Error::ProtocolError(
290                    "Page initializer missing 'mainFrame.guid' field".to_string(),
291                )
292            })?);
293
294        let base = ChannelOwnerImpl::new(
295            ParentOrConnection::Parent(parent),
296            type_name,
297            guid,
298            initializer,
299        );
300
301        // Initialize URL to about:blank
302        let url = Arc::new(RwLock::new("about:blank".to_string()));
303
304        // Initialize empty route handlers
305        let route_handlers = Arc::new(Mutex::new(Vec::new()));
306
307        // Initialize empty event handlers
308        let download_handlers = Arc::new(Mutex::new(Vec::new()));
309        let dialog_handlers = Arc::new(Mutex::new(Vec::new()));
310        let websocket_handlers = Arc::new(Mutex::new(Vec::new()));
311
312        // Initialize cached main frame as empty (will be populated on first access)
313        let cached_main_frame = Arc::new(Mutex::new(None));
314
315        // Extract viewport from initializer (may be null for no_viewport contexts)
316        let initial_viewport: Option<Viewport> =
317            base.initializer().get("viewportSize").and_then(|v| {
318                if v.is_null() {
319                    None
320                } else {
321                    serde_json::from_value(v.clone()).ok()
322                }
323            });
324        let viewport = Arc::new(RwLock::new(initial_viewport));
325
326        Ok(Self {
327            base,
328            url,
329            main_frame_guid,
330            cached_main_frame,
331            route_handlers,
332            download_handlers,
333            dialog_handlers,
334            request_handlers: Default::default(),
335            request_finished_handlers: Default::default(),
336            request_failed_handlers: Default::default(),
337            response_handlers: Default::default(),
338            websocket_handlers,
339            viewport,
340            is_closed: Arc::new(AtomicBool::new(false)),
341            default_timeout_ms: Arc::new(AtomicU64::new(crate::DEFAULT_TIMEOUT_MS.to_bits())),
342            default_navigation_timeout_ms: Arc::new(AtomicU64::new(
343                crate::DEFAULT_TIMEOUT_MS.to_bits(),
344            )),
345            binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
346        })
347    }
348
349    /// Returns the channel for sending protocol messages
350    ///
351    /// Used internally for sending RPC calls to the page.
352    fn channel(&self) -> &Channel {
353        self.base.channel()
354    }
355
356    /// Returns the main frame of the page.
357    ///
358    /// The main frame is where navigation and DOM operations actually happen.
359    ///
360    /// This method also wires up the back-reference from the frame to the page so that
361    /// `frame.page()`, `frame.locator()`, and `frame.get_by_*()` work correctly.
362    pub async fn main_frame(&self) -> Result<crate::protocol::Frame> {
363        // Get and downcast the Frame object from the connection's object registry
364        let frame: crate::protocol::Frame = self
365            .connection()
366            .get_typed::<crate::protocol::Frame>(&self.main_frame_guid)
367            .await?;
368
369        // Wire up the back-reference so frame.page() / frame.locator() work.
370        // This is safe to call multiple times (subsequent calls are no-ops once set).
371        frame.set_page(self.clone());
372
373        // Cache the frame for synchronous access in url()
374        if let Ok(mut cached) = self.cached_main_frame.lock() {
375            *cached = Some(frame.clone());
376        }
377
378        Ok(frame)
379    }
380
381    /// Returns the current URL of the page.
382    ///
383    /// This returns the last committed URL, including hash fragments from anchor navigation.
384    /// Initially, pages are at "about:blank".
385    ///
386    /// See: <https://playwright.dev/docs/api/class-page#page-url>
387    pub fn url(&self) -> String {
388        // Try to get URL from the cached main frame (source of truth for navigation including hashes)
389        if let Ok(cached) = self.cached_main_frame.lock()
390            && let Some(frame) = cached.as_ref()
391        {
392            return frame.url();
393        }
394
395        // Fallback to cached URL if frame not yet loaded
396        self.url.read().unwrap().clone()
397    }
398
399    /// Closes the page.
400    ///
401    /// This is a graceful operation that sends a close command to the page
402    /// and waits for it to shut down properly.
403    ///
404    /// # Errors
405    ///
406    /// Returns error if:
407    /// - Page has already been closed
408    /// - Communication with browser process fails
409    ///
410    /// See: <https://playwright.dev/docs/api/class-page#page-close>
411    pub async fn close(&self) -> Result<()> {
412        // Send close RPC to server
413        let result = self
414            .channel()
415            .send_no_result("close", serde_json::json!({}))
416            .await;
417        // Mark as closed regardless of error (best-effort)
418        self.is_closed.store(true, Ordering::Relaxed);
419        result
420    }
421
422    /// Returns whether the page has been closed.
423    ///
424    /// Returns `true` after `close()` has been called on this page, or after the
425    /// page receives a close event from the server (e.g. when the browser context
426    /// is closed).
427    ///
428    /// See: <https://playwright.dev/docs/api/class-page#page-is-closed>
429    pub fn is_closed(&self) -> bool {
430        self.is_closed.load(Ordering::Relaxed)
431    }
432
433    /// Sets the default timeout for all operations on this page.
434    ///
435    /// The timeout applies to actions such as `click`, `fill`, `locator.wait_for`, etc.
436    /// Pass `0` to disable timeouts.
437    ///
438    /// This stores the value locally so that subsequent action calls use it when
439    /// no explicit timeout is provided, and also notifies the Playwright server
440    /// so it can apply the same default on its side.
441    ///
442    /// # Arguments
443    ///
444    /// * `timeout` - Timeout in milliseconds
445    ///
446    /// See: <https://playwright.dev/docs/api/class-page#page-set-default-timeout>
447    pub async fn set_default_timeout(&self, timeout: f64) {
448        self.default_timeout_ms
449            .store(timeout.to_bits(), Ordering::Relaxed);
450        set_timeout_and_notify(self.channel(), "setDefaultTimeoutNoReply", timeout).await;
451    }
452
453    /// Sets the default timeout for navigation operations on this page.
454    ///
455    /// The timeout applies to navigation actions such as `goto`, `reload`,
456    /// `go_back`, and `go_forward`. Pass `0` to disable timeouts.
457    ///
458    /// # Arguments
459    ///
460    /// * `timeout` - Timeout in milliseconds
461    ///
462    /// See: <https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout>
463    pub async fn set_default_navigation_timeout(&self, timeout: f64) {
464        self.default_navigation_timeout_ms
465            .store(timeout.to_bits(), Ordering::Relaxed);
466        set_timeout_and_notify(
467            self.channel(),
468            "setDefaultNavigationTimeoutNoReply",
469            timeout,
470        )
471        .await;
472    }
473
474    /// Returns the current default action timeout in milliseconds.
475    pub fn default_timeout_ms(&self) -> f64 {
476        f64::from_bits(self.default_timeout_ms.load(Ordering::Relaxed))
477    }
478
479    /// Returns the current default navigation timeout in milliseconds.
480    pub fn default_navigation_timeout_ms(&self) -> f64 {
481        f64::from_bits(self.default_navigation_timeout_ms.load(Ordering::Relaxed))
482    }
483
484    /// Returns GotoOptions with the navigation timeout filled in if not already set.
485    ///
486    /// Used internally to ensure the page's configured default navigation timeout
487    /// is used when the caller does not provide an explicit timeout.
488    fn with_navigation_timeout(&self, options: Option<GotoOptions>) -> GotoOptions {
489        let nav_timeout = self.default_navigation_timeout_ms();
490        match options {
491            Some(opts) if opts.timeout.is_some() => opts,
492            Some(mut opts) => {
493                opts.timeout = Some(std::time::Duration::from_millis(nav_timeout as u64));
494                opts
495            }
496            None => GotoOptions {
497                timeout: Some(std::time::Duration::from_millis(nav_timeout as u64)),
498                wait_until: None,
499            },
500        }
501    }
502
503    /// Returns all frames in the page, including the main frame.
504    ///
505    /// Currently returns only the main (top-level) frame. Iframe enumeration
506    /// is not yet implemented and will be added in a future release.
507    ///
508    /// # Errors
509    ///
510    /// Returns error if:
511    /// - Page has been closed
512    /// - Communication with browser process fails
513    ///
514    /// See: <https://playwright.dev/docs/api/class-page#page-frames>
515    pub async fn frames(&self) -> Result<Vec<crate::protocol::Frame>> {
516        // Start with the main frame
517        let main = self.main_frame().await?;
518        Ok(vec![main])
519    }
520
521    /// Navigates to the specified URL.
522    ///
523    /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
524    /// about:blank). This matches Playwright's behavior across all language bindings.
525    ///
526    /// # Arguments
527    ///
528    /// * `url` - The URL to navigate to
529    /// * `options` - Optional navigation options (timeout, wait_until)
530    ///
531    /// # Errors
532    ///
533    /// Returns error if:
534    /// - URL is invalid
535    /// - Navigation timeout (default 30s)
536    /// - Network error
537    ///
538    /// See: <https://playwright.dev/docs/api/class-page#page-goto>
539    pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
540        // Inject the page-level navigation timeout when no explicit timeout is given
541        let options = self.with_navigation_timeout(options);
542
543        // Delegate to main frame
544        let frame = self.main_frame().await.map_err(|e| match e {
545            Error::TargetClosed { context, .. } => Error::TargetClosed {
546                target_type: "Page".to_string(),
547                context,
548            },
549            other => other,
550        })?;
551
552        let response = frame.goto(url, Some(options)).await.map_err(|e| match e {
553            Error::TargetClosed { context, .. } => Error::TargetClosed {
554                target_type: "Page".to_string(),
555                context,
556            },
557            other => other,
558        })?;
559
560        // Update the page's URL if we got a response
561        if let Some(ref resp) = response
562            && let Ok(mut page_url) = self.url.write()
563        {
564            *page_url = resp.url().to_string();
565        }
566
567        Ok(response)
568    }
569
570    /// Returns the browser context that the page belongs to.
571    pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
572        downcast_parent::<crate::protocol::BrowserContext>(self)
573            .ok_or_else(|| Error::ProtocolError("Page parent is not a BrowserContext".to_string()))
574    }
575
576    /// Pauses script execution.
577    ///
578    /// Playwright will stop executing the script and wait for the user to either press
579    /// "Resume" in the page overlay or in the debugger.
580    ///
581    /// See: <https://playwright.dev/docs/api/class-page#page-pause>
582    pub async fn pause(&self) -> Result<()> {
583        self.context()?.pause().await
584    }
585
586    /// Returns the page's title.
587    ///
588    /// See: <https://playwright.dev/docs/api/class-page#page-title>
589    pub async fn title(&self) -> Result<String> {
590        // Delegate to main frame
591        let frame = self.main_frame().await?;
592        frame.title().await
593    }
594
595    /// Returns the full HTML content of the page, including the DOCTYPE.
596    ///
597    /// This method retrieves the complete HTML markup of the page,
598    /// including the doctype declaration and all DOM elements.
599    ///
600    /// See: <https://playwright.dev/docs/api/class-page#page-content>
601    pub async fn content(&self) -> Result<String> {
602        // Delegate to main frame
603        let frame = self.main_frame().await?;
604        frame.content().await
605    }
606
607    /// Sets the content of the page.
608    ///
609    /// See: <https://playwright.dev/docs/api/class-page#page-set-content>
610    pub async fn set_content(&self, html: &str, options: Option<GotoOptions>) -> Result<()> {
611        let frame = self.main_frame().await?;
612        frame.set_content(html, options).await
613    }
614
615    /// Waits for the required load state to be reached.
616    ///
617    /// This resolves when the page reaches a required load state, `load` by default.
618    /// The navigation must have been committed when this method is called. If the current
619    /// document has already reached the required state, resolves immediately.
620    ///
621    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-load-state>
622    pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
623        let frame = self.main_frame().await?;
624        frame.wait_for_load_state(state).await
625    }
626
627    /// Waits for the main frame to navigate to a URL matching the given string or glob pattern.
628    ///
629    /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-url>
630    pub async fn wait_for_url(&self, url: &str, options: Option<GotoOptions>) -> Result<()> {
631        let frame = self.main_frame().await?;
632        frame.wait_for_url(url, options).await
633    }
634
635    /// Creates a locator for finding elements on the page.
636    ///
637    /// Locators are the central piece of Playwright's auto-waiting and retry-ability.
638    /// They don't execute queries until an action is performed.
639    ///
640    /// # Arguments
641    ///
642    /// * `selector` - CSS selector or other locating strategy
643    ///
644    /// See: <https://playwright.dev/docs/api/class-page#page-locator>
645    pub async fn locator(&self, selector: &str) -> crate::protocol::Locator {
646        // Get the main frame
647        let frame = self.main_frame().await.expect("Main frame should exist");
648
649        crate::protocol::Locator::new(Arc::new(frame), selector.to_string(), self.clone())
650    }
651
652    /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) for an iframe on this page.
653    ///
654    /// The `selector` identifies the iframe element (e.g., `"iframe[name='content']"`).
655    ///
656    /// See: <https://playwright.dev/docs/api/class-page#page-frame-locator>
657    pub async fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
658        let frame = self.main_frame().await.expect("Main frame should exist");
659        crate::protocol::FrameLocator::new(Arc::new(frame), selector.to_string(), self.clone())
660    }
661
662    /// Returns a locator that matches elements containing the given text.
663    ///
664    /// By default, matching is case-insensitive and searches for a substring.
665    /// Set `exact` to `true` for case-sensitive exact matching.
666    ///
667    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-text>
668    pub async fn get_by_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
669        self.locator(&crate::protocol::locator::get_by_text_selector(text, exact))
670            .await
671    }
672
673    /// Returns a locator that matches elements by their associated label text.
674    ///
675    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-label>
676    pub async fn get_by_label(&self, text: &str, exact: bool) -> crate::protocol::Locator {
677        self.locator(&crate::protocol::locator::get_by_label_selector(
678            text, exact,
679        ))
680        .await
681    }
682
683    /// Returns a locator that matches elements by their placeholder text.
684    ///
685    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-placeholder>
686    pub async fn get_by_placeholder(&self, text: &str, exact: bool) -> crate::protocol::Locator {
687        self.locator(&crate::protocol::locator::get_by_placeholder_selector(
688            text, exact,
689        ))
690        .await
691    }
692
693    /// Returns a locator that matches elements by their alt text.
694    ///
695    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-alt-text>
696    pub async fn get_by_alt_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
697        self.locator(&crate::protocol::locator::get_by_alt_text_selector(
698            text, exact,
699        ))
700        .await
701    }
702
703    /// Returns a locator that matches elements by their title attribute.
704    ///
705    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-title>
706    pub async fn get_by_title(&self, text: &str, exact: bool) -> crate::protocol::Locator {
707        self.locator(&crate::protocol::locator::get_by_title_selector(
708            text, exact,
709        ))
710        .await
711    }
712
713    /// Returns a locator that matches elements by their `data-testid` attribute.
714    ///
715    /// Always uses exact matching (case-sensitive).
716    ///
717    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-test-id>
718    pub async fn get_by_test_id(&self, test_id: &str) -> crate::protocol::Locator {
719        self.locator(&crate::protocol::locator::get_by_test_id_selector(test_id))
720            .await
721    }
722
723    /// Returns a locator that matches elements by their ARIA role.
724    ///
725    /// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
726    pub async fn get_by_role(
727        &self,
728        role: crate::protocol::locator::AriaRole,
729        options: Option<crate::protocol::locator::GetByRoleOptions>,
730    ) -> crate::protocol::Locator {
731        self.locator(&crate::protocol::locator::get_by_role_selector(
732            role, options,
733        ))
734        .await
735    }
736
737    /// Returns the keyboard instance for low-level keyboard control.
738    ///
739    /// See: <https://playwright.dev/docs/api/class-page#page-keyboard>
740    pub fn keyboard(&self) -> crate::protocol::Keyboard {
741        crate::protocol::Keyboard::new(self.clone())
742    }
743
744    /// Returns the mouse instance for low-level mouse control.
745    ///
746    /// See: <https://playwright.dev/docs/api/class-page#page-mouse>
747    pub fn mouse(&self) -> crate::protocol::Mouse {
748        crate::protocol::Mouse::new(self.clone())
749    }
750
751    // Internal keyboard methods (called by Keyboard struct)
752
753    pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
754        self.channel()
755            .send_no_result(
756                "keyboardDown",
757                serde_json::json!({
758                    "key": key
759                }),
760            )
761            .await
762    }
763
764    pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
765        self.channel()
766            .send_no_result(
767                "keyboardUp",
768                serde_json::json!({
769                    "key": key
770                }),
771            )
772            .await
773    }
774
775    pub(crate) async fn keyboard_press(
776        &self,
777        key: &str,
778        options: Option<crate::protocol::KeyboardOptions>,
779    ) -> Result<()> {
780        let mut params = serde_json::json!({
781            "key": key
782        });
783
784        if let Some(opts) = options {
785            let opts_json = opts.to_json();
786            if let Some(obj) = params.as_object_mut()
787                && let Some(opts_obj) = opts_json.as_object()
788            {
789                obj.extend(opts_obj.clone());
790            }
791        }
792
793        self.channel().send_no_result("keyboardPress", params).await
794    }
795
796    pub(crate) async fn keyboard_type(
797        &self,
798        text: &str,
799        options: Option<crate::protocol::KeyboardOptions>,
800    ) -> Result<()> {
801        let mut params = serde_json::json!({
802            "text": text
803        });
804
805        if let Some(opts) = options {
806            let opts_json = opts.to_json();
807            if let Some(obj) = params.as_object_mut()
808                && let Some(opts_obj) = opts_json.as_object()
809            {
810                obj.extend(opts_obj.clone());
811            }
812        }
813
814        self.channel().send_no_result("keyboardType", params).await
815    }
816
817    pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
818        self.channel()
819            .send_no_result(
820                "keyboardInsertText",
821                serde_json::json!({
822                    "text": text
823                }),
824            )
825            .await
826    }
827
828    // Internal mouse methods (called by Mouse struct)
829
830    pub(crate) async fn mouse_move(
831        &self,
832        x: i32,
833        y: i32,
834        options: Option<crate::protocol::MouseOptions>,
835    ) -> Result<()> {
836        let mut params = serde_json::json!({
837            "x": x,
838            "y": y
839        });
840
841        if let Some(opts) = options {
842            let opts_json = opts.to_json();
843            if let Some(obj) = params.as_object_mut()
844                && let Some(opts_obj) = opts_json.as_object()
845            {
846                obj.extend(opts_obj.clone());
847            }
848        }
849
850        self.channel().send_no_result("mouseMove", params).await
851    }
852
853    pub(crate) async fn mouse_click(
854        &self,
855        x: i32,
856        y: i32,
857        options: Option<crate::protocol::MouseOptions>,
858    ) -> Result<()> {
859        let mut params = serde_json::json!({
860            "x": x,
861            "y": y
862        });
863
864        if let Some(opts) = options {
865            let opts_json = opts.to_json();
866            if let Some(obj) = params.as_object_mut()
867                && let Some(opts_obj) = opts_json.as_object()
868            {
869                obj.extend(opts_obj.clone());
870            }
871        }
872
873        self.channel().send_no_result("mouseClick", params).await
874    }
875
876    pub(crate) async fn mouse_dblclick(
877        &self,
878        x: i32,
879        y: i32,
880        options: Option<crate::protocol::MouseOptions>,
881    ) -> Result<()> {
882        let mut params = serde_json::json!({
883            "x": x,
884            "y": y,
885            "clickCount": 2
886        });
887
888        if let Some(opts) = options {
889            let opts_json = opts.to_json();
890            if let Some(obj) = params.as_object_mut()
891                && let Some(opts_obj) = opts_json.as_object()
892            {
893                obj.extend(opts_obj.clone());
894            }
895        }
896
897        self.channel().send_no_result("mouseClick", params).await
898    }
899
900    pub(crate) async fn mouse_down(
901        &self,
902        options: Option<crate::protocol::MouseOptions>,
903    ) -> Result<()> {
904        let mut params = serde_json::json!({});
905
906        if let Some(opts) = options {
907            let opts_json = opts.to_json();
908            if let Some(obj) = params.as_object_mut()
909                && let Some(opts_obj) = opts_json.as_object()
910            {
911                obj.extend(opts_obj.clone());
912            }
913        }
914
915        self.channel().send_no_result("mouseDown", params).await
916    }
917
918    pub(crate) async fn mouse_up(
919        &self,
920        options: Option<crate::protocol::MouseOptions>,
921    ) -> Result<()> {
922        let mut params = serde_json::json!({});
923
924        if let Some(opts) = options {
925            let opts_json = opts.to_json();
926            if let Some(obj) = params.as_object_mut()
927                && let Some(opts_obj) = opts_json.as_object()
928            {
929                obj.extend(opts_obj.clone());
930            }
931        }
932
933        self.channel().send_no_result("mouseUp", params).await
934    }
935
936    pub(crate) async fn mouse_wheel(&self, delta_x: i32, delta_y: i32) -> Result<()> {
937        self.channel()
938            .send_no_result(
939                "mouseWheel",
940                serde_json::json!({
941                    "deltaX": delta_x,
942                    "deltaY": delta_y
943                }),
944            )
945            .await
946    }
947
948    /// Reloads the current page.
949    ///
950    /// # Arguments
951    ///
952    /// * `options` - Optional reload options (timeout, wait_until)
953    ///
954    /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
955    /// about:blank). This matches Playwright's behavior across all language bindings.
956    ///
957    /// See: <https://playwright.dev/docs/api/class-page#page-reload>
958    pub async fn reload(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
959        self.navigate_history("reload", options).await
960    }
961
962    /// Navigates to the previous page in history.
963    ///
964    /// Returns the main resource response. In case of multiple server redirects, the navigation
965    /// will resolve with the response of the last redirect. If can not go back, returns `None`.
966    ///
967    /// See: <https://playwright.dev/docs/api/class-page#page-go-back>
968    pub async fn go_back(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
969        self.navigate_history("goBack", options).await
970    }
971
972    /// Navigates to the next page in history.
973    ///
974    /// Returns the main resource response. In case of multiple server redirects, the navigation
975    /// will resolve with the response of the last redirect. If can not go forward, returns `None`.
976    ///
977    /// See: <https://playwright.dev/docs/api/class-page#page-go-forward>
978    pub async fn go_forward(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
979        self.navigate_history("goForward", options).await
980    }
981
982    /// Shared implementation for reload, go_back and go_forward.
983    async fn navigate_history(
984        &self,
985        method: &str,
986        options: Option<GotoOptions>,
987    ) -> Result<Option<Response>> {
988        // Inject the page-level navigation timeout when no explicit timeout is given
989        let opts = self.with_navigation_timeout(options);
990        let mut params = serde_json::json!({});
991
992        // opts.timeout is always Some(...) because with_navigation_timeout guarantees it
993        if let Some(timeout) = opts.timeout {
994            params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
995        } else {
996            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
997        }
998        if let Some(wait_until) = opts.wait_until {
999            params["waitUntil"] = serde_json::json!(wait_until.as_str());
1000        }
1001
1002        #[derive(Deserialize)]
1003        struct NavigationResponse {
1004            response: Option<ResponseReference>,
1005        }
1006
1007        #[derive(Deserialize)]
1008        struct ResponseReference {
1009            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
1010            guid: Arc<str>,
1011        }
1012
1013        let result: NavigationResponse = self.channel().send(method, params).await?;
1014
1015        if let Some(response_ref) = result.response {
1016            let response_arc = {
1017                let mut attempts = 0;
1018                let max_attempts = 20;
1019                loop {
1020                    match self.connection().get_object(&response_ref.guid).await {
1021                        Ok(obj) => break obj,
1022                        Err(_) if attempts < max_attempts => {
1023                            attempts += 1;
1024                            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1025                        }
1026                        Err(e) => return Err(e),
1027                    }
1028                }
1029            };
1030
1031            let initializer = response_arc.initializer();
1032
1033            let status = initializer["status"].as_u64().ok_or_else(|| {
1034                crate::error::Error::ProtocolError("Response missing status".to_string())
1035            })? as u16;
1036
1037            let headers = initializer["headers"]
1038                .as_array()
1039                .ok_or_else(|| {
1040                    crate::error::Error::ProtocolError("Response missing headers".to_string())
1041                })?
1042                .iter()
1043                .filter_map(|h| {
1044                    let name = h["name"].as_str()?;
1045                    let value = h["value"].as_str()?;
1046                    Some((name.to_string(), value.to_string()))
1047                })
1048                .collect();
1049
1050            let response = Response::new(
1051                initializer["url"]
1052                    .as_str()
1053                    .ok_or_else(|| {
1054                        crate::error::Error::ProtocolError("Response missing url".to_string())
1055                    })?
1056                    .to_string(),
1057                status,
1058                initializer["statusText"].as_str().unwrap_or("").to_string(),
1059                headers,
1060                Some(response_arc),
1061            );
1062
1063            if let Ok(mut page_url) = self.url.write() {
1064                *page_url = response.url().to_string();
1065            }
1066
1067            Ok(Some(response))
1068        } else {
1069            Ok(None)
1070        }
1071    }
1072
1073    /// Returns the first element matching the selector, or None if not found.
1074    ///
1075    /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
1076    pub async fn query_selector(
1077        &self,
1078        selector: &str,
1079    ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
1080        let frame = self.main_frame().await?;
1081        frame.query_selector(selector).await
1082    }
1083
1084    /// Returns all elements matching the selector.
1085    ///
1086    /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
1087    pub async fn query_selector_all(
1088        &self,
1089        selector: &str,
1090    ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
1091        let frame = self.main_frame().await?;
1092        frame.query_selector_all(selector).await
1093    }
1094
1095    /// Takes a screenshot of the page and returns the image bytes.
1096    ///
1097    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1098    pub async fn screenshot(
1099        &self,
1100        options: Option<crate::protocol::ScreenshotOptions>,
1101    ) -> Result<Vec<u8>> {
1102        let params = if let Some(opts) = options {
1103            opts.to_json()
1104        } else {
1105            // Default to PNG with required timeout
1106            serde_json::json!({
1107                "type": "png",
1108                "timeout": crate::DEFAULT_TIMEOUT_MS
1109            })
1110        };
1111
1112        #[derive(Deserialize)]
1113        struct ScreenshotResponse {
1114            binary: String,
1115        }
1116
1117        let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
1118
1119        // Decode base64 to bytes
1120        let bytes = base64::prelude::BASE64_STANDARD
1121            .decode(&response.binary)
1122            .map_err(|e| {
1123                crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
1124            })?;
1125
1126        Ok(bytes)
1127    }
1128
1129    /// Takes a screenshot and saves it to a file, also returning the bytes.
1130    ///
1131    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1132    pub async fn screenshot_to_file(
1133        &self,
1134        path: &std::path::Path,
1135        options: Option<crate::protocol::ScreenshotOptions>,
1136    ) -> Result<Vec<u8>> {
1137        // Get the screenshot bytes
1138        let bytes = self.screenshot(options).await?;
1139
1140        // Write to file
1141        tokio::fs::write(path, &bytes).await.map_err(|e| {
1142            crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
1143        })?;
1144
1145        Ok(bytes)
1146    }
1147
1148    /// Evaluates JavaScript in the page context (without return value).
1149    ///
1150    /// Executes the provided JavaScript expression or function within the page's
1151    /// context without returning a value.
1152    ///
1153    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1154    pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
1155        // Delegate to the main frame
1156        let frame = self.main_frame().await?;
1157        frame.frame_evaluate_expression(expression).await
1158    }
1159
1160    /// Evaluates JavaScript in the page context with optional arguments.
1161    ///
1162    /// Executes the provided JavaScript expression or function within the page's
1163    /// context and returns the result. The return value must be JSON-serializable.
1164    ///
1165    /// # Arguments
1166    ///
1167    /// * `expression` - JavaScript code to evaluate
1168    /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1169    ///
1170    /// # Returns
1171    ///
1172    /// The result as a `serde_json::Value`
1173    ///
1174    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1175    pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
1176        &self,
1177        expression: &str,
1178        arg: Option<&T>,
1179    ) -> Result<U> {
1180        // Delegate to the main frame
1181        let frame = self.main_frame().await?;
1182        let result = frame.evaluate(expression, arg).await?;
1183        serde_json::from_value(result).map_err(Error::from)
1184    }
1185
1186    /// Evaluates a JavaScript expression and returns the result as a String.
1187    ///
1188    /// # Arguments
1189    ///
1190    /// * `expression` - JavaScript code to evaluate
1191    ///
1192    /// # Returns
1193    ///
1194    /// The result converted to a String
1195    ///
1196    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1197    pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
1198        let frame = self.main_frame().await?;
1199        frame.frame_evaluate_expression_value(expression).await
1200    }
1201
1202    /// Registers a route handler for network interception.
1203    ///
1204    /// When a request matches the specified pattern, the handler will be called
1205    /// with a Route object that can abort, continue, or fulfill the request.
1206    ///
1207    /// # Arguments
1208    ///
1209    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
1210    /// * `handler` - Async closure that handles the route
1211    ///
1212    /// See: <https://playwright.dev/docs/api/class-page#page-route>
1213    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1214    where
1215        F: Fn(Route) -> Fut + Send + Sync + 'static,
1216        Fut: Future<Output = Result<()>> + Send + 'static,
1217    {
1218        // 1. Wrap handler in Arc with type erasure
1219        let handler =
1220            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1221
1222        // 2. Store in handlers list
1223        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1224            pattern: pattern.to_string(),
1225            handler,
1226        });
1227
1228        // 3. Enable network interception via protocol
1229        self.enable_network_interception().await?;
1230
1231        Ok(())
1232    }
1233
1234    /// Updates network interception patterns for this page
1235    async fn enable_network_interception(&self) -> Result<()> {
1236        // Collect all patterns from registered handlers
1237        // Each pattern must be an object with "glob" field
1238        let patterns: Vec<serde_json::Value> = self
1239            .route_handlers
1240            .lock()
1241            .unwrap()
1242            .iter()
1243            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1244            .collect();
1245
1246        // Send protocol command to update network interception patterns
1247        // Follows playwright-python's approach
1248        self.channel()
1249            .send_no_result(
1250                "setNetworkInterceptionPatterns",
1251                serde_json::json!({
1252                    "patterns": patterns
1253                }),
1254            )
1255            .await
1256    }
1257
1258    /// Removes route handler(s) matching the given URL pattern.
1259    ///
1260    /// # Arguments
1261    ///
1262    /// * `pattern` - URL pattern to remove handlers for
1263    ///
1264    /// See: <https://playwright.dev/docs/api/class-page#page-unroute>
1265    pub async fn unroute(&self, pattern: &str) -> Result<()> {
1266        self.route_handlers
1267            .lock()
1268            .unwrap()
1269            .retain(|entry| entry.pattern != pattern);
1270        self.enable_network_interception().await
1271    }
1272
1273    /// Removes all registered route handlers.
1274    ///
1275    /// # Arguments
1276    ///
1277    /// * `behavior` - Optional behavior for in-flight handlers
1278    ///
1279    /// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
1280    pub async fn unroute_all(
1281        &self,
1282        _behavior: Option<crate::protocol::route::UnrouteBehavior>,
1283    ) -> Result<()> {
1284        self.route_handlers.lock().unwrap().clear();
1285        self.enable_network_interception().await
1286    }
1287
1288    /// Handles a route event from the protocol
1289    ///
1290    /// Called by on_event when a "route" event is received.
1291    /// Supports handler chaining via `route.fallback()` — if a handler calls
1292    /// `fallback()` instead of `continue_()`, `abort()`, or `fulfill()`, the
1293    /// next matching handler in the chain is tried.
1294    async fn on_route_event(&self, route: Route) {
1295        let handlers = self.route_handlers.lock().unwrap().clone();
1296        let url = route.request().url().to_string();
1297
1298        // Find matching handler (last registered wins, with fallback chaining)
1299        for entry in handlers.iter().rev() {
1300            if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
1301                let handler = entry.handler.clone();
1302                if let Err(e) = handler(route.clone()).await {
1303                    tracing::warn!("Route handler error: {}", e);
1304                    break;
1305                }
1306                // If handler called fallback(), try the next matching handler
1307                if !route.was_handled() {
1308                    continue;
1309                }
1310                break;
1311            }
1312        }
1313    }
1314
1315    /// Registers a download event handler.
1316    ///
1317    /// The handler will be called when a download is triggered by the page.
1318    /// Downloads occur when the page initiates a file download (e.g., clicking a link
1319    /// with the download attribute, or a server response with Content-Disposition: attachment).
1320    ///
1321    /// # Arguments
1322    ///
1323    /// * `handler` - Async closure that receives the Download object
1324    ///
1325    /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
1326    pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1327    where
1328        F: Fn(Download) -> Fut + Send + Sync + 'static,
1329        Fut: Future<Output = Result<()>> + Send + 'static,
1330    {
1331        // Wrap handler with type erasure
1332        let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
1333            Box::pin(handler(download))
1334        });
1335
1336        // Store handler
1337        self.download_handlers.lock().unwrap().push(handler);
1338
1339        Ok(())
1340    }
1341
1342    /// Registers a dialog event handler.
1343    ///
1344    /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
1345    /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
1346    ///
1347    /// # Arguments
1348    ///
1349    /// * `handler` - Async closure that receives the Dialog object
1350    ///
1351    /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
1352    pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1353    where
1354        F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1355        Fut: Future<Output = Result<()>> + Send + 'static,
1356    {
1357        // Wrap handler with type erasure
1358        let handler =
1359            Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
1360
1361        // Store handler
1362        self.dialog_handlers.lock().unwrap().push(handler);
1363
1364        // Dialog events are auto-emitted (no subscription needed)
1365
1366        Ok(())
1367    }
1368
1369    /// See: <https://playwright.dev/docs/api/class-page#page-event-request>
1370    pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
1371    where
1372        F: Fn(Request) -> Fut + Send + Sync + 'static,
1373        Fut: Future<Output = Result<()>> + Send + 'static,
1374    {
1375        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1376            Box::pin(handler(request))
1377        });
1378
1379        let needs_subscription = self.request_handlers.lock().unwrap().is_empty();
1380        if needs_subscription {
1381            _ = self.channel().update_subscription("request", true).await;
1382        }
1383        self.request_handlers.lock().unwrap().push(handler);
1384
1385        Ok(())
1386    }
1387
1388    /// See: <https://playwright.dev/docs/api/class-page#page-event-request-finished>
1389    pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
1390    where
1391        F: Fn(Request) -> Fut + Send + Sync + 'static,
1392        Fut: Future<Output = Result<()>> + Send + 'static,
1393    {
1394        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1395            Box::pin(handler(request))
1396        });
1397
1398        let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
1399        if needs_subscription {
1400            _ = self
1401                .channel()
1402                .update_subscription("requestFinished", true)
1403                .await;
1404        }
1405        self.request_finished_handlers.lock().unwrap().push(handler);
1406
1407        Ok(())
1408    }
1409
1410    /// See: <https://playwright.dev/docs/api/class-page#page-event-request-failed>
1411    pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
1412    where
1413        F: Fn(Request) -> Fut + Send + Sync + 'static,
1414        Fut: Future<Output = Result<()>> + Send + 'static,
1415    {
1416        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1417            Box::pin(handler(request))
1418        });
1419
1420        let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
1421        if needs_subscription {
1422            _ = self
1423                .channel()
1424                .update_subscription("requestFailed", true)
1425                .await;
1426        }
1427        self.request_failed_handlers.lock().unwrap().push(handler);
1428
1429        Ok(())
1430    }
1431
1432    /// See: <https://playwright.dev/docs/api/class-page#page-event-response>
1433    pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
1434    where
1435        F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
1436        Fut: Future<Output = Result<()>> + Send + 'static,
1437    {
1438        let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
1439            Box::pin(handler(response))
1440        });
1441
1442        let needs_subscription = self.response_handlers.lock().unwrap().is_empty();
1443        if needs_subscription {
1444            _ = self.channel().update_subscription("response", true).await;
1445        }
1446        self.response_handlers.lock().unwrap().push(handler);
1447
1448        Ok(())
1449    }
1450
1451    /// Adds a listener for the `websocket` event.
1452    ///
1453    /// The handler will be called when a WebSocket request is dispatched.
1454    ///
1455    /// # Arguments
1456    ///
1457    /// * `handler` - The function to call when the event occurs
1458    ///
1459    /// See: <https://playwright.dev/docs/api/class-page#page-on-websocket>
1460    pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
1461    where
1462        F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1463        Fut: Future<Output = Result<()>> + Send + 'static,
1464    {
1465        let handler =
1466            Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
1467        self.websocket_handlers.lock().unwrap().push(handler);
1468        Ok(())
1469    }
1470
1471    /// Exposes a Rust function to this page as `window[name]` in JavaScript.
1472    ///
1473    /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
1474    /// server fires a `bindingCall` event on the **page** channel that invokes
1475    /// `callback` with the deserialized arguments. The return value is sent back
1476    /// to JS so the `await window[name](…)` expression resolves with it.
1477    ///
1478    /// The binding is page-scoped and not visible to other pages in the same context.
1479    ///
1480    /// # Arguments
1481    ///
1482    /// * `name`     – JavaScript identifier that will be available as `window[name]`.
1483    /// * `callback` – Async closure called with `Vec<serde_json::Value>` (JS arguments)
1484    ///   returning `serde_json::Value` (the result).
1485    ///
1486    /// # Errors
1487    ///
1488    /// Returns error if:
1489    /// - The page has been closed.
1490    /// - Communication with the browser process fails.
1491    ///
1492    /// See: <https://playwright.dev/docs/api/class-page#page-expose-function>
1493    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1494    where
1495        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1496        Fut: Future<Output = serde_json::Value> + Send + 'static,
1497    {
1498        self.expose_binding_internal(name, false, callback).await
1499    }
1500
1501    /// Exposes a Rust function to this page as `window[name]` in JavaScript,
1502    /// with `needsHandle: true`.
1503    ///
1504    /// Identical to [`expose_function`](Self::expose_function) but the Playwright
1505    /// server passes the first argument as a `JSHandle` object rather than a plain
1506    /// value.
1507    ///
1508    /// # Arguments
1509    ///
1510    /// * `name`     – JavaScript identifier.
1511    /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
1512    ///
1513    /// # Errors
1514    ///
1515    /// Returns error if:
1516    /// - The page has been closed.
1517    /// - Communication with the browser process fails.
1518    ///
1519    /// See: <https://playwright.dev/docs/api/class-page#page-expose-binding>
1520    pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1521    where
1522        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1523        Fut: Future<Output = serde_json::Value> + Send + 'static,
1524    {
1525        self.expose_binding_internal(name, true, callback).await
1526    }
1527
1528    /// Internal implementation shared by page-level expose_function and expose_binding.
1529    ///
1530    /// Both `expose_function` and `expose_binding` use `needsHandle: false` because
1531    /// the current implementation does not support JSHandle objects. Using
1532    /// `needsHandle: true` would cause the Playwright server to wrap the first
1533    /// argument as a `JSHandle`, which requires a JSHandle protocol object that
1534    /// is not yet implemented.
1535    async fn expose_binding_internal<F, Fut>(
1536        &self,
1537        name: &str,
1538        _needs_handle: bool,
1539        callback: F,
1540    ) -> Result<()>
1541    where
1542        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1543        Fut: Future<Output = serde_json::Value> + Send + 'static,
1544    {
1545        let callback: PageBindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
1546            Box::pin(callback(args)) as PageBindingCallbackFuture
1547        });
1548
1549        // Store callback before sending RPC (avoids race with early bindingCall events)
1550        self.binding_callbacks
1551            .lock()
1552            .unwrap()
1553            .insert(name.to_string(), callback);
1554
1555        // Tell the Playwright server to inject window[name] into this page.
1556        // Always use needsHandle: false — see note above.
1557        self.channel()
1558            .send_no_result(
1559                "exposeBinding",
1560                serde_json::json!({ "name": name, "needsHandle": false }),
1561            )
1562            .await
1563    }
1564
1565    /// Handles a download event from the protocol
1566    async fn on_download_event(&self, download: Download) {
1567        let handlers = self.download_handlers.lock().unwrap().clone();
1568
1569        for handler in handlers {
1570            if let Err(e) = handler(download.clone()).await {
1571                tracing::warn!("Download handler error: {}", e);
1572            }
1573        }
1574    }
1575
1576    /// Handles a dialog event from the protocol
1577    async fn on_dialog_event(&self, dialog: Dialog) {
1578        let handlers = self.dialog_handlers.lock().unwrap().clone();
1579
1580        for handler in handlers {
1581            if let Err(e) = handler(dialog.clone()).await {
1582                tracing::warn!("Dialog handler error: {}", e);
1583            }
1584        }
1585    }
1586
1587    async fn on_request_event(&self, request: Request) {
1588        let handlers = self.request_handlers.lock().unwrap().clone();
1589
1590        for handler in handlers {
1591            if let Err(e) = handler(request.clone()).await {
1592                tracing::warn!("Request handler error: {}", e);
1593            }
1594        }
1595    }
1596
1597    async fn on_request_failed_event(&self, request: Request) {
1598        let handlers = self.request_failed_handlers.lock().unwrap().clone();
1599
1600        for handler in handlers {
1601            if let Err(e) = handler(request.clone()).await {
1602                tracing::warn!("RequestFailed handler error: {}", e);
1603            }
1604        }
1605    }
1606
1607    async fn on_request_finished_event(&self, request: Request) {
1608        let handlers = self.request_finished_handlers.lock().unwrap().clone();
1609
1610        for handler in handlers {
1611            if let Err(e) = handler(request.clone()).await {
1612                tracing::warn!("RequestFinished handler error: {}", e);
1613            }
1614        }
1615    }
1616
1617    async fn on_response_event(&self, response: ResponseObject) {
1618        let handlers = self.response_handlers.lock().unwrap().clone();
1619
1620        for handler in handlers {
1621            if let Err(e) = handler(response.clone()).await {
1622                tracing::warn!("Response handler error: {}", e);
1623            }
1624        }
1625    }
1626
1627    /// Triggers dialog event (called by BrowserContext when dialog events arrive)
1628    ///
1629    /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
1630    /// This method is public so BrowserContext can forward dialog events.
1631    pub async fn trigger_dialog_event(&self, dialog: Dialog) {
1632        self.on_dialog_event(dialog).await;
1633    }
1634
1635    /// Triggers request event (called by BrowserContext when request events arrive)
1636    pub(crate) async fn trigger_request_event(&self, request: Request) {
1637        self.on_request_event(request).await;
1638    }
1639
1640    pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
1641        self.on_request_finished_event(request).await;
1642    }
1643
1644    pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
1645        self.on_request_failed_event(request).await;
1646    }
1647
1648    /// Triggers response event (called by BrowserContext when response events arrive)
1649    pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
1650        self.on_response_event(response).await;
1651    }
1652
1653    /// Adds a `<style>` tag into the page with the desired content.
1654    ///
1655    /// # Arguments
1656    ///
1657    /// * `options` - Style tag options (content, url, or path)
1658    ///
1659    /// # Returns
1660    ///
1661    /// Returns an ElementHandle pointing to the injected `<style>` tag
1662    ///
1663    /// # Example
1664    ///
1665    /// ```no_run
1666    /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
1667    /// # #[tokio::main]
1668    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1669    /// # let playwright = Playwright::launch().await?;
1670    /// # let browser = playwright.chromium().launch().await?;
1671    /// # let context = browser.new_context().await?;
1672    /// # let page = context.new_page().await?;
1673    /// use playwright_rs::protocol::AddStyleTagOptions;
1674    ///
1675    /// // With inline CSS
1676    /// page.add_style_tag(
1677    ///     AddStyleTagOptions::builder()
1678    ///         .content("body { background-color: red; }")
1679    ///         .build()
1680    /// ).await?;
1681    ///
1682    /// // With external URL
1683    /// page.add_style_tag(
1684    ///     AddStyleTagOptions::builder()
1685    ///         .url("https://example.com/style.css")
1686    ///         .build()
1687    /// ).await?;
1688    ///
1689    /// // From file
1690    /// page.add_style_tag(
1691    ///     AddStyleTagOptions::builder()
1692    ///         .path("./styles/custom.css")
1693    ///         .build()
1694    /// ).await?;
1695    /// # Ok(())
1696    /// # }
1697    /// ```
1698    ///
1699    /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1700    pub async fn add_style_tag(
1701        &self,
1702        options: AddStyleTagOptions,
1703    ) -> Result<Arc<crate::protocol::ElementHandle>> {
1704        let frame = self.main_frame().await?;
1705        frame.add_style_tag(options).await
1706    }
1707
1708    /// Adds a script which would be evaluated in one of the following scenarios:
1709    /// - Whenever the page is navigated
1710    /// - Whenever a child frame is attached or navigated
1711    ///
1712    /// The script is evaluated after the document was created but before any of its scripts were run.
1713    ///
1714    /// # Arguments
1715    ///
1716    /// * `script` - JavaScript code to be injected into the page
1717    ///
1718    /// # Example
1719    ///
1720    /// ```no_run
1721    /// # use playwright_rs::protocol::Playwright;
1722    /// # #[tokio::main]
1723    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1724    /// # let playwright = Playwright::launch().await?;
1725    /// # let browser = playwright.chromium().launch().await?;
1726    /// # let context = browser.new_context().await?;
1727    /// # let page = context.new_page().await?;
1728    /// page.add_init_script("window.injected = 123;").await?;
1729    /// # Ok(())
1730    /// # }
1731    /// ```
1732    ///
1733    /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
1734    pub async fn add_init_script(&self, script: &str) -> Result<()> {
1735        self.channel()
1736            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
1737            .await
1738    }
1739
1740    /// Sets the viewport size for the page.
1741    ///
1742    /// This method allows dynamic resizing of the viewport after page creation,
1743    /// useful for testing responsive layouts at different screen sizes.
1744    ///
1745    /// # Arguments
1746    ///
1747    /// * `viewport` - The viewport dimensions (width and height in pixels)
1748    ///
1749    /// # Example
1750    ///
1751    /// ```no_run
1752    /// # use playwright_rs::protocol::{Playwright, Viewport};
1753    /// # #[tokio::main]
1754    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1755    /// # let playwright = Playwright::launch().await?;
1756    /// # let browser = playwright.chromium().launch().await?;
1757    /// # let page = browser.new_page().await?;
1758    /// // Set viewport to mobile size
1759    /// let mobile = Viewport {
1760    ///     width: 375,
1761    ///     height: 667,
1762    /// };
1763    /// page.set_viewport_size(mobile).await?;
1764    ///
1765    /// // Later, test desktop layout
1766    /// let desktop = Viewport {
1767    ///     width: 1920,
1768    ///     height: 1080,
1769    /// };
1770    /// page.set_viewport_size(desktop).await?;
1771    /// # Ok(())
1772    /// # }
1773    /// ```
1774    ///
1775    /// # Errors
1776    ///
1777    /// Returns error if:
1778    /// - Page has been closed
1779    /// - Communication with browser process fails
1780    ///
1781    /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
1782    pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
1783        // Store the new viewport locally so viewport_size() can reflect the change
1784        if let Ok(mut guard) = self.viewport.write() {
1785            *guard = Some(viewport.clone());
1786        }
1787        self.channel()
1788            .send_no_result(
1789                "setViewportSize",
1790                serde_json::json!({ "viewportSize": viewport }),
1791            )
1792            .await
1793    }
1794
1795    /// Brings this page to the front (activates the tab).
1796    ///
1797    /// Activates the page in the browser, making it the focused tab. This is
1798    /// useful in multi-page tests to ensure actions target the correct page.
1799    ///
1800    /// # Errors
1801    ///
1802    /// Returns error if:
1803    /// - Page has been closed
1804    /// - Communication with browser process fails
1805    ///
1806    /// See: <https://playwright.dev/docs/api/class-page#page-bring-to-front>
1807    pub async fn bring_to_front(&self) -> Result<()> {
1808        self.channel()
1809            .send_no_result("bringToFront", serde_json::json!({}))
1810            .await
1811    }
1812
1813    /// Sets extra HTTP headers that will be sent with every request from this page.
1814    ///
1815    /// These headers are sent in addition to headers set on the browser context via
1816    /// `BrowserContext::set_extra_http_headers()`. Page-level headers take precedence
1817    /// over context-level headers when names conflict.
1818    ///
1819    /// # Arguments
1820    ///
1821    /// * `headers` - Map of header names to values.
1822    ///
1823    /// # Errors
1824    ///
1825    /// Returns error if:
1826    /// - Page has been closed
1827    /// - Communication with browser process fails
1828    ///
1829    /// See: <https://playwright.dev/docs/api/class-page#page-set-extra-http-headers>
1830    pub async fn set_extra_http_headers(
1831        &self,
1832        headers: std::collections::HashMap<String, String>,
1833    ) -> Result<()> {
1834        // Playwright protocol expects an array of {name, value} objects
1835        // This RPC is sent on the Page channel (not the Frame channel)
1836        let headers_array: Vec<serde_json::Value> = headers
1837            .into_iter()
1838            .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
1839            .collect();
1840        self.channel()
1841            .send_no_result(
1842                "setExtraHTTPHeaders",
1843                serde_json::json!({ "headers": headers_array }),
1844            )
1845            .await
1846    }
1847
1848    /// Emulates media features for the page.
1849    ///
1850    /// This method allows emulating CSS media features such as `media`, `color-scheme`,
1851    /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
1852    ///
1853    /// To reset a specific feature to the browser default, use the `NoOverride` variant.
1854    ///
1855    /// # Arguments
1856    ///
1857    /// * `options` - Optional emulation options. If `None`, this is a no-op.
1858    ///
1859    /// # Example
1860    ///
1861    /// ```no_run
1862    /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
1863    /// # #[tokio::main]
1864    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1865    /// # let playwright = Playwright::launch().await?;
1866    /// # let browser = playwright.chromium().launch().await?;
1867    /// # let page = browser.new_page().await?;
1868    /// // Emulate print media
1869    /// page.emulate_media(Some(
1870    ///     EmulateMediaOptions::builder()
1871    ///         .media(Media::Print)
1872    ///         .build()
1873    /// )).await?;
1874    ///
1875    /// // Emulate dark color scheme
1876    /// page.emulate_media(Some(
1877    ///     EmulateMediaOptions::builder()
1878    ///         .color_scheme(ColorScheme::Dark)
1879    ///         .build()
1880    /// )).await?;
1881    /// # Ok(())
1882    /// # }
1883    /// ```
1884    ///
1885    /// # Errors
1886    ///
1887    /// Returns error if:
1888    /// - Page has been closed
1889    /// - Communication with browser process fails
1890    ///
1891    /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
1892    pub async fn emulate_media(&self, options: Option<EmulateMediaOptions>) -> Result<()> {
1893        let mut params = serde_json::json!({});
1894
1895        if let Some(opts) = options {
1896            if let Some(media) = opts.media {
1897                params["media"] = serde_json::to_value(media).map_err(|e| {
1898                    crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
1899                })?;
1900            }
1901            if let Some(color_scheme) = opts.color_scheme {
1902                params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
1903                    crate::error::Error::ProtocolError(format!(
1904                        "Failed to serialize colorScheme: {}",
1905                        e
1906                    ))
1907                })?;
1908            }
1909            if let Some(reduced_motion) = opts.reduced_motion {
1910                params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
1911                    crate::error::Error::ProtocolError(format!(
1912                        "Failed to serialize reducedMotion: {}",
1913                        e
1914                    ))
1915                })?;
1916            }
1917            if let Some(forced_colors) = opts.forced_colors {
1918                params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
1919                    crate::error::Error::ProtocolError(format!(
1920                        "Failed to serialize forcedColors: {}",
1921                        e
1922                    ))
1923                })?;
1924            }
1925        }
1926
1927        self.channel().send_no_result("emulateMedia", params).await
1928    }
1929
1930    /// Generates a PDF of the page and returns it as bytes.
1931    ///
1932    /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
1933    /// not supported in Firefox or WebKit.
1934    ///
1935    /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
1936    /// saved to that file.
1937    ///
1938    /// # Arguments
1939    ///
1940    /// * `options` - Optional PDF generation options
1941    ///
1942    /// # Example
1943    ///
1944    /// ```no_run
1945    /// # use playwright_rs::protocol::Playwright;
1946    /// # #[tokio::main]
1947    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1948    /// # let playwright = Playwright::launch().await?;
1949    /// # let browser = playwright.chromium().launch().await?;
1950    /// # let page = browser.new_page().await?;
1951    /// let pdf_bytes = page.pdf(None).await?;
1952    /// assert!(!pdf_bytes.is_empty());
1953    /// # Ok(())
1954    /// # }
1955    /// ```
1956    ///
1957    /// # Errors
1958    ///
1959    /// Returns error if:
1960    /// - The browser is not Chromium (PDF only supported in Chromium)
1961    /// - Page has been closed
1962    /// - Communication with browser process fails
1963    ///
1964    /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
1965    pub async fn pdf(&self, options: Option<PdfOptions>) -> Result<Vec<u8>> {
1966        let mut params = serde_json::json!({});
1967        let mut save_path: Option<std::path::PathBuf> = None;
1968
1969        if let Some(opts) = options {
1970            // Capture the file path before consuming opts
1971            save_path = opts.path;
1972
1973            if let Some(scale) = opts.scale {
1974                params["scale"] = serde_json::json!(scale);
1975            }
1976            if let Some(v) = opts.display_header_footer {
1977                params["displayHeaderFooter"] = serde_json::json!(v);
1978            }
1979            if let Some(v) = opts.header_template {
1980                params["headerTemplate"] = serde_json::json!(v);
1981            }
1982            if let Some(v) = opts.footer_template {
1983                params["footerTemplate"] = serde_json::json!(v);
1984            }
1985            if let Some(v) = opts.print_background {
1986                params["printBackground"] = serde_json::json!(v);
1987            }
1988            if let Some(v) = opts.landscape {
1989                params["landscape"] = serde_json::json!(v);
1990            }
1991            if let Some(v) = opts.page_ranges {
1992                params["pageRanges"] = serde_json::json!(v);
1993            }
1994            if let Some(v) = opts.format {
1995                params["format"] = serde_json::json!(v);
1996            }
1997            if let Some(v) = opts.width {
1998                params["width"] = serde_json::json!(v);
1999            }
2000            if let Some(v) = opts.height {
2001                params["height"] = serde_json::json!(v);
2002            }
2003            if let Some(v) = opts.prefer_css_page_size {
2004                params["preferCSSPageSize"] = serde_json::json!(v);
2005            }
2006            if let Some(margin) = opts.margin {
2007                params["margin"] = serde_json::to_value(margin).map_err(|e| {
2008                    crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
2009                })?;
2010            }
2011        }
2012
2013        #[derive(Deserialize)]
2014        struct PdfResponse {
2015            pdf: String,
2016        }
2017
2018        let response: PdfResponse = self.channel().send("pdf", params).await?;
2019
2020        // Decode base64 to bytes
2021        let pdf_bytes = base64::engine::general_purpose::STANDARD
2022            .decode(&response.pdf)
2023            .map_err(|e| {
2024                crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
2025            })?;
2026
2027        // If a path was specified, save the PDF to disk as well
2028        if let Some(path) = save_path {
2029            tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
2030                crate::error::Error::InvalidArgument(format!(
2031                    "Failed to write PDF to '{}': {}",
2032                    path.display(),
2033                    e
2034                ))
2035            })?;
2036        }
2037
2038        Ok(pdf_bytes)
2039    }
2040
2041    /// Adds a `<script>` tag into the page with the desired URL or content.
2042    ///
2043    /// # Arguments
2044    ///
2045    /// * `options` - Optional script tag options (content, url, or path).
2046    ///   If `None`, returns an error because no source is specified.
2047    ///
2048    /// At least one of `content`, `url`, or `path` must be provided.
2049    ///
2050    /// # Example
2051    ///
2052    /// ```no_run
2053    /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
2054    /// # #[tokio::main]
2055    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2056    /// # let playwright = Playwright::launch().await?;
2057    /// # let browser = playwright.chromium().launch().await?;
2058    /// # let context = browser.new_context().await?;
2059    /// # let page = context.new_page().await?;
2060    /// // With inline JavaScript
2061    /// page.add_script_tag(Some(
2062    ///     AddScriptTagOptions::builder()
2063    ///         .content("window.myVar = 42;")
2064    ///         .build()
2065    /// )).await?;
2066    ///
2067    /// // With external URL
2068    /// page.add_script_tag(Some(
2069    ///     AddScriptTagOptions::builder()
2070    ///         .url("https://example.com/script.js")
2071    ///         .build()
2072    /// )).await?;
2073    /// # Ok(())
2074    /// # }
2075    /// ```
2076    ///
2077    /// # Errors
2078    ///
2079    /// Returns error if:
2080    /// - `options` is `None` or no content/url/path is specified
2081    /// - Page has been closed
2082    /// - Script loading fails (e.g., invalid URL)
2083    ///
2084    /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
2085    pub async fn add_script_tag(
2086        &self,
2087        options: Option<AddScriptTagOptions>,
2088    ) -> Result<Arc<crate::protocol::ElementHandle>> {
2089        let opts = options.ok_or_else(|| {
2090            Error::InvalidArgument(
2091                "At least one of content, url, or path must be specified".to_string(),
2092            )
2093        })?;
2094        let frame = self.main_frame().await?;
2095        frame.add_script_tag(opts).await
2096    }
2097
2098    /// Returns the current viewport size of the page, or `None` if no viewport is set.
2099    ///
2100    /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
2101    /// returns the dimensions configured at context creation time or updated via
2102    /// `set_viewport_size()`.
2103    ///
2104    /// # Example
2105    ///
2106    /// ```ignore
2107    /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
2108    /// # #[tokio::main]
2109    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2110    /// # let playwright = Playwright::launch().await?;
2111    /// # let browser = playwright.chromium().launch().await?;
2112    /// let context = browser.new_context_with_options(
2113    ///     BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
2114    /// ).await?;
2115    /// let page = context.new_page().await?;
2116    /// let size = page.viewport_size().expect("Viewport should be set");
2117    /// assert_eq!(size.width, 1280);
2118    /// assert_eq!(size.height, 720);
2119    /// # Ok(())
2120    /// # }
2121    /// ```
2122    ///
2123    /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
2124    pub fn viewport_size(&self) -> Option<Viewport> {
2125        self.viewport.read().ok()?.clone()
2126    }
2127}
2128
2129impl ChannelOwner for Page {
2130    fn guid(&self) -> &str {
2131        self.base.guid()
2132    }
2133
2134    fn type_name(&self) -> &str {
2135        self.base.type_name()
2136    }
2137
2138    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2139        self.base.parent()
2140    }
2141
2142    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2143        self.base.connection()
2144    }
2145
2146    fn initializer(&self) -> &Value {
2147        self.base.initializer()
2148    }
2149
2150    fn channel(&self) -> &Channel {
2151        self.base.channel()
2152    }
2153
2154    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2155        self.base.dispose(reason)
2156    }
2157
2158    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2159        self.base.adopt(child)
2160    }
2161
2162    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2163        self.base.add_child(guid, child)
2164    }
2165
2166    fn remove_child(&self, guid: &str) {
2167        self.base.remove_child(guid)
2168    }
2169
2170    fn on_event(&self, method: &str, params: Value) {
2171        match method {
2172            "navigated" => {
2173                // Update URL when page navigates
2174                if let Some(url_value) = params.get("url")
2175                    && let Some(url_str) = url_value.as_str()
2176                    && let Ok(mut url) = self.url.write()
2177                {
2178                    *url = url_str.to_string();
2179                }
2180            }
2181            "route" => {
2182                // Handle network routing event
2183                if let Some(route_guid) = params
2184                    .get("route")
2185                    .and_then(|v| v.get("guid"))
2186                    .and_then(|v| v.as_str())
2187                {
2188                    // Get the Route object from connection's registry
2189                    let connection = self.connection();
2190                    let route_guid_owned = route_guid.to_string();
2191                    let self_clone = self.clone();
2192
2193                    tokio::spawn(async move {
2194                        // Get and downcast Route object
2195                        let route: Route =
2196                            match connection.get_typed::<Route>(&route_guid_owned).await {
2197                                Ok(r) => r,
2198                                Err(e) => {
2199                                    tracing::warn!("Failed to get route object: {}", e);
2200                                    return;
2201                                }
2202                            };
2203
2204                        // Set APIRequestContext on the route for fetch() support.
2205                        // Page's parent is BrowserContext, which has the request context.
2206                        if let Some(ctx) =
2207                            downcast_parent::<crate::protocol::BrowserContext>(&self_clone)
2208                            && let Ok(api_ctx) = ctx.request().await
2209                        {
2210                            route.set_api_request_context(api_ctx);
2211                        }
2212
2213                        // Call the route handler and wait for completion
2214                        self_clone.on_route_event(route).await;
2215                    });
2216                }
2217            }
2218            "download" => {
2219                // Handle download event
2220                // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
2221                let url = params
2222                    .get("url")
2223                    .and_then(|v| v.as_str())
2224                    .unwrap_or("")
2225                    .to_string();
2226
2227                let suggested_filename = params
2228                    .get("suggestedFilename")
2229                    .and_then(|v| v.as_str())
2230                    .unwrap_or("")
2231                    .to_string();
2232
2233                if let Some(artifact_guid) = params
2234                    .get("artifact")
2235                    .and_then(|v| v.get("guid"))
2236                    .and_then(|v| v.as_str())
2237                {
2238                    let connection = self.connection();
2239                    let artifact_guid_owned = artifact_guid.to_string();
2240                    let self_clone = self.clone();
2241
2242                    tokio::spawn(async move {
2243                        // Wait for Artifact object to be created
2244                        let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
2245                            Ok(obj) => obj,
2246                            Err(e) => {
2247                                tracing::warn!("Failed to get artifact object: {}", e);
2248                                return;
2249                            }
2250                        };
2251
2252                        // Create Download wrapper from Artifact + event params
2253                        let download = Download::from_artifact(
2254                            artifact_arc,
2255                            url,
2256                            suggested_filename,
2257                            self_clone.clone(),
2258                        );
2259
2260                        // Call the download handlers
2261                        self_clone.on_download_event(download).await;
2262                    });
2263                }
2264            }
2265            "dialog" => {
2266                // Dialog events are handled by BrowserContext and forwarded to Page
2267                // This case should not be reached, but keeping for completeness
2268            }
2269            "webSocket" => {
2270                if let Some(ws_guid) = params
2271                    .get("webSocket")
2272                    .and_then(|v| v.get("guid"))
2273                    .and_then(|v| v.as_str())
2274                {
2275                    let connection = self.connection();
2276                    let ws_guid_owned = ws_guid.to_string();
2277                    let self_clone = self.clone();
2278
2279                    tokio::spawn(async move {
2280                        // Get and downcast WebSocket object
2281                        let ws: WebSocket =
2282                            match connection.get_typed::<WebSocket>(&ws_guid_owned).await {
2283                                Ok(ws) => ws,
2284                                Err(e) => {
2285                                    tracing::warn!("Failed to get WebSocket object: {}", e);
2286                                    return;
2287                                }
2288                            };
2289
2290                        // Call handlers
2291                        let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
2292                        for handler in handlers {
2293                            let ws_clone = ws.clone();
2294                            tokio::spawn(async move {
2295                                if let Err(e) = handler(ws_clone).await {
2296                                    tracing::error!("Error in websocket handler: {}", e);
2297                                }
2298                            });
2299                        }
2300                    });
2301                }
2302            }
2303            "bindingCall" => {
2304                // A JS caller on this page invoked a page-level exposed function.
2305                // Event format: {binding: {guid: "..."}}
2306                if let Some(binding_guid) = params
2307                    .get("binding")
2308                    .and_then(|v| v.get("guid"))
2309                    .and_then(|v| v.as_str())
2310                {
2311                    let connection = self.connection();
2312                    let binding_guid_owned = binding_guid.to_string();
2313                    let binding_callbacks = self.binding_callbacks.clone();
2314
2315                    tokio::spawn(async move {
2316                        let binding_call: crate::protocol::BindingCall = match connection
2317                            .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
2318                            .await
2319                        {
2320                            Ok(bc) => bc,
2321                            Err(e) => {
2322                                tracing::warn!("Failed to get BindingCall object: {}", e);
2323                                return;
2324                            }
2325                        };
2326
2327                        let name = binding_call.name().to_string();
2328
2329                        // Look up page-level callback
2330                        let callback = {
2331                            let callbacks = binding_callbacks.lock().unwrap();
2332                            callbacks.get(&name).cloned()
2333                        };
2334
2335                        let Some(callback) = callback else {
2336                            // No page-level handler — the context-level handler on
2337                            // BrowserContext::on_event("bindingCall") will handle it.
2338                            return;
2339                        };
2340
2341                        // Deserialize args from Playwright protocol format
2342                        let raw_args = binding_call.args();
2343                        let args = crate::protocol::browser_context::BrowserContext::deserialize_binding_args_pub(raw_args);
2344
2345                        // Call callback and serialize result
2346                        let result_value = callback(args).await;
2347                        let serialized =
2348                            crate::protocol::evaluate_conversion::serialize_argument(&result_value);
2349
2350                        if let Err(e) = binding_call.resolve(serialized).await {
2351                            tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
2352                        }
2353                    });
2354                }
2355            }
2356            "close" => {
2357                // Server-initiated close (e.g. context was closed)
2358                self.is_closed.store(true, Ordering::Relaxed);
2359            }
2360            _ => {
2361                // Other events will be handled in future phases
2362                // Events: load, domcontentloaded, crash, etc.
2363            }
2364        }
2365    }
2366
2367    fn was_collected(&self) -> bool {
2368        self.base.was_collected()
2369    }
2370
2371    fn as_any(&self) -> &dyn Any {
2372        self
2373    }
2374}
2375
2376impl std::fmt::Debug for Page {
2377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2378        f.debug_struct("Page")
2379            .field("guid", &self.guid())
2380            .field("url", &self.url())
2381            .finish()
2382    }
2383}
2384
2385/// Options for page.goto() and page.reload()
2386#[derive(Debug, Clone)]
2387pub struct GotoOptions {
2388    /// Maximum operation time in milliseconds
2389    pub timeout: Option<std::time::Duration>,
2390    /// When to consider operation succeeded
2391    pub wait_until: Option<WaitUntil>,
2392}
2393
2394impl GotoOptions {
2395    /// Creates new GotoOptions with default values
2396    pub fn new() -> Self {
2397        Self {
2398            timeout: None,
2399            wait_until: None,
2400        }
2401    }
2402
2403    /// Sets the timeout
2404    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
2405        self.timeout = Some(timeout);
2406        self
2407    }
2408
2409    /// Sets the wait_until option
2410    pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
2411        self.wait_until = Some(wait_until);
2412        self
2413    }
2414}
2415
2416impl Default for GotoOptions {
2417    fn default() -> Self {
2418        Self::new()
2419    }
2420}
2421
2422/// When to consider navigation succeeded
2423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2424pub enum WaitUntil {
2425    /// Consider operation to be finished when the `load` event is fired
2426    Load,
2427    /// Consider operation to be finished when the `DOMContentLoaded` event is fired
2428    DomContentLoaded,
2429    /// Consider operation to be finished when there are no network connections for at least 500ms
2430    NetworkIdle,
2431    /// Consider operation to be finished when the commit event is fired
2432    Commit,
2433}
2434
2435impl WaitUntil {
2436    pub(crate) fn as_str(&self) -> &'static str {
2437        match self {
2438            WaitUntil::Load => "load",
2439            WaitUntil::DomContentLoaded => "domcontentloaded",
2440            WaitUntil::NetworkIdle => "networkidle",
2441            WaitUntil::Commit => "commit",
2442        }
2443    }
2444}
2445
2446/// Options for adding a style tag to the page
2447///
2448/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
2449#[derive(Debug, Clone, Default)]
2450pub struct AddStyleTagOptions {
2451    /// Raw CSS content to inject
2452    pub content: Option<String>,
2453    /// URL of the `<link>` tag to add
2454    pub url: Option<String>,
2455    /// Path to a CSS file to inject
2456    pub path: Option<String>,
2457}
2458
2459impl AddStyleTagOptions {
2460    /// Creates a new builder for AddStyleTagOptions
2461    pub fn builder() -> AddStyleTagOptionsBuilder {
2462        AddStyleTagOptionsBuilder::default()
2463    }
2464
2465    /// Validates that at least one option is specified
2466    pub(crate) fn validate(&self) -> Result<()> {
2467        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
2468            return Err(Error::InvalidArgument(
2469                "At least one of content, url, or path must be specified".to_string(),
2470            ));
2471        }
2472        Ok(())
2473    }
2474}
2475
2476/// Builder for AddStyleTagOptions
2477#[derive(Debug, Clone, Default)]
2478pub struct AddStyleTagOptionsBuilder {
2479    content: Option<String>,
2480    url: Option<String>,
2481    path: Option<String>,
2482}
2483
2484impl AddStyleTagOptionsBuilder {
2485    /// Sets the CSS content to inject
2486    pub fn content(mut self, content: impl Into<String>) -> Self {
2487        self.content = Some(content.into());
2488        self
2489    }
2490
2491    /// Sets the URL of the stylesheet
2492    pub fn url(mut self, url: impl Into<String>) -> Self {
2493        self.url = Some(url.into());
2494        self
2495    }
2496
2497    /// Sets the path to a CSS file
2498    pub fn path(mut self, path: impl Into<String>) -> Self {
2499        self.path = Some(path.into());
2500        self
2501    }
2502
2503    /// Builds the AddStyleTagOptions
2504    pub fn build(self) -> AddStyleTagOptions {
2505        AddStyleTagOptions {
2506            content: self.content,
2507            url: self.url,
2508            path: self.path,
2509        }
2510    }
2511}
2512
2513// ============================================================================
2514// AddScriptTagOptions
2515// ============================================================================
2516
2517/// Options for adding a `<script>` tag to the page.
2518///
2519/// At least one of `content`, `url`, or `path` must be specified.
2520///
2521/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
2522#[derive(Debug, Clone, Default)]
2523pub struct AddScriptTagOptions {
2524    /// Raw JavaScript content to inject
2525    pub content: Option<String>,
2526    /// URL of the `<script>` tag to add
2527    pub url: Option<String>,
2528    /// Path to a JavaScript file to inject (file contents will be read and sent as content)
2529    pub path: Option<String>,
2530    /// Script type attribute (e.g., `"module"`)
2531    pub type_: Option<String>,
2532}
2533
2534impl AddScriptTagOptions {
2535    /// Creates a new builder for AddScriptTagOptions
2536    pub fn builder() -> AddScriptTagOptionsBuilder {
2537        AddScriptTagOptionsBuilder::default()
2538    }
2539
2540    /// Validates that at least one option is specified
2541    pub(crate) fn validate(&self) -> Result<()> {
2542        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
2543            return Err(Error::InvalidArgument(
2544                "At least one of content, url, or path must be specified".to_string(),
2545            ));
2546        }
2547        Ok(())
2548    }
2549}
2550
2551/// Builder for AddScriptTagOptions
2552#[derive(Debug, Clone, Default)]
2553pub struct AddScriptTagOptionsBuilder {
2554    content: Option<String>,
2555    url: Option<String>,
2556    path: Option<String>,
2557    type_: Option<String>,
2558}
2559
2560impl AddScriptTagOptionsBuilder {
2561    /// Sets the JavaScript content to inject
2562    pub fn content(mut self, content: impl Into<String>) -> Self {
2563        self.content = Some(content.into());
2564        self
2565    }
2566
2567    /// Sets the URL of the script to load
2568    pub fn url(mut self, url: impl Into<String>) -> Self {
2569        self.url = Some(url.into());
2570        self
2571    }
2572
2573    /// Sets the path to a JavaScript file to inject
2574    pub fn path(mut self, path: impl Into<String>) -> Self {
2575        self.path = Some(path.into());
2576        self
2577    }
2578
2579    /// Sets the script type attribute (e.g., `"module"`)
2580    pub fn type_(mut self, type_: impl Into<String>) -> Self {
2581        self.type_ = Some(type_.into());
2582        self
2583    }
2584
2585    /// Builds the AddScriptTagOptions
2586    pub fn build(self) -> AddScriptTagOptions {
2587        AddScriptTagOptions {
2588            content: self.content,
2589            url: self.url,
2590            path: self.path,
2591            type_: self.type_,
2592        }
2593    }
2594}
2595
2596// ============================================================================
2597// EmulateMediaOptions and related enums
2598// ============================================================================
2599
2600/// Media type for `page.emulate_media()`.
2601///
2602/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2603#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2604#[serde(rename_all = "lowercase")]
2605pub enum Media {
2606    /// Emulate screen media type
2607    Screen,
2608    /// Emulate print media type
2609    Print,
2610    /// Reset media emulation to browser default (sends `"no-override"` to protocol)
2611    #[serde(rename = "no-override")]
2612    NoOverride,
2613}
2614
2615/// Preferred color scheme for `page.emulate_media()`.
2616///
2617/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2618#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2619pub enum ColorScheme {
2620    /// Emulate light color scheme
2621    #[serde(rename = "light")]
2622    Light,
2623    /// Emulate dark color scheme
2624    #[serde(rename = "dark")]
2625    Dark,
2626    /// Emulate no preference for color scheme
2627    #[serde(rename = "no-preference")]
2628    NoPreference,
2629    /// Reset color scheme to browser default
2630    #[serde(rename = "no-override")]
2631    NoOverride,
2632}
2633
2634/// Reduced motion preference for `page.emulate_media()`.
2635///
2636/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2637#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2638pub enum ReducedMotion {
2639    /// Emulate reduced motion preference
2640    #[serde(rename = "reduce")]
2641    Reduce,
2642    /// Emulate no preference for reduced motion
2643    #[serde(rename = "no-preference")]
2644    NoPreference,
2645    /// Reset reduced motion to browser default
2646    #[serde(rename = "no-override")]
2647    NoOverride,
2648}
2649
2650/// Forced colors preference for `page.emulate_media()`.
2651///
2652/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2653#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2654pub enum ForcedColors {
2655    /// Emulate active forced colors
2656    #[serde(rename = "active")]
2657    Active,
2658    /// Emulate no forced colors
2659    #[serde(rename = "none")]
2660    None_,
2661    /// Reset forced colors to browser default
2662    #[serde(rename = "no-override")]
2663    NoOverride,
2664}
2665
2666/// Options for `page.emulate_media()`.
2667///
2668/// All fields are optional. Fields that are `None` are omitted from the protocol
2669/// message (meaning they are not changed). To reset a field to browser default,
2670/// use the `NoOverride` variant.
2671///
2672/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2673#[derive(Debug, Clone, Default)]
2674pub struct EmulateMediaOptions {
2675    /// Media type to emulate (screen, print, or no-override)
2676    pub media: Option<Media>,
2677    /// Color scheme preference to emulate
2678    pub color_scheme: Option<ColorScheme>,
2679    /// Reduced motion preference to emulate
2680    pub reduced_motion: Option<ReducedMotion>,
2681    /// Forced colors preference to emulate
2682    pub forced_colors: Option<ForcedColors>,
2683}
2684
2685impl EmulateMediaOptions {
2686    /// Creates a new builder for EmulateMediaOptions
2687    pub fn builder() -> EmulateMediaOptionsBuilder {
2688        EmulateMediaOptionsBuilder::default()
2689    }
2690}
2691
2692/// Builder for EmulateMediaOptions
2693#[derive(Debug, Clone, Default)]
2694pub struct EmulateMediaOptionsBuilder {
2695    media: Option<Media>,
2696    color_scheme: Option<ColorScheme>,
2697    reduced_motion: Option<ReducedMotion>,
2698    forced_colors: Option<ForcedColors>,
2699}
2700
2701impl EmulateMediaOptionsBuilder {
2702    /// Sets the media type to emulate
2703    pub fn media(mut self, media: Media) -> Self {
2704        self.media = Some(media);
2705        self
2706    }
2707
2708    /// Sets the color scheme preference
2709    pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
2710        self.color_scheme = Some(color_scheme);
2711        self
2712    }
2713
2714    /// Sets the reduced motion preference
2715    pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
2716        self.reduced_motion = Some(reduced_motion);
2717        self
2718    }
2719
2720    /// Sets the forced colors preference
2721    pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
2722        self.forced_colors = Some(forced_colors);
2723        self
2724    }
2725
2726    /// Builds the EmulateMediaOptions
2727    pub fn build(self) -> EmulateMediaOptions {
2728        EmulateMediaOptions {
2729            media: self.media,
2730            color_scheme: self.color_scheme,
2731            reduced_motion: self.reduced_motion,
2732            forced_colors: self.forced_colors,
2733        }
2734    }
2735}
2736
2737// ============================================================================
2738// PdfOptions
2739// ============================================================================
2740
2741/// Margin options for PDF generation.
2742///
2743/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2744#[derive(Debug, Clone, Default, Serialize)]
2745pub struct PdfMargin {
2746    /// Top margin (e.g. `"1in"`)
2747    #[serde(skip_serializing_if = "Option::is_none")]
2748    pub top: Option<String>,
2749    /// Right margin
2750    #[serde(skip_serializing_if = "Option::is_none")]
2751    pub right: Option<String>,
2752    /// Bottom margin
2753    #[serde(skip_serializing_if = "Option::is_none")]
2754    pub bottom: Option<String>,
2755    /// Left margin
2756    #[serde(skip_serializing_if = "Option::is_none")]
2757    pub left: Option<String>,
2758}
2759
2760/// Options for generating a PDF from a page.
2761///
2762/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
2763/// Firefox or WebKit will result in an error.
2764///
2765/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2766#[derive(Debug, Clone, Default)]
2767pub struct PdfOptions {
2768    /// If specified, the PDF will also be saved to this file path.
2769    pub path: Option<std::path::PathBuf>,
2770    /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
2771    pub scale: Option<f64>,
2772    /// Whether to display header and footer (default false).
2773    pub display_header_footer: Option<bool>,
2774    /// HTML template for the print header. Should be valid HTML.
2775    pub header_template: Option<String>,
2776    /// HTML template for the print footer.
2777    pub footer_template: Option<String>,
2778    /// Whether to print background graphics (default false).
2779    pub print_background: Option<bool>,
2780    /// Paper orientation — `true` for landscape (default false).
2781    pub landscape: Option<bool>,
2782    /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
2783    pub page_ranges: Option<String>,
2784    /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
2785    pub format: Option<String>,
2786    /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
2787    pub width: Option<String>,
2788    /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
2789    pub height: Option<String>,
2790    /// Whether or not to prefer page size as defined by CSS.
2791    pub prefer_css_page_size: Option<bool>,
2792    /// Paper margins, defaulting to none.
2793    pub margin: Option<PdfMargin>,
2794}
2795
2796impl PdfOptions {
2797    /// Creates a new builder for PdfOptions
2798    pub fn builder() -> PdfOptionsBuilder {
2799        PdfOptionsBuilder::default()
2800    }
2801}
2802
2803/// Builder for PdfOptions
2804#[derive(Debug, Clone, Default)]
2805pub struct PdfOptionsBuilder {
2806    path: Option<std::path::PathBuf>,
2807    scale: Option<f64>,
2808    display_header_footer: Option<bool>,
2809    header_template: Option<String>,
2810    footer_template: Option<String>,
2811    print_background: Option<bool>,
2812    landscape: Option<bool>,
2813    page_ranges: Option<String>,
2814    format: Option<String>,
2815    width: Option<String>,
2816    height: Option<String>,
2817    prefer_css_page_size: Option<bool>,
2818    margin: Option<PdfMargin>,
2819}
2820
2821impl PdfOptionsBuilder {
2822    /// Sets the file path for saving the PDF
2823    pub fn path(mut self, path: std::path::PathBuf) -> Self {
2824        self.path = Some(path);
2825        self
2826    }
2827
2828    /// Sets the scale of the webpage rendering
2829    pub fn scale(mut self, scale: f64) -> Self {
2830        self.scale = Some(scale);
2831        self
2832    }
2833
2834    /// Sets whether to display header and footer
2835    pub fn display_header_footer(mut self, display: bool) -> Self {
2836        self.display_header_footer = Some(display);
2837        self
2838    }
2839
2840    /// Sets the HTML template for the print header
2841    pub fn header_template(mut self, template: impl Into<String>) -> Self {
2842        self.header_template = Some(template.into());
2843        self
2844    }
2845
2846    /// Sets the HTML template for the print footer
2847    pub fn footer_template(mut self, template: impl Into<String>) -> Self {
2848        self.footer_template = Some(template.into());
2849        self
2850    }
2851
2852    /// Sets whether to print background graphics
2853    pub fn print_background(mut self, print: bool) -> Self {
2854        self.print_background = Some(print);
2855        self
2856    }
2857
2858    /// Sets whether to use landscape orientation
2859    pub fn landscape(mut self, landscape: bool) -> Self {
2860        self.landscape = Some(landscape);
2861        self
2862    }
2863
2864    /// Sets the page ranges to print
2865    pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
2866        self.page_ranges = Some(ranges.into());
2867        self
2868    }
2869
2870    /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
2871    pub fn format(mut self, format: impl Into<String>) -> Self {
2872        self.format = Some(format.into());
2873        self
2874    }
2875
2876    /// Sets the paper width
2877    pub fn width(mut self, width: impl Into<String>) -> Self {
2878        self.width = Some(width.into());
2879        self
2880    }
2881
2882    /// Sets the paper height
2883    pub fn height(mut self, height: impl Into<String>) -> Self {
2884        self.height = Some(height.into());
2885        self
2886    }
2887
2888    /// Sets whether to prefer page size as defined by CSS
2889    pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
2890        self.prefer_css_page_size = Some(prefer);
2891        self
2892    }
2893
2894    /// Sets the paper margins
2895    pub fn margin(mut self, margin: PdfMargin) -> Self {
2896        self.margin = Some(margin);
2897        self
2898    }
2899
2900    /// Builds the PdfOptions
2901    pub fn build(self) -> PdfOptions {
2902        PdfOptions {
2903            path: self.path,
2904            scale: self.scale,
2905            display_header_footer: self.display_header_footer,
2906            header_template: self.header_template,
2907            footer_template: self.footer_template,
2908            print_background: self.print_background,
2909            landscape: self.landscape,
2910            page_ranges: self.page_ranges,
2911            format: self.format,
2912            width: self.width,
2913            height: self.height,
2914            prefer_css_page_size: self.prefer_css_page_size,
2915            margin: self.margin,
2916        }
2917    }
2918}
2919
2920/// Response from navigation operations.
2921///
2922/// Returned from `page.goto()`, `page.reload()`, `page.go_back()`, and similar
2923/// navigation methods. Provides access to the HTTP response status, headers, and body.
2924///
2925/// See: <https://playwright.dev/docs/api/class-response>
2926#[derive(Clone)]
2927pub struct Response {
2928    url: String,
2929    status: u16,
2930    status_text: String,
2931    ok: bool,
2932    headers: std::collections::HashMap<String, String>,
2933    /// Reference to the backing channel owner for RPC calls (body, rawHeaders, etc.)
2934    /// Stored as the generic trait object so it can be downcast to ResponseObject when needed.
2935    response_channel_owner: Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
2936}
2937
2938impl Response {
2939    /// Creates a new Response from protocol data.
2940    ///
2941    /// This is used internally when constructing a Response from the protocol
2942    /// initializer (e.g., after `goto` or `reload`).
2943    pub(crate) fn new(
2944        url: String,
2945        status: u16,
2946        status_text: String,
2947        headers: std::collections::HashMap<String, String>,
2948        response_channel_owner: Option<
2949            std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>,
2950        >,
2951    ) -> Self {
2952        Self {
2953            url,
2954            status,
2955            status_text,
2956            ok: (200..300).contains(&status),
2957            headers,
2958            response_channel_owner,
2959        }
2960    }
2961}
2962
2963impl Response {
2964    /// Returns the URL of the response.
2965    ///
2966    /// See: <https://playwright.dev/docs/api/class-response#response-url>
2967    pub fn url(&self) -> &str {
2968        &self.url
2969    }
2970
2971    /// Returns the HTTP status code.
2972    ///
2973    /// See: <https://playwright.dev/docs/api/class-response#response-status>
2974    pub fn status(&self) -> u16 {
2975        self.status
2976    }
2977
2978    /// Returns the HTTP status text.
2979    ///
2980    /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
2981    pub fn status_text(&self) -> &str {
2982        &self.status_text
2983    }
2984
2985    /// Returns whether the response was successful (status 200-299).
2986    ///
2987    /// See: <https://playwright.dev/docs/api/class-response#response-ok>
2988    pub fn ok(&self) -> bool {
2989        self.ok
2990    }
2991
2992    /// Returns the response headers as a HashMap.
2993    ///
2994    /// Note: these are the headers from the protocol initializer. For the full
2995    /// raw headers (including duplicates), use `headers_array()` or `all_headers()`.
2996    ///
2997    /// See: <https://playwright.dev/docs/api/class-response#response-headers>
2998    pub fn headers(&self) -> &std::collections::HashMap<String, String> {
2999        &self.headers
3000    }
3001
3002    /// Returns the [`Request`] that triggered this response.
3003    ///
3004    /// Navigates the protocol object hierarchy: ResponseObject → parent (Request).
3005    ///
3006    /// See: <https://playwright.dev/docs/api/class-response#response-request>
3007    pub fn request(&self) -> Option<crate::protocol::Request> {
3008        let owner = self.response_channel_owner.as_ref()?;
3009        downcast_parent::<crate::protocol::Request>(&**owner)
3010    }
3011
3012    /// Returns the [`Frame`](crate::protocol::Frame) that initiated the request for this response.
3013    ///
3014    /// Navigates the protocol object hierarchy: ResponseObject → Request → Frame.
3015    ///
3016    /// See: <https://playwright.dev/docs/api/class-response#response-frame>
3017    pub fn frame(&self) -> Option<crate::protocol::Frame> {
3018        let request = self.request()?;
3019        request.frame()
3020    }
3021
3022    /// Returns the backing `ResponseObject`, or an error if unavailable.
3023    pub(crate) fn response_object(&self) -> crate::error::Result<crate::protocol::ResponseObject> {
3024        let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
3025            crate::error::Error::ProtocolError(
3026                "Response has no backing protocol object".to_string(),
3027            )
3028        })?;
3029        arc.as_any()
3030            .downcast_ref::<crate::protocol::ResponseObject>()
3031            .cloned()
3032            .ok_or_else(|| crate::error::Error::TypeMismatch {
3033                guid: arc.guid().to_string(),
3034                expected: "ResponseObject".to_string(),
3035                actual: arc.type_name().to_string(),
3036            })
3037    }
3038
3039    /// Returns TLS/SSL security details for HTTPS connections, or `None` for HTTP.
3040    ///
3041    /// See: <https://playwright.dev/docs/api/class-response#response-security-details>
3042    pub async fn security_details(
3043        &self,
3044    ) -> crate::error::Result<Option<crate::protocol::response::SecurityDetails>> {
3045        self.response_object()?.security_details().await
3046    }
3047
3048    /// Returns the server's IP address and port, or `None`.
3049    ///
3050    /// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
3051    pub async fn server_addr(
3052        &self,
3053    ) -> crate::error::Result<Option<crate::protocol::response::RemoteAddr>> {
3054        self.response_object()?.server_addr().await
3055    }
3056
3057    /// Waits for this response to finish loading.
3058    ///
3059    /// For responses obtained from navigation methods (`goto`, `reload`), the response
3060    /// is already finished when returned. For responses from `on_response` handlers,
3061    /// the body may still be loading.
3062    ///
3063    /// See: <https://playwright.dev/docs/api/class-response#response-finished>
3064    pub async fn finished(&self) -> crate::error::Result<()> {
3065        // The Playwright protocol dispatches `requestFinished` as a separate event
3066        // rather than exposing a `finished` RPC method on Response.
3067        // For responses from goto/reload, the response is already complete.
3068        // TODO: For on_response handlers, implement proper waiting via requestFinished event.
3069        Ok(())
3070    }
3071
3072    /// Returns the response body as raw bytes.
3073    ///
3074    /// Makes an RPC call to the Playwright server to fetch the response body.
3075    ///
3076    /// # Errors
3077    ///
3078    /// Returns an error if:
3079    /// - No backing protocol object is available (edge case)
3080    /// - The RPC call to the server fails
3081    /// - The base64 response cannot be decoded
3082    ///
3083    /// See: <https://playwright.dev/docs/api/class-response#response-body>
3084    pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
3085        self.response_object()?.body().await
3086    }
3087
3088    /// Returns the response body as a UTF-8 string.
3089    ///
3090    /// Calls `body()` then converts bytes to a UTF-8 string.
3091    ///
3092    /// # Errors
3093    ///
3094    /// Returns an error if:
3095    /// - `body()` fails
3096    /// - The body is not valid UTF-8
3097    ///
3098    /// See: <https://playwright.dev/docs/api/class-response#response-text>
3099    pub async fn text(&self) -> crate::error::Result<String> {
3100        let bytes = self.body().await?;
3101        String::from_utf8(bytes).map_err(|e| {
3102            crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
3103        })
3104    }
3105
3106    /// Parses the response body as JSON and deserializes it into type `T`.
3107    ///
3108    /// Calls `text()` then uses `serde_json` to deserialize the body.
3109    ///
3110    /// # Errors
3111    ///
3112    /// Returns an error if:
3113    /// - `text()` fails
3114    /// - The body is not valid JSON or doesn't match the expected type
3115    ///
3116    /// See: <https://playwright.dev/docs/api/class-response#response-json>
3117    pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
3118        let text = self.text().await?;
3119        serde_json::from_str(&text).map_err(|e| {
3120            crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
3121        })
3122    }
3123
3124    /// Returns all response headers as name-value pairs, preserving duplicates.
3125    ///
3126    /// Makes an RPC call for `"rawHeaders"` which returns the complete header list.
3127    ///
3128    /// # Errors
3129    ///
3130    /// Returns an error if:
3131    /// - No backing protocol object is available (edge case)
3132    /// - The RPC call to the server fails
3133    ///
3134    /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
3135    pub async fn headers_array(
3136        &self,
3137    ) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
3138        self.response_object()?.raw_headers().await
3139    }
3140
3141    /// Returns all response headers merged into a HashMap with lowercase keys.
3142    ///
3143    /// When multiple headers have the same name, their values are joined with `, `.
3144    /// This matches the behavior of `response.allHeaders()` in other Playwright bindings.
3145    ///
3146    /// # Errors
3147    ///
3148    /// Returns an error if:
3149    /// - No backing protocol object is available (edge case)
3150    /// - The RPC call to the server fails
3151    ///
3152    /// See: <https://playwright.dev/docs/api/class-response#response-all-headers>
3153    pub async fn all_headers(
3154        &self,
3155    ) -> crate::error::Result<std::collections::HashMap<String, String>> {
3156        let entries = self.headers_array().await?;
3157        let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
3158        for entry in entries {
3159            let key = entry.name.to_lowercase();
3160            map.entry(key)
3161                .and_modify(|v| {
3162                    v.push_str(", ");
3163                    v.push_str(&entry.value);
3164                })
3165                .or_insert(entry.value);
3166        }
3167        Ok(map)
3168    }
3169
3170    /// Returns the value for a single response header, or `None` if not present.
3171    ///
3172    /// The lookup is case-insensitive.
3173    ///
3174    /// # Errors
3175    ///
3176    /// Returns an error if:
3177    /// - No backing protocol object is available (edge case)
3178    /// - The RPC call to the server fails
3179    ///
3180    /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
3181    /// Returns the value for a single response header, or `None` if not present.
3182    ///
3183    /// The lookup is case-insensitive. When multiple headers share the same name,
3184    /// their values are joined with `, ` (matching Playwright's behavior).
3185    ///
3186    /// Uses the raw headers from the server for accurate results.
3187    ///
3188    /// # Errors
3189    ///
3190    /// Returns an error if the underlying `headers_array()` RPC call fails.
3191    ///
3192    /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
3193    pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
3194        let entries = self.headers_array().await?;
3195        let name_lower = name.to_lowercase();
3196        let mut values: Vec<String> = entries
3197            .into_iter()
3198            .filter(|h| h.name.to_lowercase() == name_lower)
3199            .map(|h| h.value)
3200            .collect();
3201
3202        if values.is_empty() {
3203            Ok(None)
3204        } else if values.len() == 1 {
3205            Ok(Some(values.remove(0)))
3206        } else {
3207            Ok(Some(values.join(", ")))
3208        }
3209    }
3210}
3211
3212impl std::fmt::Debug for Response {
3213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3214        f.debug_struct("Response")
3215            .field("url", &self.url)
3216            .field("status", &self.status)
3217            .field("status_text", &self.status_text)
3218            .field("ok", &self.ok)
3219            .finish_non_exhaustive()
3220    }
3221}
3222
3223/// Shared helper: store timeout locally and notify the Playwright server.
3224/// Used by both Page and BrowserContext timeout setters.
3225pub(crate) async fn set_timeout_and_notify(
3226    channel: &crate::server::channel::Channel,
3227    method: &str,
3228    timeout: f64,
3229) {
3230    if let Err(e) = channel
3231        .send_no_result(method, serde_json::json!({ "timeout": timeout }))
3232        .await
3233    {
3234        tracing::warn!("{} send error: {}", method, e);
3235    }
3236}