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