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