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}