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!')", None).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 (without return value).
775    ///
776    /// Executes the provided JavaScript expression or function within the page's
777    /// context without returning a value.
778    ///
779    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
780    pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
781        // Delegate to the main frame
782        let frame = self.main_frame().await?;
783        frame.frame_evaluate_expression(expression).await
784    }
785
786    /// Evaluates JavaScript in the page context with optional arguments.
787    ///
788    /// Executes the provided JavaScript expression or function within the page's
789    /// context and returns the result. The return value must be JSON-serializable.
790    ///
791    /// # Arguments
792    ///
793    /// * `expression` - JavaScript code to evaluate
794    /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
795    ///
796    /// # Returns
797    ///
798    /// The result as a `serde_json::Value`
799    ///
800    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
801    pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
802        &self,
803        expression: &str,
804        arg: Option<&T>,
805    ) -> Result<U> {
806        // Delegate to the main frame
807        let frame = self.main_frame().await?;
808        let result = frame.evaluate(expression, arg).await?;
809        serde_json::from_value(result).map_err(Error::from)
810    }
811
812    /// Evaluates a JavaScript expression and returns the result as a String.
813    ///
814    /// # Arguments
815    ///
816    /// * `expression` - JavaScript code to evaluate
817    ///
818    /// # Returns
819    ///
820    /// The result converted to a String
821    ///
822    /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
823    pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
824        let frame = self.main_frame().await?;
825        frame.frame_evaluate_expression_value(expression).await
826    }
827
828    /// Registers a route handler for network interception.
829    ///
830    /// When a request matches the specified pattern, the handler will be called
831    /// with a Route object that can abort, continue, or fulfill the request.
832    ///
833    /// # Arguments
834    ///
835    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
836    /// * `handler` - Async closure that handles the route
837    ///
838    /// See: <https://playwright.dev/docs/api/class-page#page-route>
839    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
840    where
841        F: Fn(Route) -> Fut + Send + Sync + 'static,
842        Fut: Future<Output = Result<()>> + Send + 'static,
843    {
844        // 1. Wrap handler in Arc with type erasure
845        let handler =
846            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
847
848        // 2. Store in handlers list
849        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
850            pattern: pattern.to_string(),
851            handler,
852        });
853
854        // 3. Enable network interception via protocol
855        self.enable_network_interception().await?;
856
857        Ok(())
858    }
859
860    /// Updates network interception patterns for this page
861    async fn enable_network_interception(&self) -> Result<()> {
862        // Collect all patterns from registered handlers
863        // Each pattern must be an object with "glob" field
864        let patterns: Vec<serde_json::Value> = self
865            .route_handlers
866            .lock()
867            .unwrap()
868            .iter()
869            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
870            .collect();
871
872        // Send protocol command to update network interception patterns
873        // Follows playwright-python's approach
874        self.channel()
875            .send_no_result(
876                "setNetworkInterceptionPatterns",
877                serde_json::json!({
878                    "patterns": patterns
879                }),
880            )
881            .await
882    }
883
884    /// Handles a route event from the protocol
885    ///
886    /// Called by on_event when a "route" event is received
887    async fn on_route_event(&self, route: Route) {
888        let handlers = self.route_handlers.lock().unwrap().clone();
889        let url = route.request().url().to_string();
890
891        // Find matching handler (last registered wins)
892        for entry in handlers.iter().rev() {
893            // Use glob pattern matching
894            if Self::matches_pattern(&entry.pattern, &url) {
895                let handler = entry.handler.clone();
896                // Execute handler and wait for completion
897                // This ensures fulfill/continue/abort completes before browser continues
898                if let Err(e) = handler(route).await {
899                    tracing::warn!("Route handler error: {}", e);
900                }
901                break;
902            }
903        }
904    }
905
906    /// Checks if a URL matches a glob pattern
907    ///
908    /// Supports standard glob patterns:
909    /// - `*` matches any characters except `/`
910    /// - `**` matches any characters including `/`
911    /// - `?` matches a single character
912    fn matches_pattern(pattern: &str, url: &str) -> bool {
913        use glob::Pattern;
914
915        // Try to compile the glob pattern
916        match Pattern::new(pattern) {
917            Ok(glob_pattern) => glob_pattern.matches(url),
918            Err(_) => {
919                // If pattern is invalid, fall back to exact string match
920                pattern == url
921            }
922        }
923    }
924
925    /// Registers a download event handler.
926    ///
927    /// The handler will be called when a download is triggered by the page.
928    /// Downloads occur when the page initiates a file download (e.g., clicking a link
929    /// with the download attribute, or a server response with Content-Disposition: attachment).
930    ///
931    /// # Arguments
932    ///
933    /// * `handler` - Async closure that receives the Download object
934    ///
935    /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
936    pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
937    where
938        F: Fn(Download) -> Fut + Send + Sync + 'static,
939        Fut: Future<Output = Result<()>> + Send + 'static,
940    {
941        // Wrap handler with type erasure
942        let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
943            Box::pin(handler(download))
944        });
945
946        // Store handler
947        self.download_handlers.lock().unwrap().push(handler);
948
949        Ok(())
950    }
951
952    /// Registers a dialog event handler.
953    ///
954    /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
955    /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
956    ///
957    /// # Arguments
958    ///
959    /// * `handler` - Async closure that receives the Dialog object
960    ///
961    /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
962    pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
963    where
964        F: Fn(Dialog) -> Fut + Send + Sync + 'static,
965        Fut: Future<Output = Result<()>> + Send + 'static,
966    {
967        // Wrap handler with type erasure
968        let handler =
969            Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
970
971        // Store handler
972        self.dialog_handlers.lock().unwrap().push(handler);
973
974        // Dialog events are auto-emitted (no subscription needed)
975
976        Ok(())
977    }
978
979    /// Handles a download event from the protocol
980    async fn on_download_event(&self, download: Download) {
981        let handlers = self.download_handlers.lock().unwrap().clone();
982
983        for handler in handlers {
984            if let Err(e) = handler(download.clone()).await {
985                tracing::warn!("Download handler error: {}", e);
986            }
987        }
988    }
989
990    /// Handles a dialog event from the protocol
991    async fn on_dialog_event(&self, dialog: Dialog) {
992        let handlers = self.dialog_handlers.lock().unwrap().clone();
993
994        for handler in handlers {
995            if let Err(e) = handler(dialog.clone()).await {
996                tracing::warn!("Dialog handler error: {}", e);
997            }
998        }
999    }
1000
1001    /// Triggers dialog event (called by BrowserContext when dialog events arrive)
1002    ///
1003    /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
1004    /// This method is public so BrowserContext can forward dialog events.
1005    pub async fn trigger_dialog_event(&self, dialog: Dialog) {
1006        self.on_dialog_event(dialog).await;
1007    }
1008
1009    /// Adds a `<style>` tag into the page with the desired content.
1010    ///
1011    /// # Arguments
1012    ///
1013    /// * `options` - Style tag options (content, url, or path)
1014    ///
1015    /// # Returns
1016    ///
1017    /// Returns an ElementHandle pointing to the injected `<style>` tag
1018    ///
1019    /// # Example
1020    ///
1021    /// ```no_run
1022    /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
1023    /// # #[tokio::main]
1024    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1025    /// # let playwright = Playwright::launch().await?;
1026    /// # let browser = playwright.chromium().launch().await?;
1027    /// # let context = browser.new_context().await?;
1028    /// # let page = context.new_page().await?;
1029    /// use playwright_rs::protocol::AddStyleTagOptions;
1030    ///
1031    /// // With inline CSS
1032    /// page.add_style_tag(
1033    ///     AddStyleTagOptions::builder()
1034    ///         .content("body { background-color: red; }")
1035    ///         .build()
1036    /// ).await?;
1037    ///
1038    /// // With external URL
1039    /// page.add_style_tag(
1040    ///     AddStyleTagOptions::builder()
1041    ///         .url("https://example.com/style.css")
1042    ///         .build()
1043    /// ).await?;
1044    ///
1045    /// // From file
1046    /// page.add_style_tag(
1047    ///     AddStyleTagOptions::builder()
1048    ///         .path("./styles/custom.css")
1049    ///         .build()
1050    /// ).await?;
1051    /// # Ok(())
1052    /// # }
1053    /// ```
1054    ///
1055    /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1056    pub async fn add_style_tag(
1057        &self,
1058        options: AddStyleTagOptions,
1059    ) -> Result<Arc<crate::protocol::ElementHandle>> {
1060        let frame = self.main_frame().await?;
1061        frame.add_style_tag(options).await
1062    }
1063
1064    /// Adds a script which would be evaluated in one of the following scenarios:
1065    /// - Whenever the page is navigated
1066    /// - Whenever a child frame is attached or navigated
1067    ///
1068    /// The script is evaluated after the document was created but before any of its scripts were run.
1069    ///
1070    /// # Arguments
1071    ///
1072    /// * `script` - JavaScript code to be injected into the page
1073    ///
1074    /// # Example
1075    ///
1076    /// ```no_run
1077    /// # use playwright_rs::protocol::Playwright;
1078    /// # #[tokio::main]
1079    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1080    /// # let playwright = Playwright::launch().await?;
1081    /// # let browser = playwright.chromium().launch().await?;
1082    /// # let context = browser.new_context().await?;
1083    /// # let page = context.new_page().await?;
1084    /// page.add_init_script("window.injected = 123;").await?;
1085    /// # Ok(())
1086    /// # }
1087    /// ```
1088    ///
1089    /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
1090    pub async fn add_init_script(&self, script: &str) -> Result<()> {
1091        self.channel()
1092            .send_no_result("addInitScript", serde_json::json!({ "source": script }))
1093            .await
1094    }
1095}
1096
1097impl ChannelOwner for Page {
1098    fn guid(&self) -> &str {
1099        self.base.guid()
1100    }
1101
1102    fn type_name(&self) -> &str {
1103        self.base.type_name()
1104    }
1105
1106    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
1107        self.base.parent()
1108    }
1109
1110    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
1111        self.base.connection()
1112    }
1113
1114    fn initializer(&self) -> &Value {
1115        self.base.initializer()
1116    }
1117
1118    fn channel(&self) -> &Channel {
1119        self.base.channel()
1120    }
1121
1122    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
1123        self.base.dispose(reason)
1124    }
1125
1126    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
1127        self.base.adopt(child)
1128    }
1129
1130    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
1131        self.base.add_child(guid, child)
1132    }
1133
1134    fn remove_child(&self, guid: &str) {
1135        self.base.remove_child(guid)
1136    }
1137
1138    fn on_event(&self, method: &str, params: Value) {
1139        match method {
1140            "navigated" => {
1141                // Update URL when page navigates
1142                if let Some(url_value) = params.get("url") {
1143                    if let Some(url_str) = url_value.as_str() {
1144                        if let Ok(mut url) = self.url.write() {
1145                            *url = url_str.to_string();
1146                        }
1147                    }
1148                }
1149            }
1150            "route" => {
1151                // Handle network routing event
1152                if let Some(route_guid) = params
1153                    .get("route")
1154                    .and_then(|v| v.get("guid"))
1155                    .and_then(|v| v.as_str())
1156                {
1157                    // Get the Route object from connection's registry
1158                    let connection = self.connection();
1159                    let route_guid_owned = route_guid.to_string();
1160                    let self_clone = self.clone();
1161
1162                    tokio::spawn(async move {
1163                        // Wait for Route object to be created
1164                        let route_arc = match connection.get_object(&route_guid_owned).await {
1165                            Ok(obj) => obj,
1166                            Err(e) => {
1167                                tracing::warn!("Failed to get route object: {}", e);
1168                                return;
1169                            }
1170                        };
1171
1172                        // Downcast to Route
1173                        let route = match route_arc.as_any().downcast_ref::<Route>() {
1174                            Some(r) => r.clone(),
1175                            None => {
1176                                tracing::warn!("Failed to downcast to Route");
1177                                return;
1178                            }
1179                        };
1180
1181                        // Call the route handler and wait for completion
1182                        self_clone.on_route_event(route).await;
1183                    });
1184                }
1185            }
1186            "download" => {
1187                // Handle download event
1188                // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
1189                let url = params
1190                    .get("url")
1191                    .and_then(|v| v.as_str())
1192                    .unwrap_or("")
1193                    .to_string();
1194
1195                let suggested_filename = params
1196                    .get("suggestedFilename")
1197                    .and_then(|v| v.as_str())
1198                    .unwrap_or("")
1199                    .to_string();
1200
1201                if let Some(artifact_guid) = params
1202                    .get("artifact")
1203                    .and_then(|v| v.get("guid"))
1204                    .and_then(|v| v.as_str())
1205                {
1206                    let connection = self.connection();
1207                    let artifact_guid_owned = artifact_guid.to_string();
1208                    let self_clone = self.clone();
1209
1210                    tokio::spawn(async move {
1211                        // Wait for Artifact object to be created
1212                        let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
1213                            Ok(obj) => obj,
1214                            Err(e) => {
1215                                tracing::warn!("Failed to get artifact object: {}", e);
1216                                return;
1217                            }
1218                        };
1219
1220                        // Create Download wrapper from Artifact + event params
1221                        let download =
1222                            Download::from_artifact(artifact_arc, url, suggested_filename);
1223
1224                        // Call the download handlers
1225                        self_clone.on_download_event(download).await;
1226                    });
1227                }
1228            }
1229            "dialog" => {
1230                // Dialog events are handled by BrowserContext and forwarded to Page
1231                // This case should not be reached, but keeping for completeness
1232            }
1233            _ => {
1234                // Other events will be handled in future phases
1235                // Events: load, domcontentloaded, close, crash, etc.
1236            }
1237        }
1238    }
1239
1240    fn was_collected(&self) -> bool {
1241        self.base.was_collected()
1242    }
1243
1244    fn as_any(&self) -> &dyn Any {
1245        self
1246    }
1247}
1248
1249impl std::fmt::Debug for Page {
1250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1251        f.debug_struct("Page")
1252            .field("guid", &self.guid())
1253            .field("url", &self.url())
1254            .finish()
1255    }
1256}
1257
1258/// Options for page.goto() and page.reload()
1259#[derive(Debug, Clone)]
1260pub struct GotoOptions {
1261    /// Maximum operation time in milliseconds
1262    pub timeout: Option<std::time::Duration>,
1263    /// When to consider operation succeeded
1264    pub wait_until: Option<WaitUntil>,
1265}
1266
1267impl GotoOptions {
1268    /// Creates new GotoOptions with default values
1269    pub fn new() -> Self {
1270        Self {
1271            timeout: None,
1272            wait_until: None,
1273        }
1274    }
1275
1276    /// Sets the timeout
1277    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
1278        self.timeout = Some(timeout);
1279        self
1280    }
1281
1282    /// Sets the wait_until option
1283    pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
1284        self.wait_until = Some(wait_until);
1285        self
1286    }
1287}
1288
1289impl Default for GotoOptions {
1290    fn default() -> Self {
1291        Self::new()
1292    }
1293}
1294
1295/// When to consider navigation succeeded
1296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1297pub enum WaitUntil {
1298    /// Consider operation to be finished when the `load` event is fired
1299    Load,
1300    /// Consider operation to be finished when the `DOMContentLoaded` event is fired
1301    DomContentLoaded,
1302    /// Consider operation to be finished when there are no network connections for at least 500ms
1303    NetworkIdle,
1304    /// Consider operation to be finished when the commit event is fired
1305    Commit,
1306}
1307
1308impl WaitUntil {
1309    pub(crate) fn as_str(&self) -> &'static str {
1310        match self {
1311            WaitUntil::Load => "load",
1312            WaitUntil::DomContentLoaded => "domcontentloaded",
1313            WaitUntil::NetworkIdle => "networkidle",
1314            WaitUntil::Commit => "commit",
1315        }
1316    }
1317}
1318
1319/// Options for adding a style tag to the page
1320///
1321/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1322#[derive(Debug, Clone, Default)]
1323pub struct AddStyleTagOptions {
1324    /// Raw CSS content to inject
1325    pub content: Option<String>,
1326    /// URL of the `<link>` tag to add
1327    pub url: Option<String>,
1328    /// Path to a CSS file to inject
1329    pub path: Option<String>,
1330}
1331
1332impl AddStyleTagOptions {
1333    /// Creates a new builder for AddStyleTagOptions
1334    pub fn builder() -> AddStyleTagOptionsBuilder {
1335        AddStyleTagOptionsBuilder::default()
1336    }
1337
1338    /// Validates that at least one option is specified
1339    pub(crate) fn validate(&self) -> Result<()> {
1340        if self.content.is_none() && self.url.is_none() && self.path.is_none() {
1341            return Err(Error::InvalidArgument(
1342                "At least one of content, url, or path must be specified".to_string(),
1343            ));
1344        }
1345        Ok(())
1346    }
1347}
1348
1349/// Builder for AddStyleTagOptions
1350#[derive(Debug, Clone, Default)]
1351pub struct AddStyleTagOptionsBuilder {
1352    content: Option<String>,
1353    url: Option<String>,
1354    path: Option<String>,
1355}
1356
1357impl AddStyleTagOptionsBuilder {
1358    /// Sets the CSS content to inject
1359    pub fn content(mut self, content: impl Into<String>) -> Self {
1360        self.content = Some(content.into());
1361        self
1362    }
1363
1364    /// Sets the URL of the stylesheet
1365    pub fn url(mut self, url: impl Into<String>) -> Self {
1366        self.url = Some(url.into());
1367        self
1368    }
1369
1370    /// Sets the path to a CSS file
1371    pub fn path(mut self, path: impl Into<String>) -> Self {
1372        self.path = Some(path.into());
1373        self
1374    }
1375
1376    /// Builds the AddStyleTagOptions
1377    pub fn build(self) -> AddStyleTagOptions {
1378        AddStyleTagOptions {
1379            content: self.content,
1380            url: self.url,
1381            path: self.path,
1382        }
1383    }
1384}
1385
1386/// Response from navigation operations
1387#[derive(Debug, Clone)]
1388pub struct Response {
1389    /// URL of the response
1390    pub url: String,
1391    /// HTTP status code
1392    pub status: u16,
1393    /// HTTP status text
1394    pub status_text: String,
1395    /// Whether the response was successful (status 200-299)
1396    pub ok: bool,
1397    /// Response headers
1398    pub headers: std::collections::HashMap<String, String>,
1399}
1400
1401impl Response {
1402    /// Returns the URL of the response
1403    pub fn url(&self) -> &str {
1404        &self.url
1405    }
1406
1407    /// Returns the HTTP status code
1408    pub fn status(&self) -> u16 {
1409        self.status
1410    }
1411
1412    /// Returns the HTTP status text
1413    pub fn status_text(&self) -> &str {
1414        &self.status_text
1415    }
1416
1417    /// Returns whether the response was successful (status 200-299)
1418    pub fn ok(&self) -> bool {
1419        self.ok
1420    }
1421
1422    /// Returns the response headers
1423    pub fn headers(&self) -> &std::collections::HashMap<String, String> {
1424        &self.headers
1425    }
1426}