playwright_rs/protocol/
page.rs

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