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