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::{Dialog, Download, Route};
8use crate::server::channel::Channel;
9use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
10use base64::Engine;
11use serde::Deserialize;
12use serde_json::Value;
13use std::any::Any;
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::{Arc, Mutex, RwLock};
17
18/// Page represents a web page within a browser context.
19///
20/// A Page is created when you call `BrowserContext::new_page()` or `Browser::new_page()`.
21/// Each page is an isolated tab/window within its parent context.
22///
23/// Initially, pages are navigated to "about:blank". Use navigation methods
24/// (implemented in Phase 3) to navigate to URLs.
25///
26/// # Example
27///
28/// ```ignore
29/// use playwright_rs::protocol::{Playwright, ScreenshotOptions, ScreenshotType, AddStyleTagOptions};
30/// use std::path::PathBuf;
31///
32/// #[tokio::main]
33/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
34///     let playwright = Playwright::launch().await?;
35///     let browser = playwright.chromium().launch().await?;
36///     let page = browser.new_page().await?;
37///
38///     // Demonstrate url() - initially at about:blank
39///     assert_eq!(page.url(), "about:blank");
40///
41///     // Demonstrate goto() - navigate to a page
42///     let html = r#"
43///         <html>
44///             <head><title>Test Page</title></head>
45///             <body>
46///                 <h1 id="heading">Hello World</h1>
47///                 <p>First paragraph</p>
48///                 <p>Second paragraph</p>
49///                 <button onclick="alert('Alert!')">Alert</button>
50///                 <a href="data:text/plain,file" download="test.txt">Download</a>
51///             </body>
52///         </html>
53///     "#;
54///     // Data URLs may not return a response (this is normal)
55///     let _response = page.goto(&format!("data:text/html,{}", html), None).await?;
56///
57///     // Demonstrate title()
58///     let title = page.title().await?;
59///     assert_eq!(title, "Test Page");
60///
61///     // Demonstrate locator()
62///     let heading = page.locator("#heading").await;
63///     let text = heading.text_content().await?;
64///     assert_eq!(text, Some("Hello World".to_string()));
65///
66///     // Demonstrate query_selector()
67///     let element = page.query_selector("h1").await?;
68///     assert!(element.is_some(), "Should find the h1 element");
69///
70///     // Demonstrate query_selector_all()
71///     let paragraphs = page.query_selector_all("p").await?;
72///     assert_eq!(paragraphs.len(), 2);
73///
74///     // Demonstrate evaluate()
75///     page.evaluate("console.log('Hello from Playwright!')").await?;
76///
77///     // Demonstrate evaluate_value()
78///     let result = page.evaluate_value("1 + 1").await?;
79///     assert_eq!(result, "2");
80///
81///     // Demonstrate screenshot()
82///     let bytes = page.screenshot(None).await?;
83///     assert!(!bytes.is_empty());
84///
85///     // Demonstrate screenshot_to_file()
86///     let temp_dir = std::env::temp_dir();
87///     let path = temp_dir.join("playwright_doctest_screenshot.png");
88///     let bytes = page.screenshot_to_file(&path, Some(
89///         ScreenshotOptions::builder()
90///             .screenshot_type(ScreenshotType::Png)
91///             .build()
92///     )).await?;
93///     assert!(!bytes.is_empty());
94///
95///     // Demonstrate reload()
96///     // Data URLs may not return a response on reload (this is normal)
97///     let _response = page.reload(None).await?;
98///
99///     // Demonstrate route() - network interception
100///     page.route("**/*.png", |route| async move {
101///         route.abort(None).await
102///     }).await?;
103///
104///     // Demonstrate on_download() - download handler
105///     page.on_download(|download| async move {
106///         println!("Download started: {}", download.url());
107///         Ok(())
108///     }).await?;
109///
110///     // Demonstrate on_dialog() - dialog handler
111///     page.on_dialog(|dialog| async move {
112///         println!("Dialog: {} - {}", dialog.type_(), dialog.message());
113///         dialog.accept(None).await
114///     }).await?;
115///
116///     // Demonstrate add_style_tag() - inject CSS
117///     page.add_style_tag(
118///         AddStyleTagOptions::builder()
119///             .content("body { background-color: blue; }")
120///             .build()
121///     ).await?;
122///
123///     // Demonstrate close()
124///     page.close().await?;
125///
126///     browser.close().await?;
127///     Ok(())
128/// }
129/// ```
130///
131/// See: <https://playwright.dev/docs/api/class-page>
132#[derive(Clone)]
133pub struct Page {
134    base: ChannelOwnerImpl,
135    /// Current URL of the page
136    /// Wrapped in RwLock to allow updates from events
137    url: Arc<RwLock<String>>,
138    /// GUID of the main frame
139    main_frame_guid: Arc<str>,
140    /// Route handlers for network interception
141    route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
142    /// Download event handlers
143    download_handlers: Arc<Mutex<Vec<DownloadHandler>>>,
144    /// Dialog event handlers
145    dialog_handlers: Arc<Mutex<Vec<DialogHandler>>>,
146}
147
148/// Type alias for boxed route handler future
149type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
150
151/// Type alias for boxed download handler future
152type DownloadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
153
154/// Type alias for boxed dialog handler future
155type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
156
157/// Storage for a single route handler
158#[derive(Clone)]
159struct RouteHandlerEntry {
160    pattern: String,
161    handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
162}
163
164/// Download event handler
165type DownloadHandler = Arc<dyn Fn(Download) -> DownloadHandlerFuture + Send + Sync>;
166
167/// Dialog event handler
168type DialogHandler = Arc<dyn Fn(Dialog) -> DialogHandlerFuture + Send + Sync>;
169
170impl Page {
171    /// Creates a new Page from protocol initialization
172    ///
173    /// This is called by the object factory when the server sends a `__create__` message
174    /// for a Page object.
175    ///
176    /// # Arguments
177    ///
178    /// * `parent` - The parent BrowserContext object
179    /// * `type_name` - The protocol type name ("Page")
180    /// * `guid` - The unique identifier for this page
181    /// * `initializer` - The initialization data from the server
182    ///
183    /// # Errors
184    ///
185    /// Returns error if initializer is malformed
186    pub fn new(
187        parent: Arc<dyn ChannelOwner>,
188        type_name: String,
189        guid: Arc<str>,
190        initializer: Value,
191    ) -> Result<Self> {
192        // Extract mainFrame GUID from initializer
193        let main_frame_guid: Arc<str> =
194            Arc::from(initializer["mainFrame"]["guid"].as_str().ok_or_else(|| {
195                crate::error::Error::ProtocolError(
196                    "Page initializer missing 'mainFrame.guid' field".to_string(),
197                )
198            })?);
199
200        let base = ChannelOwnerImpl::new(
201            ParentOrConnection::Parent(parent),
202            type_name,
203            guid,
204            initializer,
205        );
206
207        // Initialize URL to about:blank
208        let url = Arc::new(RwLock::new("about:blank".to_string()));
209
210        // Initialize empty route handlers
211        let route_handlers = Arc::new(Mutex::new(Vec::new()));
212
213        // Initialize empty event handlers
214        let download_handlers = Arc::new(Mutex::new(Vec::new()));
215        let dialog_handlers = Arc::new(Mutex::new(Vec::new()));
216
217        Ok(Self {
218            base,
219            url,
220            main_frame_guid,
221            route_handlers,
222            download_handlers,
223            dialog_handlers,
224        })
225    }
226
227    /// Returns the channel for sending protocol messages
228    ///
229    /// Used internally for sending RPC calls to the page.
230    fn channel(&self) -> &Channel {
231        self.base.channel()
232    }
233
234    /// Returns the main frame of the page.
235    ///
236    /// The main frame is where navigation and DOM operations actually happen.
237    pub async fn main_frame(&self) -> Result<crate::protocol::Frame> {
238        // Get the Frame object from the connection's object registry
239        let frame_arc = self.connection().get_object(&self.main_frame_guid).await?;
240
241        // Downcast to Frame
242        let frame = frame_arc
243            .as_any()
244            .downcast_ref::<crate::protocol::Frame>()
245            .ok_or_else(|| {
246                crate::error::Error::ProtocolError(format!(
247                    "Expected Frame object, got {}",
248                    frame_arc.type_name()
249                ))
250            })?;
251
252        Ok(frame.clone())
253    }
254
255    /// Returns the current URL of the page.
256    ///
257    /// This returns the last committed URL. Initially, pages are at "about:blank".
258    ///
259    /// See: <https://playwright.dev/docs/api/class-page#page-url>
260    pub fn url(&self) -> String {
261        // Return a clone of the current URL
262        self.url.read().unwrap().clone()
263    }
264
265    /// Closes the page.
266    ///
267    /// This is a graceful operation that sends a close command to the page
268    /// and waits for it to shut down properly.
269    ///
270    /// # Errors
271    ///
272    /// Returns error if:
273    /// - Page has already been closed
274    /// - Communication with browser process fails
275    ///
276    /// See: <https://playwright.dev/docs/api/class-page#page-close>
277    pub async fn close(&self) -> Result<()> {
278        // Send close RPC to server
279        self.channel()
280            .send_no_result("close", serde_json::json!({}))
281            .await
282    }
283
284    /// Navigates to the specified URL.
285    ///
286    /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
287    /// about:blank). This matches Playwright's behavior across all language bindings.
288    ///
289    /// # Arguments
290    ///
291    /// * `url` - The URL to navigate to
292    /// * `options` - Optional navigation options (timeout, wait_until)
293    ///
294    /// # Errors
295    ///
296    /// Returns error if:
297    /// - URL is invalid
298    /// - Navigation timeout (default 30s)
299    /// - Network error
300    ///
301    /// See: <https://playwright.dev/docs/api/class-page#page-goto>
302    pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
303        // Delegate to main frame
304        let frame = self.main_frame().await.map_err(|e| match e {
305            Error::TargetClosed { context, .. } => Error::TargetClosed {
306                target_type: "Page".to_string(),
307                context,
308            },
309            other => other,
310        })?;
311
312        let response = frame.goto(url, options).await.map_err(|e| match e {
313            Error::TargetClosed { context, .. } => Error::TargetClosed {
314                target_type: "Page".to_string(),
315                context,
316            },
317            other => other,
318        })?;
319
320        // Update the page's URL if we got a response
321        if let Some(ref resp) = response {
322            if let Ok(mut page_url) = self.url.write() {
323                *page_url = resp.url().to_string();
324            }
325        }
326
327        Ok(response)
328    }
329
330    /// Returns the browser context that the page belongs to.
331    pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
332        let parent = self.base.parent().ok_or_else(|| Error::TargetClosed {
333            target_type: "Page".into(),
334            context: "Parent context not found".into(),
335        })?;
336
337        let context = parent
338            .as_any()
339            .downcast_ref::<crate::protocol::BrowserContext>()
340            .ok_or_else(|| {
341                Error::ProtocolError("Page parent is not a BrowserContext".to_string())
342            })?;
343
344        Ok(context.clone())
345    }
346
347    /// Pauses script execution.
348    ///
349    /// Playwright will stop executing the script and wait for the user to either press
350    /// "Resume" in the page overlay or in the debugger.
351    ///
352    /// See: <https://playwright.dev/docs/api/class-page#page-pause>
353    pub async fn pause(&self) -> Result<()> {
354        self.context()?.pause().await
355    }
356
357    /// Returns the page's title.
358    ///
359    /// See: <https://playwright.dev/docs/api/class-page#page-title>
360    pub async fn title(&self) -> Result<String> {
361        // Delegate to main frame
362        let frame = self.main_frame().await?;
363        frame.title().await
364    }
365
366    /// Creates a locator for finding elements on the page.
367    ///
368    /// Locators are the central piece of Playwright's auto-waiting and retry-ability.
369    /// They don't execute queries until an action is performed.
370    ///
371    /// # Arguments
372    ///
373    /// * `selector` - CSS selector or other locating strategy
374    ///
375    /// See: <https://playwright.dev/docs/api/class-page#page-locator>
376    pub async fn locator(&self, selector: &str) -> crate::protocol::Locator {
377        // Get the main frame
378        let frame = self.main_frame().await.expect("Main frame should exist");
379
380        crate::protocol::Locator::new(Arc::new(frame), selector.to_string())
381    }
382
383    /// Returns the keyboard instance for low-level keyboard control.
384    ///
385    /// See: <https://playwright.dev/docs/api/class-page#page-keyboard>
386    pub fn keyboard(&self) -> crate::protocol::Keyboard {
387        crate::protocol::Keyboard::new(self.clone())
388    }
389
390    /// Returns the mouse instance for low-level mouse control.
391    ///
392    /// See: <https://playwright.dev/docs/api/class-page#page-mouse>
393    pub fn mouse(&self) -> crate::protocol::Mouse {
394        crate::protocol::Mouse::new(self.clone())
395    }
396
397    // Internal keyboard methods (called by Keyboard struct)
398
399    pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
400        self.channel()
401            .send_no_result(
402                "keyboardDown",
403                serde_json::json!({
404                    "key": key
405                }),
406            )
407            .await
408    }
409
410    pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
411        self.channel()
412            .send_no_result(
413                "keyboardUp",
414                serde_json::json!({
415                    "key": key
416                }),
417            )
418            .await
419    }
420
421    pub(crate) async fn keyboard_press(
422        &self,
423        key: &str,
424        options: Option<crate::protocol::KeyboardOptions>,
425    ) -> Result<()> {
426        let mut params = serde_json::json!({
427            "key": key
428        });
429
430        if let Some(opts) = options {
431            let opts_json = opts.to_json();
432            if let Some(obj) = params.as_object_mut() {
433                if let Some(opts_obj) = opts_json.as_object() {
434                    obj.extend(opts_obj.clone());
435                }
436            }
437        }
438
439        self.channel().send_no_result("keyboardPress", params).await
440    }
441
442    pub(crate) async fn keyboard_type(
443        &self,
444        text: &str,
445        options: Option<crate::protocol::KeyboardOptions>,
446    ) -> Result<()> {
447        let mut params = serde_json::json!({
448            "text": text
449        });
450
451        if let Some(opts) = options {
452            let opts_json = opts.to_json();
453            if let Some(obj) = params.as_object_mut() {
454                if let Some(opts_obj) = opts_json.as_object() {
455                    obj.extend(opts_obj.clone());
456                }
457            }
458        }
459
460        self.channel().send_no_result("keyboardType", params).await
461    }
462
463    pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
464        self.channel()
465            .send_no_result(
466                "keyboardInsertText",
467                serde_json::json!({
468                    "text": text
469                }),
470            )
471            .await
472    }
473
474    // Internal mouse methods (called by Mouse struct)
475
476    pub(crate) async fn mouse_move(
477        &self,
478        x: i32,
479        y: i32,
480        options: Option<crate::protocol::MouseOptions>,
481    ) -> Result<()> {
482        let mut params = serde_json::json!({
483            "x": x,
484            "y": y
485        });
486
487        if let Some(opts) = options {
488            let opts_json = opts.to_json();
489            if let Some(obj) = params.as_object_mut() {
490                if let Some(opts_obj) = opts_json.as_object() {
491                    obj.extend(opts_obj.clone());
492                }
493            }
494        }
495
496        self.channel().send_no_result("mouseMove", params).await
497    }
498
499    pub(crate) async fn mouse_click(
500        &self,
501        x: i32,
502        y: i32,
503        options: Option<crate::protocol::MouseOptions>,
504    ) -> Result<()> {
505        let mut params = serde_json::json!({
506            "x": x,
507            "y": y
508        });
509
510        if let Some(opts) = options {
511            let opts_json = opts.to_json();
512            if let Some(obj) = params.as_object_mut() {
513                if let Some(opts_obj) = opts_json.as_object() {
514                    obj.extend(opts_obj.clone());
515                }
516            }
517        }
518
519        self.channel().send_no_result("mouseClick", params).await
520    }
521
522    pub(crate) async fn mouse_dblclick(
523        &self,
524        x: i32,
525        y: i32,
526        options: Option<crate::protocol::MouseOptions>,
527    ) -> Result<()> {
528        let mut params = serde_json::json!({
529            "x": x,
530            "y": y,
531            "clickCount": 2
532        });
533
534        if let Some(opts) = options {
535            let opts_json = opts.to_json();
536            if let Some(obj) = params.as_object_mut() {
537                if let Some(opts_obj) = opts_json.as_object() {
538                    obj.extend(opts_obj.clone());
539                }
540            }
541        }
542
543        self.channel().send_no_result("mouseClick", params).await
544    }
545
546    pub(crate) async fn mouse_down(
547        &self,
548        options: Option<crate::protocol::MouseOptions>,
549    ) -> Result<()> {
550        let mut params = serde_json::json!({});
551
552        if let Some(opts) = options {
553            let opts_json = opts.to_json();
554            if let Some(obj) = params.as_object_mut() {
555                if let Some(opts_obj) = opts_json.as_object() {
556                    obj.extend(opts_obj.clone());
557                }
558            }
559        }
560
561        self.channel().send_no_result("mouseDown", params).await
562    }
563
564    pub(crate) async fn mouse_up(
565        &self,
566        options: Option<crate::protocol::MouseOptions>,
567    ) -> Result<()> {
568        let mut params = serde_json::json!({});
569
570        if let Some(opts) = options {
571            let opts_json = opts.to_json();
572            if let Some(obj) = params.as_object_mut() {
573                if let Some(opts_obj) = opts_json.as_object() {
574                    obj.extend(opts_obj.clone());
575                }
576            }
577        }
578
579        self.channel().send_no_result("mouseUp", params).await
580    }
581
582    pub(crate) async fn mouse_wheel(&self, delta_x: i32, delta_y: i32) -> Result<()> {
583        self.channel()
584            .send_no_result(
585                "mouseWheel",
586                serde_json::json!({
587                    "deltaX": delta_x,
588                    "deltaY": delta_y
589                }),
590            )
591            .await
592    }
593
594    /// Reloads the current page.
595    ///
596    /// # Arguments
597    ///
598    /// * `options` - Optional reload options (timeout, wait_until)
599    ///
600    /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
601    /// about:blank). This matches Playwright's behavior across all language bindings.
602    ///
603    /// See: <https://playwright.dev/docs/api/class-page#page-reload>
604    pub async fn reload(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
605        // Build params
606        let mut params = serde_json::json!({});
607
608        if let Some(opts) = options {
609            if let Some(timeout) = opts.timeout {
610                params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
611            } else {
612                params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
613            }
614            if let Some(wait_until) = opts.wait_until {
615                params["waitUntil"] = serde_json::json!(wait_until.as_str());
616            }
617        } else {
618            params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
619        }
620
621        // Send reload RPC directly to Page (not Frame!)
622        #[derive(Deserialize)]
623        struct ReloadResponse {
624            response: Option<ResponseReference>,
625        }
626
627        #[derive(Deserialize)]
628        struct ResponseReference {
629            #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
630            guid: Arc<str>,
631        }
632
633        let reload_result: ReloadResponse = self.channel().send("reload", params).await?;
634
635        // If reload returned a response, get the Response object
636        if let Some(response_ref) = reload_result.response {
637            // Wait for Response object to be created
638            let response_arc = {
639                let mut attempts = 0;
640                let max_attempts = 20;
641                loop {
642                    match self.connection().get_object(&response_ref.guid).await {
643                        Ok(obj) => break obj,
644                        Err(_) if attempts < max_attempts => {
645                            attempts += 1;
646                            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
647                        }
648                        Err(e) => return Err(e),
649                    }
650                }
651            };
652
653            // Extract response data from initializer
654            let initializer = response_arc.initializer();
655
656            let status = initializer["status"].as_u64().ok_or_else(|| {
657                crate::error::Error::ProtocolError("Response missing status".to_string())
658            })? as u16;
659
660            let headers = initializer["headers"]
661                .as_array()
662                .ok_or_else(|| {
663                    crate::error::Error::ProtocolError("Response missing headers".to_string())
664                })?
665                .iter()
666                .filter_map(|h| {
667                    let name = h["name"].as_str()?;
668                    let value = h["value"].as_str()?;
669                    Some((name.to_string(), value.to_string()))
670                })
671                .collect();
672
673            let response = Response {
674                url: initializer["url"]
675                    .as_str()
676                    .ok_or_else(|| {
677                        crate::error::Error::ProtocolError("Response missing url".to_string())
678                    })?
679                    .to_string(),
680                status,
681                status_text: initializer["statusText"].as_str().unwrap_or("").to_string(),
682                ok: (200..300).contains(&status),
683                headers,
684            };
685
686            // Update the page's URL
687            if let Ok(mut page_url) = self.url.write() {
688                *page_url = response.url().to_string();
689            }
690
691            Ok(Some(response))
692        } else {
693            // Reload returned null (e.g., data URLs, about:blank)
694            // This is a valid result, not an error
695            Ok(None)
696        }
697    }
698
699    /// Returns the first element matching the selector, or None if not found.
700    ///
701    /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
702    pub async fn query_selector(
703        &self,
704        selector: &str,
705    ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
706        let frame = self.main_frame().await?;
707        frame.query_selector(selector).await
708    }
709
710    /// Returns all elements matching the selector.
711    ///
712    /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
713    pub async fn query_selector_all(
714        &self,
715        selector: &str,
716    ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
717        let frame = self.main_frame().await?;
718        frame.query_selector_all(selector).await
719    }
720
721    /// Takes a screenshot of the page and returns the image bytes.
722    ///
723    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
724    pub async fn screenshot(
725        &self,
726        options: Option<crate::protocol::ScreenshotOptions>,
727    ) -> Result<Vec<u8>> {
728        let params = if let Some(opts) = options {
729            opts.to_json()
730        } else {
731            // Default to PNG with required timeout
732            serde_json::json!({
733                "type": "png",
734                "timeout": crate::DEFAULT_TIMEOUT_MS
735            })
736        };
737
738        #[derive(Deserialize)]
739        struct ScreenshotResponse {
740            binary: String,
741        }
742
743        let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
744
745        // Decode base64 to bytes
746        let bytes = base64::prelude::BASE64_STANDARD
747            .decode(&response.binary)
748            .map_err(|e| {
749                crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
750            })?;
751
752        Ok(bytes)
753    }
754
755    /// Takes a screenshot and saves it to a file, also returning the bytes.
756    ///
757    /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
758    pub async fn screenshot_to_file(
759        &self,
760        path: &std::path::Path,
761        options: Option<crate::protocol::ScreenshotOptions>,
762    ) -> Result<Vec<u8>> {
763        // Get the screenshot bytes
764        let bytes = self.screenshot(options).await?;
765
766        // Write to file
767        tokio::fs::write(path, &bytes).await.map_err(|e| {
768            crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
769        })?;
770
771        Ok(bytes)
772    }
773
774    /// Evaluates JavaScript in the page context.
775    ///
776    /// Executes the provided JavaScript expression or function within the page's
777    /// context and returns the result. The return value must be JSON-serializable.
778    ///
779    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
780    pub async fn evaluate(&self, expression: &str) -> Result<()> {
781        // Delegate to the main frame, matching playwright-python's behavior
782        let frame = self.main_frame().await?;
783        frame.frame_evaluate_expression(expression).await
784    }
785
786    /// Evaluates a JavaScript expression and returns the result as a String.
787    ///
788    /// # Arguments
789    ///
790    /// * `expression` - JavaScript code to evaluate
791    ///
792    /// # Returns
793    ///
794    /// The result converted to a String
795    ///
796    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
797    pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
798        let frame = self.main_frame().await?;
799        frame.frame_evaluate_expression_value(expression).await
800    }
801
802    /// Registers a route handler for network interception.
803    ///
804    /// When a request matches the specified pattern, the handler will be called
805    /// with a Route object that can abort, continue, or fulfill the request.
806    ///
807    /// # Arguments
808    ///
809    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
810    /// * `handler` - Async closure that handles the route
811    ///
812    /// See: <https://playwright.dev/docs/api/class-page#page-route>
813    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
814    where
815        F: Fn(Route) -> Fut + Send + Sync + 'static,
816        Fut: Future<Output = Result<()>> + Send + 'static,
817    {
818        // 1. Wrap handler in Arc with type erasure
819        let handler =
820            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
821
822        // 2. Store in handlers list
823        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
824            pattern: pattern.to_string(),
825            handler,
826        });
827
828        // 3. Enable network interception via protocol
829        self.enable_network_interception().await?;
830
831        Ok(())
832    }
833
834    /// Updates network interception patterns for this page
835    async fn enable_network_interception(&self) -> Result<()> {
836        // Collect all patterns from registered handlers
837        // Each pattern must be an object with "glob" field
838        let patterns: Vec<serde_json::Value> = self
839            .route_handlers
840            .lock()
841            .unwrap()
842            .iter()
843            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
844            .collect();
845
846        // Send protocol command to update network interception patterns
847        // Follows playwright-python's approach
848        self.channel()
849            .send_no_result(
850                "setNetworkInterceptionPatterns",
851                serde_json::json!({
852                    "patterns": patterns
853                }),
854            )
855            .await
856    }
857
858    /// Handles a route event from the protocol
859    ///
860    /// Called by on_event when a "route" event is received
861    async fn on_route_event(&self, route: Route) {
862        let handlers = self.route_handlers.lock().unwrap().clone();
863        let url = route.request().url().to_string();
864
865        // Find matching handler (last registered wins)
866        for entry in handlers.iter().rev() {
867            // Use glob pattern matching
868            if Self::matches_pattern(&entry.pattern, &url) {
869                let handler = entry.handler.clone();
870                // Execute handler and wait for completion
871                // This ensures fulfill/continue/abort completes before browser continues
872                if let Err(e) = handler(route).await {
873                    tracing::warn!("Route handler error: {}", e);
874                }
875                break;
876            }
877        }
878    }
879
880    /// Checks if a URL matches a glob pattern
881    ///
882    /// Supports standard glob patterns:
883    /// - `*` matches any characters except `/`
884    /// - `**` matches any characters including `/`
885    /// - `?` matches a single character
886    fn matches_pattern(pattern: &str, url: &str) -> bool {
887        use glob::Pattern;
888
889        // Try to compile the glob pattern
890        match Pattern::new(pattern) {
891            Ok(glob_pattern) => glob_pattern.matches(url),
892            Err(_) => {
893                // If pattern is invalid, fall back to exact string match
894                pattern == url
895            }
896        }
897    }
898
899    /// Registers a download event handler.
900    ///
901    /// The handler will be called when a download is triggered by the page.
902    /// Downloads occur when the page initiates a file download (e.g., clicking a link
903    /// with the download attribute, or a server response with Content-Disposition: attachment).
904    ///
905    /// # Arguments
906    ///
907    /// * `handler` - Async closure that receives the Download object
908    ///
909    /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
910    pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
911    where
912        F: Fn(Download) -> Fut + Send + Sync + 'static,
913        Fut: Future<Output = Result<()>> + Send + 'static,
914    {
915        // Wrap handler with type erasure
916        let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
917            Box::pin(handler(download))
918        });
919
920        // Store handler
921        self.download_handlers.lock().unwrap().push(handler);
922
923        Ok(())
924    }
925
926    /// Registers a dialog event handler.
927    ///
928    /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
929    /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
930    ///
931    /// # Arguments
932    ///
933    /// * `handler` - Async closure that receives the Dialog object
934    ///
935    /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
936    pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
937    where
938        F: Fn(Dialog) -> Fut + Send + Sync + 'static,
939        Fut: Future<Output = Result<()>> + Send + 'static,
940    {
941        // Wrap handler with type erasure
942        let handler =
943            Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
944
945        // Store handler
946        self.dialog_handlers.lock().unwrap().push(handler);
947
948        // Dialog events are auto-emitted (no subscription needed)
949
950        Ok(())
951    }
952
953    /// Handles a download event from the protocol
954    async fn on_download_event(&self, download: Download) {
955        let handlers = self.download_handlers.lock().unwrap().clone();
956
957        for handler in handlers {
958            if let Err(e) = handler(download.clone()).await {
959                tracing::warn!("Download handler error: {}", e);
960            }
961        }
962    }
963
964    /// Handles a dialog event from the protocol
965    async fn on_dialog_event(&self, dialog: Dialog) {
966        let handlers = self.dialog_handlers.lock().unwrap().clone();
967
968        for handler in handlers {
969            if let Err(e) = handler(dialog.clone()).await {
970                tracing::warn!("Dialog handler error: {}", e);
971            }
972        }
973    }
974
975    /// Triggers dialog event (called by BrowserContext when dialog events arrive)
976    ///
977    /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
978    /// This method is public so BrowserContext can forward dialog events.
979    pub async fn trigger_dialog_event(&self, dialog: Dialog) {
980        self.on_dialog_event(dialog).await;
981    }
982
983    /// Adds a `<style>` tag into the page with the desired content.
984    ///
985    /// # Arguments
986    ///
987    /// * `options` - Style tag options (content, url, or path)
988    ///
989    /// # Returns
990    ///
991    /// Returns an ElementHandle pointing to the injected `<style>` tag
992    ///
993    /// # Example
994    ///
995    /// ```no_run
996    /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
997    /// # #[tokio::main]
998    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
999    /// # let playwright = Playwright::launch().await?;
1000    /// # let browser = playwright.chromium().launch().await?;
1001    /// # let context = browser.new_context().await?;
1002    /// # let page = context.new_page().await?;
1003    /// use playwright_rs::protocol::AddStyleTagOptions;
1004    ///
1005    /// // With inline CSS
1006    /// page.add_style_tag(
1007    ///     AddStyleTagOptions::builder()
1008    ///         .content("body { background-color: red; }")
1009    ///         .build()
1010    /// ).await?;
1011    ///
1012    /// // With external URL
1013    /// page.add_style_tag(
1014    ///     AddStyleTagOptions::builder()
1015    ///         .url("https://example.com/style.css")
1016    ///         .build()
1017    /// ).await?;
1018    ///
1019    /// // From file
1020    /// page.add_style_tag(
1021    ///     AddStyleTagOptions::builder()
1022    ///         .path("./styles/custom.css")
1023    ///         .build()
1024    /// ).await?;
1025    /// # Ok(())
1026    /// # }
1027    /// ```
1028    ///
1029    /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1030    pub async fn add_style_tag(
1031        &self,
1032        options: AddStyleTagOptions,
1033    ) -> Result<Arc<crate::protocol::ElementHandle>> {
1034        let frame = self.main_frame().await?;
1035        frame.add_style_tag(options).await
1036    }
1037
1038    /// Adds a script which would be evaluated in one of the following scenarios:
1039    /// - Whenever the page is navigated
1040    /// - Whenever a child frame is attached or navigated
1041    ///
1042    /// The script is evaluated after the document was created but before any of its scripts were run.
1043    ///
1044    /// # Arguments
1045    ///
1046    /// * `script` - JavaScript code to be injected into the page
1047    ///
1048    /// # Example
1049    ///
1050    /// ```no_run
1051    /// # use playwright_rs::protocol::Playwright;
1052    /// # #[tokio::main]
1053    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1054    /// # let playwright = Playwright::launch().await?;
1055    /// # let browser = playwright.chromium().launch().await?;
1056    /// # let context = browser.new_context().await?;
1057    /// # let page = context.new_page().await?;
1058    /// page.add_init_script("window.injected = 123;").await?;
1059    /// # Ok(())
1060    /// # }
1061    /// ```
1062    ///
1063    /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
1064    pub async fn add_init_script(&self, script: &str) -> Result<()> {
1065        self.channel()
1066            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
1067            .await
1068    }
1069}
1070
1071impl ChannelOwner for Page {
1072    fn guid(&self) -> &str {
1073        self.base.guid()
1074    }
1075
1076    fn type_name(&self) -> &str {
1077        self.base.type_name()
1078    }
1079
1080    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
1081        self.base.parent()
1082    }
1083
1084    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
1085        self.base.connection()
1086    }
1087
1088    fn initializer(&self) -> &Value {
1089        self.base.initializer()
1090    }
1091
1092    fn channel(&self) -> &Channel {
1093        self.base.channel()
1094    }
1095
1096    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
1097        self.base.dispose(reason)
1098    }
1099
1100    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
1101        self.base.adopt(child)
1102    }
1103
1104    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
1105        self.base.add_child(guid, child)
1106    }
1107
1108    fn remove_child(&self, guid: &str) {
1109        self.base.remove_child(guid)
1110    }
1111
1112    fn on_event(&self, method: &str, params: Value) {
1113        match method {
1114            "navigated" => {
1115                // Update URL when page navigates
1116                if let Some(url_value) = params.get("url") {
1117                    if let Some(url_str) = url_value.as_str() {
1118                        if let Ok(mut url) = self.url.write() {
1119                            *url = url_str.to_string();
1120                        }
1121                    }
1122                }
1123            }
1124            "route" => {
1125                // Handle network routing event
1126                if let Some(route_guid) = params
1127                    .get("route")
1128                    .and_then(|v| v.get("guid"))
1129                    .and_then(|v| v.as_str())
1130                {
1131                    // Get the Route object from connection's registry
1132                    let connection = self.connection();
1133                    let route_guid_owned = route_guid.to_string();
1134                    let self_clone = self.clone();
1135
1136                    tokio::spawn(async move {
1137                        // Wait for Route object to be created
1138                        let route_arc = match connection.get_object(&route_guid_owned).await {
1139                            Ok(obj) => obj,
1140                            Err(e) => {
1141                                tracing::warn!("Failed to get route object: {}", e);
1142                                return;
1143                            }
1144                        };
1145
1146                        // Downcast to Route
1147                        let route = match route_arc.as_any().downcast_ref::<Route>() {
1148                            Some(r) => r.clone(),
1149                            None => {
1150                                tracing::warn!("Failed to downcast to Route");
1151                                return;
1152                            }
1153                        };
1154
1155                        // Call the route handler and wait for completion
1156                        self_clone.on_route_event(route).await;
1157                    });
1158                }
1159            }
1160            "download" => {
1161                // Handle download event
1162                // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
1163                let url = params
1164                    .get("url")
1165                    .and_then(|v| v.as_str())
1166                    .unwrap_or("")
1167                    .to_string();
1168
1169                let suggested_filename = params
1170                    .get("suggestedFilename")
1171                    .and_then(|v| v.as_str())
1172                    .unwrap_or("")
1173                    .to_string();
1174
1175                if let Some(artifact_guid) = params
1176                    .get("artifact")
1177                    .and_then(|v| v.get("guid"))
1178                    .and_then(|v| v.as_str())
1179                {
1180                    let connection = self.connection();
1181                    let artifact_guid_owned = artifact_guid.to_string();
1182                    let self_clone = self.clone();
1183
1184                    tokio::spawn(async move {
1185                        // Wait for Artifact object to be created
1186                        let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
1187                            Ok(obj) => obj,
1188                            Err(e) => {
1189                                tracing::warn!("Failed to get artifact object: {}", e);
1190                                return;
1191                            }
1192                        };
1193
1194                        // Create Download wrapper from Artifact + event params
1195                        let download =
1196                            Download::from_artifact(artifact_arc, url, suggested_filename);
1197
1198                        // Call the download handlers
1199                        self_clone.on_download_event(download).await;
1200                    });
1201                }
1202            }
1203            "dialog" => {
1204                // Dialog events are handled by BrowserContext and forwarded to Page
1205                // This case should not be reached, but keeping for completeness
1206            }
1207            _ => {
1208                // Other events will be handled in future phases
1209                // Events: load, domcontentloaded, close, crash, etc.
1210            }
1211        }
1212    }
1213
1214    fn was_collected(&self) -> bool {
1215        self.base.was_collected()
1216    }
1217
1218    fn as_any(&self) -> &dyn Any {
1219        self
1220    }
1221}
1222
1223impl std::fmt::Debug for Page {
1224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1225        f.debug_struct("Page")
1226            .field("guid", &self.guid())
1227            .field("url", &self.url())
1228            .finish()
1229    }
1230}
1231
1232/// Options for page.goto() and page.reload()
1233#[derive(Debug, Clone)]
1234pub struct GotoOptions {
1235    /// Maximum operation time in milliseconds
1236    pub timeout: Option<std::time::Duration>,
1237    /// When to consider operation succeeded
1238    pub wait_until: Option<WaitUntil>,
1239}
1240
1241impl GotoOptions {
1242    /// Creates new GotoOptions with default values
1243    pub fn new() -> Self {
1244        Self {
1245            timeout: None,
1246            wait_until: None,
1247        }
1248    }
1249
1250    /// Sets the timeout
1251    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
1252        self.timeout = Some(timeout);
1253        self
1254    }
1255
1256    /// Sets the wait_until option
1257    pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
1258        self.wait_until = Some(wait_until);
1259        self
1260    }
1261}
1262
1263impl Default for GotoOptions {
1264    fn default() -> Self {
1265        Self::new()
1266    }
1267}
1268
1269/// When to consider navigation succeeded
1270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1271pub enum WaitUntil {
1272    /// Consider operation to be finished when the `load` event is fired
1273    Load,
1274    /// Consider operation to be finished when the `DOMContentLoaded` event is fired
1275    DomContentLoaded,
1276    /// Consider operation to be finished when there are no network connections for at least 500ms
1277    NetworkIdle,
1278    /// Consider operation to be finished when the commit event is fired
1279    Commit,
1280}
1281
1282impl WaitUntil {
1283    pub(crate) fn as_str(&self) -> &'static str {
1284        match self {
1285            WaitUntil::Load => "load",
1286            WaitUntil::DomContentLoaded => "domcontentloaded",
1287            WaitUntil::NetworkIdle => "networkidle",
1288            WaitUntil::Commit => "commit",
1289        }
1290    }
1291}
1292
1293/// Options for adding a style tag to the page
1294///
1295/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1296#[derive(Debug, Clone, Default)]
1297pub struct AddStyleTagOptions {
1298    /// Raw CSS content to inject
1299    pub content: Option<String>,
1300    /// URL of the `<link>` tag to add
1301    pub url: Option<String>,
1302    /// Path to a CSS file to inject
1303    pub path: Option<String>,
1304}
1305
1306impl AddStyleTagOptions {
1307    /// Creates a new builder for AddStyleTagOptions
1308    pub fn builder() -> AddStyleTagOptionsBuilder {
1309        AddStyleTagOptionsBuilder::default()
1310    }
1311
1312    /// Validates that at least one option is specified
1313    pub(crate) fn validate(&self) -> Result<()> {
1314        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
1315            return Err(Error::InvalidArgument(
1316                "At least one of content, url, or path must be specified".to_string(),
1317            ));
1318        }
1319        Ok(())
1320    }
1321}
1322
1323/// Builder for AddStyleTagOptions
1324#[derive(Debug, Clone, Default)]
1325pub struct AddStyleTagOptionsBuilder {
1326    content: Option<String>,
1327    url: Option<String>,
1328    path: Option<String>,
1329}
1330
1331impl AddStyleTagOptionsBuilder {
1332    /// Sets the CSS content to inject
1333    pub fn content(mut self, content: impl Into<String>) -> Self {
1334        self.content = Some(content.into());
1335        self
1336    }
1337
1338    /// Sets the URL of the stylesheet
1339    pub fn url(mut self, url: impl Into<String>) -> Self {
1340        self.url = Some(url.into());
1341        self
1342    }
1343
1344    /// Sets the path to a CSS file
1345    pub fn path(mut self, path: impl Into<String>) -> Self {
1346        self.path = Some(path.into());
1347        self
1348    }
1349
1350    /// Builds the AddStyleTagOptions
1351    pub fn build(self) -> AddStyleTagOptions {
1352        AddStyleTagOptions {
1353            content: self.content,
1354            url: self.url,
1355            path: self.path,
1356        }
1357    }
1358}
1359
1360/// Response from navigation operations
1361#[derive(Debug, Clone)]
1362pub struct Response {
1363    /// URL of the response
1364    pub url: String,
1365    /// HTTP status code
1366    pub status: u16,
1367    /// HTTP status text
1368    pub status_text: String,
1369    /// Whether the response was successful (status 200-299)
1370    pub ok: bool,
1371    /// Response headers
1372    pub headers: std::collections::HashMap<String, String>,
1373}
1374
1375impl Response {
1376    /// Returns the URL of the response
1377    pub fn url(&self) -> &str {
1378        &self.url
1379    }
1380
1381    /// Returns the HTTP status code
1382    pub fn status(&self) -> u16 {
1383        self.status
1384    }
1385
1386    /// Returns the HTTP status text
1387    pub fn status_text(&self) -> &str {
1388        &self.status_text
1389    }
1390
1391    /// Returns whether the response was successful (status 200-299)
1392    pub fn ok(&self) -> bool {
1393        self.ok
1394    }
1395
1396    /// Returns the response headers
1397    pub fn headers(&self) -> &std::collections::HashMap<String, String> {
1398        &self.headers
1399    }
1400}