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