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::new(
1043                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                initializer["statusText"].as_str().unwrap_or("").to_string(),
1051                headers,
1052                Some(response_arc),
1053            );
1054
1055            if let Ok(mut page_url) = self.url.write() {
1056                *page_url = response.url().to_string();
1057            }
1058
1059            Ok(Some(response))
1060        } else {
1061            Ok(None)
1062        }
1063    }
1064
1065    /// Returns the first element matching the selector, or None if not found.
1066    ///
1067    /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
1068    pub async fn query_selector(
1069        &self,
1070        selector: &str,
1071    ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
1072        let frame = self.main_frame().await?;
1073        frame.query_selector(selector).await
1074    }
1075
1076    /// Returns all elements matching the selector.
1077    ///
1078    /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
1079    pub async fn query_selector_all(
1080        &self,
1081        selector: &str,
1082    ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
1083        let frame = self.main_frame().await?;
1084        frame.query_selector_all(selector).await
1085    }
1086
1087    /// Takes a screenshot of the page and returns the image bytes.
1088    ///
1089    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1090    pub async fn screenshot(
1091        &self,
1092        options: Option<crate::protocol::ScreenshotOptions>,
1093    ) -> Result<Vec<u8>> {
1094        let params = if let Some(opts) = options {
1095            opts.to_json()
1096        } else {
1097            // Default to PNG with required timeout
1098            serde_json::json!({
1099                "type": "png",
1100                "timeout": crate::DEFAULT_TIMEOUT_MS
1101            })
1102        };
1103
1104        #[derive(Deserialize)]
1105        struct ScreenshotResponse {
1106            binary: String,
1107        }
1108
1109        let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
1110
1111        // Decode base64 to bytes
1112        let bytes = base64::prelude::BASE64_STANDARD
1113            .decode(&response.binary)
1114            .map_err(|e| {
1115                crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
1116            })?;
1117
1118        Ok(bytes)
1119    }
1120
1121    /// Takes a screenshot and saves it to a file, also returning the bytes.
1122    ///
1123    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1124    pub async fn screenshot_to_file(
1125        &self,
1126        path: &std::path::Path,
1127        options: Option<crate::protocol::ScreenshotOptions>,
1128    ) -> Result<Vec<u8>> {
1129        // Get the screenshot bytes
1130        let bytes = self.screenshot(options).await?;
1131
1132        // Write to file
1133        tokio::fs::write(path, &bytes).await.map_err(|e| {
1134            crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
1135        })?;
1136
1137        Ok(bytes)
1138    }
1139
1140    /// Evaluates JavaScript in the page context (without return value).
1141    ///
1142    /// Executes the provided JavaScript expression or function within the page's
1143    /// context without returning a value.
1144    ///
1145    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1146    pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
1147        // Delegate to the main frame
1148        let frame = self.main_frame().await?;
1149        frame.frame_evaluate_expression(expression).await
1150    }
1151
1152    /// Evaluates JavaScript in the page context with optional arguments.
1153    ///
1154    /// Executes the provided JavaScript expression or function within the page's
1155    /// context and returns the result. The return value must be JSON-serializable.
1156    ///
1157    /// # Arguments
1158    ///
1159    /// * `expression` - JavaScript code to evaluate
1160    /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1161    ///
1162    /// # Returns
1163    ///
1164    /// The result as a `serde_json::Value`
1165    ///
1166    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1167    pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
1168        &self,
1169        expression: &str,
1170        arg: Option<&T>,
1171    ) -> Result<U> {
1172        // Delegate to the main frame
1173        let frame = self.main_frame().await?;
1174        let result = frame.evaluate(expression, arg).await?;
1175        serde_json::from_value(result).map_err(Error::from)
1176    }
1177
1178    /// Evaluates a JavaScript expression and returns the result as a String.
1179    ///
1180    /// # Arguments
1181    ///
1182    /// * `expression` - JavaScript code to evaluate
1183    ///
1184    /// # Returns
1185    ///
1186    /// The result converted to a String
1187    ///
1188    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1189    pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
1190        let frame = self.main_frame().await?;
1191        frame.frame_evaluate_expression_value(expression).await
1192    }
1193
1194    /// Registers a route handler for network interception.
1195    ///
1196    /// When a request matches the specified pattern, the handler will be called
1197    /// with a Route object that can abort, continue, or fulfill the request.
1198    ///
1199    /// # Arguments
1200    ///
1201    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
1202    /// * `handler` - Async closure that handles the route
1203    ///
1204    /// See: <https://playwright.dev/docs/api/class-page#page-route>
1205    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1206    where
1207        F: Fn(Route) -> Fut + Send + Sync + 'static,
1208        Fut: Future<Output = Result<()>> + Send + 'static,
1209    {
1210        // 1. Wrap handler in Arc with type erasure
1211        let handler =
1212            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1213
1214        // 2. Store in handlers list
1215        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1216            pattern: pattern.to_string(),
1217            handler,
1218        });
1219
1220        // 3. Enable network interception via protocol
1221        self.enable_network_interception().await?;
1222
1223        Ok(())
1224    }
1225
1226    /// Updates network interception patterns for this page
1227    async fn enable_network_interception(&self) -> Result<()> {
1228        // Collect all patterns from registered handlers
1229        // Each pattern must be an object with "glob" field
1230        let patterns: Vec<serde_json::Value> = self
1231            .route_handlers
1232            .lock()
1233            .unwrap()
1234            .iter()
1235            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1236            .collect();
1237
1238        // Send protocol command to update network interception patterns
1239        // Follows playwright-python's approach
1240        self.channel()
1241            .send_no_result(
1242                "setNetworkInterceptionPatterns",
1243                serde_json::json!({
1244                    "patterns": patterns
1245                }),
1246            )
1247            .await
1248    }
1249
1250    /// Removes route handler(s) matching the given URL pattern.
1251    ///
1252    /// # Arguments
1253    ///
1254    /// * `pattern` - URL pattern to remove handlers for
1255    ///
1256    /// See: <https://playwright.dev/docs/api/class-page#page-unroute>
1257    pub async fn unroute(&self, pattern: &str) -> Result<()> {
1258        self.route_handlers
1259            .lock()
1260            .unwrap()
1261            .retain(|entry| entry.pattern != pattern);
1262        self.enable_network_interception().await
1263    }
1264
1265    /// Removes all registered route handlers.
1266    ///
1267    /// # Arguments
1268    ///
1269    /// * `behavior` - Optional behavior for in-flight handlers
1270    ///
1271    /// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
1272    pub async fn unroute_all(
1273        &self,
1274        _behavior: Option<crate::protocol::route::UnrouteBehavior>,
1275    ) -> Result<()> {
1276        self.route_handlers.lock().unwrap().clear();
1277        self.enable_network_interception().await
1278    }
1279
1280    /// Handles a route event from the protocol
1281    ///
1282    /// Called by on_event when a "route" event is received.
1283    /// Supports handler chaining via `route.fallback()` — if a handler calls
1284    /// `fallback()` instead of `continue_()`, `abort()`, or `fulfill()`, the
1285    /// next matching handler in the chain is tried.
1286    async fn on_route_event(&self, route: Route) {
1287        let handlers = self.route_handlers.lock().unwrap().clone();
1288        let url = route.request().url().to_string();
1289
1290        // Find matching handler (last registered wins, with fallback chaining)
1291        for entry in handlers.iter().rev() {
1292            if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
1293                let handler = entry.handler.clone();
1294                if let Err(e) = handler(route.clone()).await {
1295                    tracing::warn!("Route handler error: {}", e);
1296                    break;
1297                }
1298                // If handler called fallback(), try the next matching handler
1299                if !route.was_handled() {
1300                    continue;
1301                }
1302                break;
1303            }
1304        }
1305    }
1306
1307    /// Registers a download event handler.
1308    ///
1309    /// The handler will be called when a download is triggered by the page.
1310    /// Downloads occur when the page initiates a file download (e.g., clicking a link
1311    /// with the download attribute, or a server response with Content-Disposition: attachment).
1312    ///
1313    /// # Arguments
1314    ///
1315    /// * `handler` - Async closure that receives the Download object
1316    ///
1317    /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
1318    pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1319    where
1320        F: Fn(Download) -> Fut + Send + Sync + 'static,
1321        Fut: Future<Output = Result<()>> + Send + 'static,
1322    {
1323        // Wrap handler with type erasure
1324        let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
1325            Box::pin(handler(download))
1326        });
1327
1328        // Store handler
1329        self.download_handlers.lock().unwrap().push(handler);
1330
1331        Ok(())
1332    }
1333
1334    /// Registers a dialog event handler.
1335    ///
1336    /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
1337    /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
1338    ///
1339    /// # Arguments
1340    ///
1341    /// * `handler` - Async closure that receives the Dialog object
1342    ///
1343    /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
1344    pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1345    where
1346        F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1347        Fut: Future<Output = Result<()>> + Send + 'static,
1348    {
1349        // Wrap handler with type erasure
1350        let handler =
1351            Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
1352
1353        // Store handler
1354        self.dialog_handlers.lock().unwrap().push(handler);
1355
1356        // Dialog events are auto-emitted (no subscription needed)
1357
1358        Ok(())
1359    }
1360
1361    /// See: <https://playwright.dev/docs/api/class-page#page-event-request>
1362    pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
1363    where
1364        F: Fn(Request) -> Fut + Send + Sync + 'static,
1365        Fut: Future<Output = Result<()>> + Send + 'static,
1366    {
1367        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1368            Box::pin(handler(request))
1369        });
1370
1371        let needs_subscription = self.request_handlers.lock().unwrap().is_empty();
1372        if needs_subscription {
1373            _ = self.channel().update_subscription("request", true).await;
1374        }
1375        self.request_handlers.lock().unwrap().push(handler);
1376
1377        Ok(())
1378    }
1379
1380    /// See: <https://playwright.dev/docs/api/class-page#page-event-request-finished>
1381    pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
1382    where
1383        F: Fn(Request) -> Fut + Send + Sync + 'static,
1384        Fut: Future<Output = Result<()>> + Send + 'static,
1385    {
1386        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1387            Box::pin(handler(request))
1388        });
1389
1390        let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
1391        if needs_subscription {
1392            _ = self
1393                .channel()
1394                .update_subscription("requestFinished", true)
1395                .await;
1396        }
1397        self.request_finished_handlers.lock().unwrap().push(handler);
1398
1399        Ok(())
1400    }
1401
1402    /// See: <https://playwright.dev/docs/api/class-page#page-event-request-failed>
1403    pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
1404    where
1405        F: Fn(Request) -> Fut + Send + Sync + 'static,
1406        Fut: Future<Output = Result<()>> + Send + 'static,
1407    {
1408        let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1409            Box::pin(handler(request))
1410        });
1411
1412        let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
1413        if needs_subscription {
1414            _ = self
1415                .channel()
1416                .update_subscription("requestFailed", true)
1417                .await;
1418        }
1419        self.request_failed_handlers.lock().unwrap().push(handler);
1420
1421        Ok(())
1422    }
1423
1424    /// See: <https://playwright.dev/docs/api/class-page#page-event-response>
1425    pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
1426    where
1427        F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
1428        Fut: Future<Output = Result<()>> + Send + 'static,
1429    {
1430        let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
1431            Box::pin(handler(response))
1432        });
1433
1434        let needs_subscription = self.response_handlers.lock().unwrap().is_empty();
1435        if needs_subscription {
1436            _ = self.channel().update_subscription("response", true).await;
1437        }
1438        self.response_handlers.lock().unwrap().push(handler);
1439
1440        Ok(())
1441    }
1442
1443    /// Adds a listener for the `websocket` event.
1444    ///
1445    /// The handler will be called when a WebSocket request is dispatched.
1446    ///
1447    /// # Arguments
1448    ///
1449    /// * `handler` - The function to call when the event occurs
1450    ///
1451    /// See: <https://playwright.dev/docs/api/class-page#page-on-websocket>
1452    pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
1453    where
1454        F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1455        Fut: Future<Output = Result<()>> + Send + 'static,
1456    {
1457        let handler =
1458            Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
1459        self.websocket_handlers.lock().unwrap().push(handler);
1460        Ok(())
1461    }
1462
1463    /// Handles a download event from the protocol
1464    async fn on_download_event(&self, download: Download) {
1465        let handlers = self.download_handlers.lock().unwrap().clone();
1466
1467        for handler in handlers {
1468            if let Err(e) = handler(download.clone()).await {
1469                tracing::warn!("Download handler error: {}", e);
1470            }
1471        }
1472    }
1473
1474    /// Handles a dialog event from the protocol
1475    async fn on_dialog_event(&self, dialog: Dialog) {
1476        let handlers = self.dialog_handlers.lock().unwrap().clone();
1477
1478        for handler in handlers {
1479            if let Err(e) = handler(dialog.clone()).await {
1480                tracing::warn!("Dialog handler error: {}", e);
1481            }
1482        }
1483    }
1484
1485    async fn on_request_event(&self, request: Request) {
1486        let handlers = self.request_handlers.lock().unwrap().clone();
1487
1488        for handler in handlers {
1489            if let Err(e) = handler(request.clone()).await {
1490                tracing::warn!("Request handler error: {}", e);
1491            }
1492        }
1493    }
1494
1495    async fn on_request_failed_event(&self, request: Request) {
1496        let handlers = self.request_failed_handlers.lock().unwrap().clone();
1497
1498        for handler in handlers {
1499            if let Err(e) = handler(request.clone()).await {
1500                tracing::warn!("RequestFailed handler error: {}", e);
1501            }
1502        }
1503    }
1504
1505    async fn on_request_finished_event(&self, request: Request) {
1506        let handlers = self.request_finished_handlers.lock().unwrap().clone();
1507
1508        for handler in handlers {
1509            if let Err(e) = handler(request.clone()).await {
1510                tracing::warn!("RequestFinished handler error: {}", e);
1511            }
1512        }
1513    }
1514
1515    async fn on_response_event(&self, response: ResponseObject) {
1516        let handlers = self.response_handlers.lock().unwrap().clone();
1517
1518        for handler in handlers {
1519            if let Err(e) = handler(response.clone()).await {
1520                tracing::warn!("Response handler error: {}", e);
1521            }
1522        }
1523    }
1524
1525    /// Triggers dialog event (called by BrowserContext when dialog events arrive)
1526    ///
1527    /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
1528    /// This method is public so BrowserContext can forward dialog events.
1529    pub async fn trigger_dialog_event(&self, dialog: Dialog) {
1530        self.on_dialog_event(dialog).await;
1531    }
1532
1533    /// Triggers request event (called by BrowserContext when request events arrive)
1534    pub(crate) async fn trigger_request_event(&self, request: Request) {
1535        self.on_request_event(request).await;
1536    }
1537
1538    pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
1539        self.on_request_finished_event(request).await;
1540    }
1541
1542    pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
1543        self.on_request_failed_event(request).await;
1544    }
1545
1546    /// Triggers response event (called by BrowserContext when response events arrive)
1547    pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
1548        self.on_response_event(response).await;
1549    }
1550
1551    /// Adds a `<style>` tag into the page with the desired content.
1552    ///
1553    /// # Arguments
1554    ///
1555    /// * `options` - Style tag options (content, url, or path)
1556    ///
1557    /// # Returns
1558    ///
1559    /// Returns an ElementHandle pointing to the injected `<style>` tag
1560    ///
1561    /// # Example
1562    ///
1563    /// ```no_run
1564    /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
1565    /// # #[tokio::main]
1566    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1567    /// # let playwright = Playwright::launch().await?;
1568    /// # let browser = playwright.chromium().launch().await?;
1569    /// # let context = browser.new_context().await?;
1570    /// # let page = context.new_page().await?;
1571    /// use playwright_rs::protocol::AddStyleTagOptions;
1572    ///
1573    /// // With inline CSS
1574    /// page.add_style_tag(
1575    ///     AddStyleTagOptions::builder()
1576    ///         .content("body { background-color: red; }")
1577    ///         .build()
1578    /// ).await?;
1579    ///
1580    /// // With external URL
1581    /// page.add_style_tag(
1582    ///     AddStyleTagOptions::builder()
1583    ///         .url("https://example.com/style.css")
1584    ///         .build()
1585    /// ).await?;
1586    ///
1587    /// // From file
1588    /// page.add_style_tag(
1589    ///     AddStyleTagOptions::builder()
1590    ///         .path("./styles/custom.css")
1591    ///         .build()
1592    /// ).await?;
1593    /// # Ok(())
1594    /// # }
1595    /// ```
1596    ///
1597    /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1598    pub async fn add_style_tag(
1599        &self,
1600        options: AddStyleTagOptions,
1601    ) -> Result<Arc<crate::protocol::ElementHandle>> {
1602        let frame = self.main_frame().await?;
1603        frame.add_style_tag(options).await
1604    }
1605
1606    /// Adds a script which would be evaluated in one of the following scenarios:
1607    /// - Whenever the page is navigated
1608    /// - Whenever a child frame is attached or navigated
1609    ///
1610    /// The script is evaluated after the document was created but before any of its scripts were run.
1611    ///
1612    /// # Arguments
1613    ///
1614    /// * `script` - JavaScript code to be injected into the page
1615    ///
1616    /// # Example
1617    ///
1618    /// ```no_run
1619    /// # use playwright_rs::protocol::Playwright;
1620    /// # #[tokio::main]
1621    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1622    /// # let playwright = Playwright::launch().await?;
1623    /// # let browser = playwright.chromium().launch().await?;
1624    /// # let context = browser.new_context().await?;
1625    /// # let page = context.new_page().await?;
1626    /// page.add_init_script("window.injected = 123;").await?;
1627    /// # Ok(())
1628    /// # }
1629    /// ```
1630    ///
1631    /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
1632    pub async fn add_init_script(&self, script: &str) -> Result<()> {
1633        self.channel()
1634            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
1635            .await
1636    }
1637
1638    /// Sets the viewport size for the page.
1639    ///
1640    /// This method allows dynamic resizing of the viewport after page creation,
1641    /// useful for testing responsive layouts at different screen sizes.
1642    ///
1643    /// # Arguments
1644    ///
1645    /// * `viewport` - The viewport dimensions (width and height in pixels)
1646    ///
1647    /// # Example
1648    ///
1649    /// ```no_run
1650    /// # use playwright_rs::protocol::{Playwright, Viewport};
1651    /// # #[tokio::main]
1652    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1653    /// # let playwright = Playwright::launch().await?;
1654    /// # let browser = playwright.chromium().launch().await?;
1655    /// # let page = browser.new_page().await?;
1656    /// // Set viewport to mobile size
1657    /// let mobile = Viewport {
1658    ///     width: 375,
1659    ///     height: 667,
1660    /// };
1661    /// page.set_viewport_size(mobile).await?;
1662    ///
1663    /// // Later, test desktop layout
1664    /// let desktop = Viewport {
1665    ///     width: 1920,
1666    ///     height: 1080,
1667    /// };
1668    /// page.set_viewport_size(desktop).await?;
1669    /// # Ok(())
1670    /// # }
1671    /// ```
1672    ///
1673    /// # Errors
1674    ///
1675    /// Returns error if:
1676    /// - Page has been closed
1677    /// - Communication with browser process fails
1678    ///
1679    /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
1680    pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
1681        // Store the new viewport locally so viewport_size() can reflect the change
1682        if let Ok(mut guard) = self.viewport.write() {
1683            *guard = Some(viewport.clone());
1684        }
1685        self.channel()
1686            .send_no_result(
1687                "setViewportSize",
1688                serde_json::json!({ "viewportSize": viewport }),
1689            )
1690            .await
1691    }
1692
1693    /// Brings this page to the front (activates the tab).
1694    ///
1695    /// Activates the page in the browser, making it the focused tab. This is
1696    /// useful in multi-page tests to ensure actions target the correct page.
1697    ///
1698    /// # Errors
1699    ///
1700    /// Returns error if:
1701    /// - Page has been closed
1702    /// - Communication with browser process fails
1703    ///
1704    /// See: <https://playwright.dev/docs/api/class-page#page-bring-to-front>
1705    pub async fn bring_to_front(&self) -> Result<()> {
1706        self.channel()
1707            .send_no_result("bringToFront", serde_json::json!({}))
1708            .await
1709    }
1710
1711    /// Sets extra HTTP headers that will be sent with every request from this page.
1712    ///
1713    /// These headers are sent in addition to headers set on the browser context via
1714    /// `BrowserContext::set_extra_http_headers()`. Page-level headers take precedence
1715    /// over context-level headers when names conflict.
1716    ///
1717    /// # Arguments
1718    ///
1719    /// * `headers` - Map of header names to values.
1720    ///
1721    /// # Errors
1722    ///
1723    /// Returns error if:
1724    /// - Page has been closed
1725    /// - Communication with browser process fails
1726    ///
1727    /// See: <https://playwright.dev/docs/api/class-page#page-set-extra-http-headers>
1728    pub async fn set_extra_http_headers(
1729        &self,
1730        headers: std::collections::HashMap<String, String>,
1731    ) -> Result<()> {
1732        // Playwright protocol expects an array of {name, value} objects
1733        // This RPC is sent on the Page channel (not the Frame channel)
1734        let headers_array: Vec<serde_json::Value> = headers
1735            .into_iter()
1736            .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
1737            .collect();
1738        self.channel()
1739            .send_no_result(
1740                "setExtraHTTPHeaders",
1741                serde_json::json!({ "headers": headers_array }),
1742            )
1743            .await
1744    }
1745
1746    /// Emulates media features for the page.
1747    ///
1748    /// This method allows emulating CSS media features such as `media`, `color-scheme`,
1749    /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
1750    ///
1751    /// To reset a specific feature to the browser default, use the `NoOverride` variant.
1752    ///
1753    /// # Arguments
1754    ///
1755    /// * `options` - Optional emulation options. If `None`, this is a no-op.
1756    ///
1757    /// # Example
1758    ///
1759    /// ```no_run
1760    /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
1761    /// # #[tokio::main]
1762    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1763    /// # let playwright = Playwright::launch().await?;
1764    /// # let browser = playwright.chromium().launch().await?;
1765    /// # let page = browser.new_page().await?;
1766    /// // Emulate print media
1767    /// page.emulate_media(Some(
1768    ///     EmulateMediaOptions::builder()
1769    ///         .media(Media::Print)
1770    ///         .build()
1771    /// )).await?;
1772    ///
1773    /// // Emulate dark color scheme
1774    /// page.emulate_media(Some(
1775    ///     EmulateMediaOptions::builder()
1776    ///         .color_scheme(ColorScheme::Dark)
1777    ///         .build()
1778    /// )).await?;
1779    /// # Ok(())
1780    /// # }
1781    /// ```
1782    ///
1783    /// # Errors
1784    ///
1785    /// Returns error if:
1786    /// - Page has been closed
1787    /// - Communication with browser process fails
1788    ///
1789    /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
1790    pub async fn emulate_media(&self, options: Option<EmulateMediaOptions>) -> Result<()> {
1791        let mut params = serde_json::json!({});
1792
1793        if let Some(opts) = options {
1794            if let Some(media) = opts.media {
1795                params["media"] = serde_json::to_value(media).map_err(|e| {
1796                    crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
1797                })?;
1798            }
1799            if let Some(color_scheme) = opts.color_scheme {
1800                params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
1801                    crate::error::Error::ProtocolError(format!(
1802                        "Failed to serialize colorScheme: {}",
1803                        e
1804                    ))
1805                })?;
1806            }
1807            if let Some(reduced_motion) = opts.reduced_motion {
1808                params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
1809                    crate::error::Error::ProtocolError(format!(
1810                        "Failed to serialize reducedMotion: {}",
1811                        e
1812                    ))
1813                })?;
1814            }
1815            if let Some(forced_colors) = opts.forced_colors {
1816                params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
1817                    crate::error::Error::ProtocolError(format!(
1818                        "Failed to serialize forcedColors: {}",
1819                        e
1820                    ))
1821                })?;
1822            }
1823        }
1824
1825        self.channel().send_no_result("emulateMedia", params).await
1826    }
1827
1828    /// Generates a PDF of the page and returns it as bytes.
1829    ///
1830    /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
1831    /// not supported in Firefox or WebKit.
1832    ///
1833    /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
1834    /// saved to that file.
1835    ///
1836    /// # Arguments
1837    ///
1838    /// * `options` - Optional PDF generation options
1839    ///
1840    /// # Example
1841    ///
1842    /// ```no_run
1843    /// # use playwright_rs::protocol::Playwright;
1844    /// # #[tokio::main]
1845    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1846    /// # let playwright = Playwright::launch().await?;
1847    /// # let browser = playwright.chromium().launch().await?;
1848    /// # let page = browser.new_page().await?;
1849    /// let pdf_bytes = page.pdf(None).await?;
1850    /// assert!(!pdf_bytes.is_empty());
1851    /// # Ok(())
1852    /// # }
1853    /// ```
1854    ///
1855    /// # Errors
1856    ///
1857    /// Returns error if:
1858    /// - The browser is not Chromium (PDF only supported in Chromium)
1859    /// - Page has been closed
1860    /// - Communication with browser process fails
1861    ///
1862    /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
1863    pub async fn pdf(&self, options: Option<PdfOptions>) -> Result<Vec<u8>> {
1864        let mut params = serde_json::json!({});
1865        let mut save_path: Option<std::path::PathBuf> = None;
1866
1867        if let Some(opts) = options {
1868            // Capture the file path before consuming opts
1869            save_path = opts.path;
1870
1871            if let Some(scale) = opts.scale {
1872                params["scale"] = serde_json::json!(scale);
1873            }
1874            if let Some(v) = opts.display_header_footer {
1875                params["displayHeaderFooter"] = serde_json::json!(v);
1876            }
1877            if let Some(v) = opts.header_template {
1878                params["headerTemplate"] = serde_json::json!(v);
1879            }
1880            if let Some(v) = opts.footer_template {
1881                params["footerTemplate"] = serde_json::json!(v);
1882            }
1883            if let Some(v) = opts.print_background {
1884                params["printBackground"] = serde_json::json!(v);
1885            }
1886            if let Some(v) = opts.landscape {
1887                params["landscape"] = serde_json::json!(v);
1888            }
1889            if let Some(v) = opts.page_ranges {
1890                params["pageRanges"] = serde_json::json!(v);
1891            }
1892            if let Some(v) = opts.format {
1893                params["format"] = serde_json::json!(v);
1894            }
1895            if let Some(v) = opts.width {
1896                params["width"] = serde_json::json!(v);
1897            }
1898            if let Some(v) = opts.height {
1899                params["height"] = serde_json::json!(v);
1900            }
1901            if let Some(v) = opts.prefer_css_page_size {
1902                params["preferCSSPageSize"] = serde_json::json!(v);
1903            }
1904            if let Some(margin) = opts.margin {
1905                params["margin"] = serde_json::to_value(margin).map_err(|e| {
1906                    crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
1907                })?;
1908            }
1909        }
1910
1911        #[derive(Deserialize)]
1912        struct PdfResponse {
1913            pdf: String,
1914        }
1915
1916        let response: PdfResponse = self.channel().send("pdf", params).await?;
1917
1918        // Decode base64 to bytes
1919        let pdf_bytes = base64::engine::general_purpose::STANDARD
1920            .decode(&response.pdf)
1921            .map_err(|e| {
1922                crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
1923            })?;
1924
1925        // If a path was specified, save the PDF to disk as well
1926        if let Some(path) = save_path {
1927            tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
1928                crate::error::Error::InvalidArgument(format!(
1929                    "Failed to write PDF to '{}': {}",
1930                    path.display(),
1931                    e
1932                ))
1933            })?;
1934        }
1935
1936        Ok(pdf_bytes)
1937    }
1938
1939    /// Adds a `<script>` tag into the page with the desired URL or content.
1940    ///
1941    /// # Arguments
1942    ///
1943    /// * `options` - Optional script tag options (content, url, or path).
1944    ///   If `None`, returns an error because no source is specified.
1945    ///
1946    /// At least one of `content`, `url`, or `path` must be provided.
1947    ///
1948    /// # Example
1949    ///
1950    /// ```no_run
1951    /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
1952    /// # #[tokio::main]
1953    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1954    /// # let playwright = Playwright::launch().await?;
1955    /// # let browser = playwright.chromium().launch().await?;
1956    /// # let context = browser.new_context().await?;
1957    /// # let page = context.new_page().await?;
1958    /// // With inline JavaScript
1959    /// page.add_script_tag(Some(
1960    ///     AddScriptTagOptions::builder()
1961    ///         .content("window.myVar = 42;")
1962    ///         .build()
1963    /// )).await?;
1964    ///
1965    /// // With external URL
1966    /// page.add_script_tag(Some(
1967    ///     AddScriptTagOptions::builder()
1968    ///         .url("https://example.com/script.js")
1969    ///         .build()
1970    /// )).await?;
1971    /// # Ok(())
1972    /// # }
1973    /// ```
1974    ///
1975    /// # Errors
1976    ///
1977    /// Returns error if:
1978    /// - `options` is `None` or no content/url/path is specified
1979    /// - Page has been closed
1980    /// - Script loading fails (e.g., invalid URL)
1981    ///
1982    /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
1983    pub async fn add_script_tag(
1984        &self,
1985        options: Option<AddScriptTagOptions>,
1986    ) -> Result<Arc<crate::protocol::ElementHandle>> {
1987        let opts = options.ok_or_else(|| {
1988            Error::InvalidArgument(
1989                "At least one of content, url, or path must be specified".to_string(),
1990            )
1991        })?;
1992        let frame = self.main_frame().await?;
1993        frame.add_script_tag(opts).await
1994    }
1995
1996    /// Returns the current viewport size of the page, or `None` if no viewport is set.
1997    ///
1998    /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
1999    /// returns the dimensions configured at context creation time or updated via
2000    /// `set_viewport_size()`.
2001    ///
2002    /// # Example
2003    ///
2004    /// ```ignore
2005    /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
2006    /// # #[tokio::main]
2007    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2008    /// # let playwright = Playwright::launch().await?;
2009    /// # let browser = playwright.chromium().launch().await?;
2010    /// let context = browser.new_context_with_options(
2011    ///     BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
2012    /// ).await?;
2013    /// let page = context.new_page().await?;
2014    /// let size = page.viewport_size().expect("Viewport should be set");
2015    /// assert_eq!(size.width, 1280);
2016    /// assert_eq!(size.height, 720);
2017    /// # Ok(())
2018    /// # }
2019    /// ```
2020    ///
2021    /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
2022    pub fn viewport_size(&self) -> Option<Viewport> {
2023        self.viewport.read().ok()?.clone()
2024    }
2025}
2026
2027impl ChannelOwner for Page {
2028    fn guid(&self) -> &str {
2029        self.base.guid()
2030    }
2031
2032    fn type_name(&self) -> &str {
2033        self.base.type_name()
2034    }
2035
2036    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2037        self.base.parent()
2038    }
2039
2040    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2041        self.base.connection()
2042    }
2043
2044    fn initializer(&self) -> &Value {
2045        self.base.initializer()
2046    }
2047
2048    fn channel(&self) -> &Channel {
2049        self.base.channel()
2050    }
2051
2052    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2053        self.base.dispose(reason)
2054    }
2055
2056    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2057        self.base.adopt(child)
2058    }
2059
2060    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2061        self.base.add_child(guid, child)
2062    }
2063
2064    fn remove_child(&self, guid: &str) {
2065        self.base.remove_child(guid)
2066    }
2067
2068    fn on_event(&self, method: &str, params: Value) {
2069        match method {
2070            "navigated" => {
2071                // Update URL when page navigates
2072                if let Some(url_value) = params.get("url") {
2073                    if let Some(url_str) = url_value.as_str() {
2074                        if let Ok(mut url) = self.url.write() {
2075                            *url = url_str.to_string();
2076                        }
2077                    }
2078                }
2079            }
2080            "route" => {
2081                // Handle network routing event
2082                if let Some(route_guid) = params
2083                    .get("route")
2084                    .and_then(|v| v.get("guid"))
2085                    .and_then(|v| v.as_str())
2086                {
2087                    // Get the Route object from connection's registry
2088                    let connection = self.connection();
2089                    let route_guid_owned = route_guid.to_string();
2090                    let self_clone = self.clone();
2091
2092                    tokio::spawn(async move {
2093                        // Wait for Route object to be created
2094                        let route_arc = match connection.get_object(&route_guid_owned).await {
2095                            Ok(obj) => obj,
2096                            Err(e) => {
2097                                tracing::warn!("Failed to get route object: {}", e);
2098                                return;
2099                            }
2100                        };
2101
2102                        // Downcast to Route
2103                        let route = match route_arc.as_any().downcast_ref::<Route>() {
2104                            Some(r) => r.clone(),
2105                            None => {
2106                                tracing::warn!("Failed to downcast to Route");
2107                                return;
2108                            }
2109                        };
2110
2111                        // Set APIRequestContext on the route for fetch() support.
2112                        // Page's parent is BrowserContext, which has the request context.
2113                        if let Some(parent) = self_clone.parent() {
2114                            if let Some(ctx) = parent
2115                                .as_any()
2116                                .downcast_ref::<crate::protocol::BrowserContext>()
2117                            {
2118                                if let Ok(api_ctx) = ctx.request().await {
2119                                    route.set_api_request_context(api_ctx);
2120                                }
2121                            }
2122                        }
2123
2124                        // Call the route handler and wait for completion
2125                        self_clone.on_route_event(route).await;
2126                    });
2127                }
2128            }
2129            "download" => {
2130                // Handle download event
2131                // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
2132                let url = params
2133                    .get("url")
2134                    .and_then(|v| v.as_str())
2135                    .unwrap_or("")
2136                    .to_string();
2137
2138                let suggested_filename = params
2139                    .get("suggestedFilename")
2140                    .and_then(|v| v.as_str())
2141                    .unwrap_or("")
2142                    .to_string();
2143
2144                if let Some(artifact_guid) = params
2145                    .get("artifact")
2146                    .and_then(|v| v.get("guid"))
2147                    .and_then(|v| v.as_str())
2148                {
2149                    let connection = self.connection();
2150                    let artifact_guid_owned = artifact_guid.to_string();
2151                    let self_clone = self.clone();
2152
2153                    tokio::spawn(async move {
2154                        // Wait for Artifact object to be created
2155                        let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
2156                            Ok(obj) => obj,
2157                            Err(e) => {
2158                                tracing::warn!("Failed to get artifact object: {}", e);
2159                                return;
2160                            }
2161                        };
2162
2163                        // Create Download wrapper from Artifact + event params
2164                        let download = Download::from_artifact(
2165                            artifact_arc,
2166                            url,
2167                            suggested_filename,
2168                            self_clone.clone(),
2169                        );
2170
2171                        // Call the download handlers
2172                        self_clone.on_download_event(download).await;
2173                    });
2174                }
2175            }
2176            "dialog" => {
2177                // Dialog events are handled by BrowserContext and forwarded to Page
2178                // This case should not be reached, but keeping for completeness
2179            }
2180            "webSocket" => {
2181                if let Some(ws_guid) = params
2182                    .get("webSocket")
2183                    .and_then(|v| v.get("guid"))
2184                    .and_then(|v| v.as_str())
2185                {
2186                    let connection = self.connection();
2187                    let ws_guid_owned = ws_guid.to_string();
2188                    let self_clone = self.clone();
2189
2190                    tokio::spawn(async move {
2191                        // Wait for WebSocket object to be created
2192                        let ws_arc = match connection.get_object(&ws_guid_owned).await {
2193                            Ok(obj) => obj,
2194                            Err(e) => {
2195                                tracing::warn!("Failed to get WebSocket object: {}", e);
2196                                return;
2197                            }
2198                        };
2199
2200                        // Downcast to WebSocket
2201                        let ws = if let Some(ws) = ws_arc.as_any().downcast_ref::<WebSocket>() {
2202                            ws.clone()
2203                        } else {
2204                            tracing::warn!("Expected WebSocket object, got {}", ws_arc.type_name());
2205                            return;
2206                        };
2207
2208                        // Call handlers
2209                        let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
2210                        for handler in handlers {
2211                            let ws_clone = ws.clone();
2212                            tokio::spawn(async move {
2213                                if let Err(e) = handler(ws_clone).await {
2214                                    tracing::error!("Error in websocket handler: {}", e);
2215                                }
2216                            });
2217                        }
2218                    });
2219                }
2220            }
2221            "close" => {
2222                // Server-initiated close (e.g. context was closed)
2223                self.is_closed.store(true, Ordering::Relaxed);
2224            }
2225            _ => {
2226                // Other events will be handled in future phases
2227                // Events: load, domcontentloaded, crash, etc.
2228            }
2229        }
2230    }
2231
2232    fn was_collected(&self) -> bool {
2233        self.base.was_collected()
2234    }
2235
2236    fn as_any(&self) -> &dyn Any {
2237        self
2238    }
2239}
2240
2241impl std::fmt::Debug for Page {
2242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2243        f.debug_struct("Page")
2244            .field("guid", &self.guid())
2245            .field("url", &self.url())
2246            .finish()
2247    }
2248}
2249
2250/// Options for page.goto() and page.reload()
2251#[derive(Debug, Clone)]
2252pub struct GotoOptions {
2253    /// Maximum operation time in milliseconds
2254    pub timeout: Option<std::time::Duration>,
2255    /// When to consider operation succeeded
2256    pub wait_until: Option<WaitUntil>,
2257}
2258
2259impl GotoOptions {
2260    /// Creates new GotoOptions with default values
2261    pub fn new() -> Self {
2262        Self {
2263            timeout: None,
2264            wait_until: None,
2265        }
2266    }
2267
2268    /// Sets the timeout
2269    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
2270        self.timeout = Some(timeout);
2271        self
2272    }
2273
2274    /// Sets the wait_until option
2275    pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
2276        self.wait_until = Some(wait_until);
2277        self
2278    }
2279}
2280
2281impl Default for GotoOptions {
2282    fn default() -> Self {
2283        Self::new()
2284    }
2285}
2286
2287/// When to consider navigation succeeded
2288#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2289pub enum WaitUntil {
2290    /// Consider operation to be finished when the `load` event is fired
2291    Load,
2292    /// Consider operation to be finished when the `DOMContentLoaded` event is fired
2293    DomContentLoaded,
2294    /// Consider operation to be finished when there are no network connections for at least 500ms
2295    NetworkIdle,
2296    /// Consider operation to be finished when the commit event is fired
2297    Commit,
2298}
2299
2300impl WaitUntil {
2301    pub(crate) fn as_str(&self) -> &'static str {
2302        match self {
2303            WaitUntil::Load => "load",
2304            WaitUntil::DomContentLoaded => "domcontentloaded",
2305            WaitUntil::NetworkIdle => "networkidle",
2306            WaitUntil::Commit => "commit",
2307        }
2308    }
2309}
2310
2311/// Options for adding a style tag to the page
2312///
2313/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
2314#[derive(Debug, Clone, Default)]
2315pub struct AddStyleTagOptions {
2316    /// Raw CSS content to inject
2317    pub content: Option<String>,
2318    /// URL of the `<link>` tag to add
2319    pub url: Option<String>,
2320    /// Path to a CSS file to inject
2321    pub path: Option<String>,
2322}
2323
2324impl AddStyleTagOptions {
2325    /// Creates a new builder for AddStyleTagOptions
2326    pub fn builder() -> AddStyleTagOptionsBuilder {
2327        AddStyleTagOptionsBuilder::default()
2328    }
2329
2330    /// Validates that at least one option is specified
2331    pub(crate) fn validate(&self) -> Result<()> {
2332        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
2333            return Err(Error::InvalidArgument(
2334                "At least one of content, url, or path must be specified".to_string(),
2335            ));
2336        }
2337        Ok(())
2338    }
2339}
2340
2341/// Builder for AddStyleTagOptions
2342#[derive(Debug, Clone, Default)]
2343pub struct AddStyleTagOptionsBuilder {
2344    content: Option<String>,
2345    url: Option<String>,
2346    path: Option<String>,
2347}
2348
2349impl AddStyleTagOptionsBuilder {
2350    /// Sets the CSS content to inject
2351    pub fn content(mut self, content: impl Into<String>) -> Self {
2352        self.content = Some(content.into());
2353        self
2354    }
2355
2356    /// Sets the URL of the stylesheet
2357    pub fn url(mut self, url: impl Into<String>) -> Self {
2358        self.url = Some(url.into());
2359        self
2360    }
2361
2362    /// Sets the path to a CSS file
2363    pub fn path(mut self, path: impl Into<String>) -> Self {
2364        self.path = Some(path.into());
2365        self
2366    }
2367
2368    /// Builds the AddStyleTagOptions
2369    pub fn build(self) -> AddStyleTagOptions {
2370        AddStyleTagOptions {
2371            content: self.content,
2372            url: self.url,
2373            path: self.path,
2374        }
2375    }
2376}
2377
2378// ============================================================================
2379// AddScriptTagOptions
2380// ============================================================================
2381
2382/// Options for adding a `<script>` tag to the page.
2383///
2384/// At least one of `content`, `url`, or `path` must be specified.
2385///
2386/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
2387#[derive(Debug, Clone, Default)]
2388pub struct AddScriptTagOptions {
2389    /// Raw JavaScript content to inject
2390    pub content: Option<String>,
2391    /// URL of the `<script>` tag to add
2392    pub url: Option<String>,
2393    /// Path to a JavaScript file to inject (file contents will be read and sent as content)
2394    pub path: Option<String>,
2395    /// Script type attribute (e.g., `"module"`)
2396    pub type_: Option<String>,
2397}
2398
2399impl AddScriptTagOptions {
2400    /// Creates a new builder for AddScriptTagOptions
2401    pub fn builder() -> AddScriptTagOptionsBuilder {
2402        AddScriptTagOptionsBuilder::default()
2403    }
2404
2405    /// Validates that at least one option is specified
2406    pub(crate) fn validate(&self) -> Result<()> {
2407        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
2408            return Err(Error::InvalidArgument(
2409                "At least one of content, url, or path must be specified".to_string(),
2410            ));
2411        }
2412        Ok(())
2413    }
2414}
2415
2416/// Builder for AddScriptTagOptions
2417#[derive(Debug, Clone, Default)]
2418pub struct AddScriptTagOptionsBuilder {
2419    content: Option<String>,
2420    url: Option<String>,
2421    path: Option<String>,
2422    type_: Option<String>,
2423}
2424
2425impl AddScriptTagOptionsBuilder {
2426    /// Sets the JavaScript content to inject
2427    pub fn content(mut self, content: impl Into<String>) -> Self {
2428        self.content = Some(content.into());
2429        self
2430    }
2431
2432    /// Sets the URL of the script to load
2433    pub fn url(mut self, url: impl Into<String>) -> Self {
2434        self.url = Some(url.into());
2435        self
2436    }
2437
2438    /// Sets the path to a JavaScript file to inject
2439    pub fn path(mut self, path: impl Into<String>) -> Self {
2440        self.path = Some(path.into());
2441        self
2442    }
2443
2444    /// Sets the script type attribute (e.g., `"module"`)
2445    pub fn type_(mut self, type_: impl Into<String>) -> Self {
2446        self.type_ = Some(type_.into());
2447        self
2448    }
2449
2450    /// Builds the AddScriptTagOptions
2451    pub fn build(self) -> AddScriptTagOptions {
2452        AddScriptTagOptions {
2453            content: self.content,
2454            url: self.url,
2455            path: self.path,
2456            type_: self.type_,
2457        }
2458    }
2459}
2460
2461// ============================================================================
2462// EmulateMediaOptions and related enums
2463// ============================================================================
2464
2465/// Media type for `page.emulate_media()`.
2466///
2467/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2468#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2469#[serde(rename_all = "lowercase")]
2470pub enum Media {
2471    /// Emulate screen media type
2472    Screen,
2473    /// Emulate print media type
2474    Print,
2475    /// Reset media emulation to browser default (sends `"no-override"` to protocol)
2476    #[serde(rename = "no-override")]
2477    NoOverride,
2478}
2479
2480/// Preferred color scheme for `page.emulate_media()`.
2481///
2482/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2483#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2484pub enum ColorScheme {
2485    /// Emulate light color scheme
2486    #[serde(rename = "light")]
2487    Light,
2488    /// Emulate dark color scheme
2489    #[serde(rename = "dark")]
2490    Dark,
2491    /// Emulate no preference for color scheme
2492    #[serde(rename = "no-preference")]
2493    NoPreference,
2494    /// Reset color scheme to browser default
2495    #[serde(rename = "no-override")]
2496    NoOverride,
2497}
2498
2499/// Reduced motion preference for `page.emulate_media()`.
2500///
2501/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2502#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2503pub enum ReducedMotion {
2504    /// Emulate reduced motion preference
2505    #[serde(rename = "reduce")]
2506    Reduce,
2507    /// Emulate no preference for reduced motion
2508    #[serde(rename = "no-preference")]
2509    NoPreference,
2510    /// Reset reduced motion to browser default
2511    #[serde(rename = "no-override")]
2512    NoOverride,
2513}
2514
2515/// Forced colors preference for `page.emulate_media()`.
2516///
2517/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2518#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2519pub enum ForcedColors {
2520    /// Emulate active forced colors
2521    #[serde(rename = "active")]
2522    Active,
2523    /// Emulate no forced colors
2524    #[serde(rename = "none")]
2525    None_,
2526    /// Reset forced colors to browser default
2527    #[serde(rename = "no-override")]
2528    NoOverride,
2529}
2530
2531/// Options for `page.emulate_media()`.
2532///
2533/// All fields are optional. Fields that are `None` are omitted from the protocol
2534/// message (meaning they are not changed). To reset a field to browser default,
2535/// use the `NoOverride` variant.
2536///
2537/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2538#[derive(Debug, Clone, Default)]
2539pub struct EmulateMediaOptions {
2540    /// Media type to emulate (screen, print, or no-override)
2541    pub media: Option<Media>,
2542    /// Color scheme preference to emulate
2543    pub color_scheme: Option<ColorScheme>,
2544    /// Reduced motion preference to emulate
2545    pub reduced_motion: Option<ReducedMotion>,
2546    /// Forced colors preference to emulate
2547    pub forced_colors: Option<ForcedColors>,
2548}
2549
2550impl EmulateMediaOptions {
2551    /// Creates a new builder for EmulateMediaOptions
2552    pub fn builder() -> EmulateMediaOptionsBuilder {
2553        EmulateMediaOptionsBuilder::default()
2554    }
2555}
2556
2557/// Builder for EmulateMediaOptions
2558#[derive(Debug, Clone, Default)]
2559pub struct EmulateMediaOptionsBuilder {
2560    media: Option<Media>,
2561    color_scheme: Option<ColorScheme>,
2562    reduced_motion: Option<ReducedMotion>,
2563    forced_colors: Option<ForcedColors>,
2564}
2565
2566impl EmulateMediaOptionsBuilder {
2567    /// Sets the media type to emulate
2568    pub fn media(mut self, media: Media) -> Self {
2569        self.media = Some(media);
2570        self
2571    }
2572
2573    /// Sets the color scheme preference
2574    pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
2575        self.color_scheme = Some(color_scheme);
2576        self
2577    }
2578
2579    /// Sets the reduced motion preference
2580    pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
2581        self.reduced_motion = Some(reduced_motion);
2582        self
2583    }
2584
2585    /// Sets the forced colors preference
2586    pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
2587        self.forced_colors = Some(forced_colors);
2588        self
2589    }
2590
2591    /// Builds the EmulateMediaOptions
2592    pub fn build(self) -> EmulateMediaOptions {
2593        EmulateMediaOptions {
2594            media: self.media,
2595            color_scheme: self.color_scheme,
2596            reduced_motion: self.reduced_motion,
2597            forced_colors: self.forced_colors,
2598        }
2599    }
2600}
2601
2602// ============================================================================
2603// PdfOptions
2604// ============================================================================
2605
2606/// Margin options for PDF generation.
2607///
2608/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2609#[derive(Debug, Clone, Default, Serialize)]
2610pub struct PdfMargin {
2611    /// Top margin (e.g. `"1in"`)
2612    #[serde(skip_serializing_if = "Option::is_none")]
2613    pub top: Option<String>,
2614    /// Right margin
2615    #[serde(skip_serializing_if = "Option::is_none")]
2616    pub right: Option<String>,
2617    /// Bottom margin
2618    #[serde(skip_serializing_if = "Option::is_none")]
2619    pub bottom: Option<String>,
2620    /// Left margin
2621    #[serde(skip_serializing_if = "Option::is_none")]
2622    pub left: Option<String>,
2623}
2624
2625/// Options for generating a PDF from a page.
2626///
2627/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
2628/// Firefox or WebKit will result in an error.
2629///
2630/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2631#[derive(Debug, Clone, Default)]
2632pub struct PdfOptions {
2633    /// If specified, the PDF will also be saved to this file path.
2634    pub path: Option<std::path::PathBuf>,
2635    /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
2636    pub scale: Option<f64>,
2637    /// Whether to display header and footer (default false).
2638    pub display_header_footer: Option<bool>,
2639    /// HTML template for the print header. Should be valid HTML.
2640    pub header_template: Option<String>,
2641    /// HTML template for the print footer.
2642    pub footer_template: Option<String>,
2643    /// Whether to print background graphics (default false).
2644    pub print_background: Option<bool>,
2645    /// Paper orientation — `true` for landscape (default false).
2646    pub landscape: Option<bool>,
2647    /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
2648    pub page_ranges: Option<String>,
2649    /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
2650    pub format: Option<String>,
2651    /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
2652    pub width: Option<String>,
2653    /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
2654    pub height: Option<String>,
2655    /// Whether or not to prefer page size as defined by CSS.
2656    pub prefer_css_page_size: Option<bool>,
2657    /// Paper margins, defaulting to none.
2658    pub margin: Option<PdfMargin>,
2659}
2660
2661impl PdfOptions {
2662    /// Creates a new builder for PdfOptions
2663    pub fn builder() -> PdfOptionsBuilder {
2664        PdfOptionsBuilder::default()
2665    }
2666}
2667
2668/// Builder for PdfOptions
2669#[derive(Debug, Clone, Default)]
2670pub struct PdfOptionsBuilder {
2671    path: Option<std::path::PathBuf>,
2672    scale: Option<f64>,
2673    display_header_footer: Option<bool>,
2674    header_template: Option<String>,
2675    footer_template: Option<String>,
2676    print_background: Option<bool>,
2677    landscape: Option<bool>,
2678    page_ranges: Option<String>,
2679    format: Option<String>,
2680    width: Option<String>,
2681    height: Option<String>,
2682    prefer_css_page_size: Option<bool>,
2683    margin: Option<PdfMargin>,
2684}
2685
2686impl PdfOptionsBuilder {
2687    /// Sets the file path for saving the PDF
2688    pub fn path(mut self, path: std::path::PathBuf) -> Self {
2689        self.path = Some(path);
2690        self
2691    }
2692
2693    /// Sets the scale of the webpage rendering
2694    pub fn scale(mut self, scale: f64) -> Self {
2695        self.scale = Some(scale);
2696        self
2697    }
2698
2699    /// Sets whether to display header and footer
2700    pub fn display_header_footer(mut self, display: bool) -> Self {
2701        self.display_header_footer = Some(display);
2702        self
2703    }
2704
2705    /// Sets the HTML template for the print header
2706    pub fn header_template(mut self, template: impl Into<String>) -> Self {
2707        self.header_template = Some(template.into());
2708        self
2709    }
2710
2711    /// Sets the HTML template for the print footer
2712    pub fn footer_template(mut self, template: impl Into<String>) -> Self {
2713        self.footer_template = Some(template.into());
2714        self
2715    }
2716
2717    /// Sets whether to print background graphics
2718    pub fn print_background(mut self, print: bool) -> Self {
2719        self.print_background = Some(print);
2720        self
2721    }
2722
2723    /// Sets whether to use landscape orientation
2724    pub fn landscape(mut self, landscape: bool) -> Self {
2725        self.landscape = Some(landscape);
2726        self
2727    }
2728
2729    /// Sets the page ranges to print
2730    pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
2731        self.page_ranges = Some(ranges.into());
2732        self
2733    }
2734
2735    /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
2736    pub fn format(mut self, format: impl Into<String>) -> Self {
2737        self.format = Some(format.into());
2738        self
2739    }
2740
2741    /// Sets the paper width
2742    pub fn width(mut self, width: impl Into<String>) -> Self {
2743        self.width = Some(width.into());
2744        self
2745    }
2746
2747    /// Sets the paper height
2748    pub fn height(mut self, height: impl Into<String>) -> Self {
2749        self.height = Some(height.into());
2750        self
2751    }
2752
2753    /// Sets whether to prefer page size as defined by CSS
2754    pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
2755        self.prefer_css_page_size = Some(prefer);
2756        self
2757    }
2758
2759    /// Sets the paper margins
2760    pub fn margin(mut self, margin: PdfMargin) -> Self {
2761        self.margin = Some(margin);
2762        self
2763    }
2764
2765    /// Builds the PdfOptions
2766    pub fn build(self) -> PdfOptions {
2767        PdfOptions {
2768            path: self.path,
2769            scale: self.scale,
2770            display_header_footer: self.display_header_footer,
2771            header_template: self.header_template,
2772            footer_template: self.footer_template,
2773            print_background: self.print_background,
2774            landscape: self.landscape,
2775            page_ranges: self.page_ranges,
2776            format: self.format,
2777            width: self.width,
2778            height: self.height,
2779            prefer_css_page_size: self.prefer_css_page_size,
2780            margin: self.margin,
2781        }
2782    }
2783}
2784
2785/// Response from navigation operations.
2786///
2787/// Returned from `page.goto()`, `page.reload()`, `page.go_back()`, and similar
2788/// navigation methods. Provides access to the HTTP response status, headers, and body.
2789///
2790/// See: <https://playwright.dev/docs/api/class-response>
2791#[derive(Clone)]
2792pub struct Response {
2793    url: String,
2794    status: u16,
2795    status_text: String,
2796    ok: bool,
2797    headers: std::collections::HashMap<String, String>,
2798    /// Reference to the backing channel owner for RPC calls (body, rawHeaders, etc.)
2799    /// Stored as the generic trait object so it can be downcast to ResponseObject when needed.
2800    response_channel_owner: Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
2801}
2802
2803impl Response {
2804    /// Creates a new Response from protocol data.
2805    ///
2806    /// This is used internally when constructing a Response from the protocol
2807    /// initializer (e.g., after `goto` or `reload`).
2808    pub(crate) fn new(
2809        url: String,
2810        status: u16,
2811        status_text: String,
2812        headers: std::collections::HashMap<String, String>,
2813        response_channel_owner: Option<
2814            std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>,
2815        >,
2816    ) -> Self {
2817        Self {
2818            url,
2819            status,
2820            status_text,
2821            ok: (200..300).contains(&status),
2822            headers,
2823            response_channel_owner,
2824        }
2825    }
2826}
2827
2828impl Response {
2829    /// Returns the URL of the response.
2830    ///
2831    /// See: <https://playwright.dev/docs/api/class-response#response-url>
2832    pub fn url(&self) -> &str {
2833        &self.url
2834    }
2835
2836    /// Returns the HTTP status code.
2837    ///
2838    /// See: <https://playwright.dev/docs/api/class-response#response-status>
2839    pub fn status(&self) -> u16 {
2840        self.status
2841    }
2842
2843    /// Returns the HTTP status text.
2844    ///
2845    /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
2846    pub fn status_text(&self) -> &str {
2847        &self.status_text
2848    }
2849
2850    /// Returns whether the response was successful (status 200-299).
2851    ///
2852    /// See: <https://playwright.dev/docs/api/class-response#response-ok>
2853    pub fn ok(&self) -> bool {
2854        self.ok
2855    }
2856
2857    /// Returns the response headers as a HashMap.
2858    ///
2859    /// Note: these are the headers from the protocol initializer. For the full
2860    /// raw headers (including duplicates), use `headers_array()` or `all_headers()`.
2861    ///
2862    /// See: <https://playwright.dev/docs/api/class-response#response-headers>
2863    pub fn headers(&self) -> &std::collections::HashMap<String, String> {
2864        &self.headers
2865    }
2866
2867    /// Returns the [`Request`](crate::protocol::Request) that triggered this response.
2868    ///
2869    /// Navigates the protocol object hierarchy: ResponseObject → parent (Request).
2870    ///
2871    /// See: <https://playwright.dev/docs/api/class-response#response-request>
2872    pub fn request(&self) -> Option<crate::protocol::Request> {
2873        let owner = self.response_channel_owner.as_ref()?;
2874        let parent = owner.parent()?;
2875        parent
2876            .as_any()
2877            .downcast_ref::<crate::protocol::Request>()
2878            .cloned()
2879    }
2880
2881    /// Returns the [`Frame`](crate::protocol::Frame) that initiated the request for this response.
2882    ///
2883    /// Navigates the protocol object hierarchy: ResponseObject → Request → Frame.
2884    ///
2885    /// See: <https://playwright.dev/docs/api/class-response#response-frame>
2886    pub fn frame(&self) -> Option<crate::protocol::Frame> {
2887        let request = self.request()?;
2888        request.frame()
2889    }
2890
2891    /// Returns the backing `ResponseObject`, or an error if unavailable.
2892    pub(crate) fn response_object(&self) -> crate::error::Result<crate::protocol::ResponseObject> {
2893        let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
2894            crate::error::Error::ProtocolError(
2895                "Response has no backing protocol object".to_string(),
2896            )
2897        })?;
2898        arc.as_any()
2899            .downcast_ref::<crate::protocol::ResponseObject>()
2900            .cloned()
2901            .ok_or_else(|| {
2902                crate::error::Error::ProtocolError(
2903                    "Response backing object is not a ResponseObject".to_string(),
2904                )
2905            })
2906    }
2907
2908    /// Returns TLS/SSL security details for HTTPS connections, or `None` for HTTP.
2909    ///
2910    /// See: <https://playwright.dev/docs/api/class-response#response-security-details>
2911    pub async fn security_details(
2912        &self,
2913    ) -> crate::error::Result<Option<crate::protocol::response::SecurityDetails>> {
2914        self.response_object()?.security_details().await
2915    }
2916
2917    /// Returns the server's IP address and port, or `None`.
2918    ///
2919    /// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
2920    pub async fn server_addr(
2921        &self,
2922    ) -> crate::error::Result<Option<crate::protocol::response::RemoteAddr>> {
2923        self.response_object()?.server_addr().await
2924    }
2925
2926    /// Waits for this response to finish loading.
2927    ///
2928    /// For responses obtained from navigation methods (`goto`, `reload`), the response
2929    /// is already finished when returned. For responses from `on_response` handlers,
2930    /// the body may still be loading.
2931    ///
2932    /// See: <https://playwright.dev/docs/api/class-response#response-finished>
2933    pub async fn finished(&self) -> crate::error::Result<()> {
2934        // The Playwright protocol dispatches `requestFinished` as a separate event
2935        // rather than exposing a `finished` RPC method on Response.
2936        // For responses from goto/reload, the response is already complete.
2937        // TODO: For on_response handlers, implement proper waiting via requestFinished event.
2938        Ok(())
2939    }
2940
2941    /// Returns the response body as raw bytes.
2942    ///
2943    /// Makes an RPC call to the Playwright server to fetch the response body.
2944    ///
2945    /// # Errors
2946    ///
2947    /// Returns an error if:
2948    /// - No backing protocol object is available (edge case)
2949    /// - The RPC call to the server fails
2950    /// - The base64 response cannot be decoded
2951    ///
2952    /// See: <https://playwright.dev/docs/api/class-response#response-body>
2953    pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
2954        self.response_object()?.body().await
2955    }
2956
2957    /// Returns the response body as a UTF-8 string.
2958    ///
2959    /// Calls `body()` then converts bytes to a UTF-8 string.
2960    ///
2961    /// # Errors
2962    ///
2963    /// Returns an error if:
2964    /// - `body()` fails
2965    /// - The body is not valid UTF-8
2966    ///
2967    /// See: <https://playwright.dev/docs/api/class-response#response-text>
2968    pub async fn text(&self) -> crate::error::Result<String> {
2969        let bytes = self.body().await?;
2970        String::from_utf8(bytes).map_err(|e| {
2971            crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
2972        })
2973    }
2974
2975    /// Parses the response body as JSON and deserializes it into type `T`.
2976    ///
2977    /// Calls `text()` then uses `serde_json` to deserialize the body.
2978    ///
2979    /// # Errors
2980    ///
2981    /// Returns an error if:
2982    /// - `text()` fails
2983    /// - The body is not valid JSON or doesn't match the expected type
2984    ///
2985    /// See: <https://playwright.dev/docs/api/class-response#response-json>
2986    pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
2987        let text = self.text().await?;
2988        serde_json::from_str(&text).map_err(|e| {
2989            crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
2990        })
2991    }
2992
2993    /// Returns all response headers as name-value pairs, preserving duplicates.
2994    ///
2995    /// Makes an RPC call for `"rawHeaders"` which returns the complete header list.
2996    ///
2997    /// # Errors
2998    ///
2999    /// Returns an error if:
3000    /// - No backing protocol object is available (edge case)
3001    /// - The RPC call to the server fails
3002    ///
3003    /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
3004    pub async fn headers_array(
3005        &self,
3006    ) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
3007        self.response_object()?.raw_headers().await
3008    }
3009
3010    /// Returns all response headers merged into a HashMap with lowercase keys.
3011    ///
3012    /// When multiple headers have the same name, their values are joined with `, `.
3013    /// This matches the behavior of `response.allHeaders()` in other Playwright bindings.
3014    ///
3015    /// # Errors
3016    ///
3017    /// Returns an error if:
3018    /// - No backing protocol object is available (edge case)
3019    /// - The RPC call to the server fails
3020    ///
3021    /// See: <https://playwright.dev/docs/api/class-response#response-all-headers>
3022    pub async fn all_headers(
3023        &self,
3024    ) -> crate::error::Result<std::collections::HashMap<String, String>> {
3025        let entries = self.headers_array().await?;
3026        let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
3027        for entry in entries {
3028            let key = entry.name.to_lowercase();
3029            map.entry(key)
3030                .and_modify(|v| {
3031                    v.push_str(", ");
3032                    v.push_str(&entry.value);
3033                })
3034                .or_insert(entry.value);
3035        }
3036        Ok(map)
3037    }
3038
3039    /// Returns the value for a single response header, or `None` if not present.
3040    ///
3041    /// The lookup is case-insensitive.
3042    ///
3043    /// # Errors
3044    ///
3045    /// Returns an error if:
3046    /// - No backing protocol object is available (edge case)
3047    /// - The RPC call to the server fails
3048    ///
3049    /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
3050    /// Returns the value for a single response header, or `None` if not present.
3051    ///
3052    /// The lookup is case-insensitive. When multiple headers share the same name,
3053    /// their values are joined with `, ` (matching Playwright's behavior).
3054    ///
3055    /// Uses the raw headers from the server for accurate results.
3056    ///
3057    /// # Errors
3058    ///
3059    /// Returns an error if the underlying `headers_array()` RPC call fails.
3060    ///
3061    /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
3062    pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
3063        let entries = self.headers_array().await?;
3064        let name_lower = name.to_lowercase();
3065        let mut values: Vec<String> = entries
3066            .into_iter()
3067            .filter(|h| h.name.to_lowercase() == name_lower)
3068            .map(|h| h.value)
3069            .collect();
3070
3071        if values.is_empty() {
3072            Ok(None)
3073        } else if values.len() == 1 {
3074            Ok(Some(values.remove(0)))
3075        } else {
3076            Ok(Some(values.join(", ")))
3077        }
3078    }
3079}
3080
3081impl std::fmt::Debug for Response {
3082    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3083        f.debug_struct("Response")
3084            .field("url", &self.url)
3085            .field("status", &self.status)
3086            .field("status_text", &self.status_text)
3087            .field("ok", &self.ok)
3088            .finish_non_exhaustive()
3089    }
3090}
3091
3092/// Shared helper: store timeout locally and notify the Playwright server.
3093/// Used by both Page and BrowserContext timeout setters.
3094pub(crate) async fn set_timeout_and_notify(
3095    channel: &crate::server::channel::Channel,
3096    method: &str,
3097    timeout: f64,
3098) {
3099    if let Err(e) = channel
3100        .send_no_result(method, serde_json::json!({ "timeout": timeout }))
3101        .await
3102    {
3103        tracing::warn!("{} send error: {}", method, e);
3104    }
3105}