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}