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!')", 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}
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 browser context that the page belongs to.
331 pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
332 let parent = self.base.parent().ok_or_else(|| Error::TargetClosed {
333 target_type: "Page".into(),
334 context: "Parent context not found".into(),
335 })?;
336
337 let context = parent
338 .as_any()
339 .downcast_ref::<crate::protocol::BrowserContext>()
340 .ok_or_else(|| {
341 Error::ProtocolError("Page parent is not a BrowserContext".to_string())
342 })?;
343
344 Ok(context.clone())
345 }
346
347 /// Pauses script execution.
348 ///
349 /// Playwright will stop executing the script and wait for the user to either press
350 /// "Resume" in the page overlay or in the debugger.
351 ///
352 /// See: <https://playwright.dev/docs/api/class-page#page-pause>
353 pub async fn pause(&self) -> Result<()> {
354 self.context()?.pause().await
355 }
356
357 /// Returns the page's title.
358 ///
359 /// See: <https://playwright.dev/docs/api/class-page#page-title>
360 pub async fn title(&self) -> Result<String> {
361 // Delegate to main frame
362 let frame = self.main_frame().await?;
363 frame.title().await
364 }
365
366 /// Creates a locator for finding elements on the page.
367 ///
368 /// Locators are the central piece of Playwright's auto-waiting and retry-ability.
369 /// They don't execute queries until an action is performed.
370 ///
371 /// # Arguments
372 ///
373 /// * `selector` - CSS selector or other locating strategy
374 ///
375 /// See: <https://playwright.dev/docs/api/class-page#page-locator>
376 pub async fn locator(&self, selector: &str) -> crate::protocol::Locator {
377 // Get the main frame
378 let frame = self.main_frame().await.expect("Main frame should exist");
379
380 crate::protocol::Locator::new(Arc::new(frame), selector.to_string())
381 }
382
383 /// Returns the keyboard instance for low-level keyboard control.
384 ///
385 /// See: <https://playwright.dev/docs/api/class-page#page-keyboard>
386 pub fn keyboard(&self) -> crate::protocol::Keyboard {
387 crate::protocol::Keyboard::new(self.clone())
388 }
389
390 /// Returns the mouse instance for low-level mouse control.
391 ///
392 /// See: <https://playwright.dev/docs/api/class-page#page-mouse>
393 pub fn mouse(&self) -> crate::protocol::Mouse {
394 crate::protocol::Mouse::new(self.clone())
395 }
396
397 // Internal keyboard methods (called by Keyboard struct)
398
399 pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
400 self.channel()
401 .send_no_result(
402 "keyboardDown",
403 serde_json::json!({
404 "key": key
405 }),
406 )
407 .await
408 }
409
410 pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
411 self.channel()
412 .send_no_result(
413 "keyboardUp",
414 serde_json::json!({
415 "key": key
416 }),
417 )
418 .await
419 }
420
421 pub(crate) async fn keyboard_press(
422 &self,
423 key: &str,
424 options: Option<crate::protocol::KeyboardOptions>,
425 ) -> Result<()> {
426 let mut params = serde_json::json!({
427 "key": key
428 });
429
430 if let Some(opts) = options {
431 let opts_json = opts.to_json();
432 if let Some(obj) = params.as_object_mut() {
433 if let Some(opts_obj) = opts_json.as_object() {
434 obj.extend(opts_obj.clone());
435 }
436 }
437 }
438
439 self.channel().send_no_result("keyboardPress", params).await
440 }
441
442 pub(crate) async fn keyboard_type(
443 &self,
444 text: &str,
445 options: Option<crate::protocol::KeyboardOptions>,
446 ) -> Result<()> {
447 let mut params = serde_json::json!({
448 "text": text
449 });
450
451 if let Some(opts) = options {
452 let opts_json = opts.to_json();
453 if let Some(obj) = params.as_object_mut() {
454 if let Some(opts_obj) = opts_json.as_object() {
455 obj.extend(opts_obj.clone());
456 }
457 }
458 }
459
460 self.channel().send_no_result("keyboardType", params).await
461 }
462
463 pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
464 self.channel()
465 .send_no_result(
466 "keyboardInsertText",
467 serde_json::json!({
468 "text": text
469 }),
470 )
471 .await
472 }
473
474 // Internal mouse methods (called by Mouse struct)
475
476 pub(crate) async fn mouse_move(
477 &self,
478 x: i32,
479 y: i32,
480 options: Option<crate::protocol::MouseOptions>,
481 ) -> Result<()> {
482 let mut params = serde_json::json!({
483 "x": x,
484 "y": y
485 });
486
487 if let Some(opts) = options {
488 let opts_json = opts.to_json();
489 if let Some(obj) = params.as_object_mut() {
490 if let Some(opts_obj) = opts_json.as_object() {
491 obj.extend(opts_obj.clone());
492 }
493 }
494 }
495
496 self.channel().send_no_result("mouseMove", params).await
497 }
498
499 pub(crate) async fn mouse_click(
500 &self,
501 x: i32,
502 y: i32,
503 options: Option<crate::protocol::MouseOptions>,
504 ) -> Result<()> {
505 let mut params = serde_json::json!({
506 "x": x,
507 "y": y
508 });
509
510 if let Some(opts) = options {
511 let opts_json = opts.to_json();
512 if let Some(obj) = params.as_object_mut() {
513 if let Some(opts_obj) = opts_json.as_object() {
514 obj.extend(opts_obj.clone());
515 }
516 }
517 }
518
519 self.channel().send_no_result("mouseClick", params).await
520 }
521
522 pub(crate) async fn mouse_dblclick(
523 &self,
524 x: i32,
525 y: i32,
526 options: Option<crate::protocol::MouseOptions>,
527 ) -> Result<()> {
528 let mut params = serde_json::json!({
529 "x": x,
530 "y": y,
531 "clickCount": 2
532 });
533
534 if let Some(opts) = options {
535 let opts_json = opts.to_json();
536 if let Some(obj) = params.as_object_mut() {
537 if let Some(opts_obj) = opts_json.as_object() {
538 obj.extend(opts_obj.clone());
539 }
540 }
541 }
542
543 self.channel().send_no_result("mouseClick", params).await
544 }
545
546 pub(crate) async fn mouse_down(
547 &self,
548 options: Option<crate::protocol::MouseOptions>,
549 ) -> Result<()> {
550 let mut params = serde_json::json!({});
551
552 if let Some(opts) = options {
553 let opts_json = opts.to_json();
554 if let Some(obj) = params.as_object_mut() {
555 if let Some(opts_obj) = opts_json.as_object() {
556 obj.extend(opts_obj.clone());
557 }
558 }
559 }
560
561 self.channel().send_no_result("mouseDown", params).await
562 }
563
564 pub(crate) async fn mouse_up(
565 &self,
566 options: Option<crate::protocol::MouseOptions>,
567 ) -> Result<()> {
568 let mut params = serde_json::json!({});
569
570 if let Some(opts) = options {
571 let opts_json = opts.to_json();
572 if let Some(obj) = params.as_object_mut() {
573 if let Some(opts_obj) = opts_json.as_object() {
574 obj.extend(opts_obj.clone());
575 }
576 }
577 }
578
579 self.channel().send_no_result("mouseUp", params).await
580 }
581
582 pub(crate) async fn mouse_wheel(&self, delta_x: i32, delta_y: i32) -> Result<()> {
583 self.channel()
584 .send_no_result(
585 "mouseWheel",
586 serde_json::json!({
587 "deltaX": delta_x,
588 "deltaY": delta_y
589 }),
590 )
591 .await
592 }
593
594 /// Reloads the current page.
595 ///
596 /// # Arguments
597 ///
598 /// * `options` - Optional reload options (timeout, wait_until)
599 ///
600 /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
601 /// about:blank). This matches Playwright's behavior across all language bindings.
602 ///
603 /// See: <https://playwright.dev/docs/api/class-page#page-reload>
604 pub async fn reload(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
605 // Build params
606 let mut params = serde_json::json!({});
607
608 if let Some(opts) = options {
609 if let Some(timeout) = opts.timeout {
610 params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
611 } else {
612 params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
613 }
614 if let Some(wait_until) = opts.wait_until {
615 params["waitUntil"] = serde_json::json!(wait_until.as_str());
616 }
617 } else {
618 params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
619 }
620
621 // Send reload RPC directly to Page (not Frame!)
622 #[derive(Deserialize)]
623 struct ReloadResponse {
624 response: Option<ResponseReference>,
625 }
626
627 #[derive(Deserialize)]
628 struct ResponseReference {
629 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
630 guid: Arc<str>,
631 }
632
633 let reload_result: ReloadResponse = self.channel().send("reload", params).await?;
634
635 // If reload returned a response, get the Response object
636 if let Some(response_ref) = reload_result.response {
637 // Wait for Response object to be created
638 let response_arc = {
639 let mut attempts = 0;
640 let max_attempts = 20;
641 loop {
642 match self.connection().get_object(&response_ref.guid).await {
643 Ok(obj) => break obj,
644 Err(_) if attempts < max_attempts => {
645 attempts += 1;
646 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
647 }
648 Err(e) => return Err(e),
649 }
650 }
651 };
652
653 // Extract response data from initializer
654 let initializer = response_arc.initializer();
655
656 let status = initializer["status"].as_u64().ok_or_else(|| {
657 crate::error::Error::ProtocolError("Response missing status".to_string())
658 })? as u16;
659
660 let headers = initializer["headers"]
661 .as_array()
662 .ok_or_else(|| {
663 crate::error::Error::ProtocolError("Response missing headers".to_string())
664 })?
665 .iter()
666 .filter_map(|h| {
667 let name = h["name"].as_str()?;
668 let value = h["value"].as_str()?;
669 Some((name.to_string(), value.to_string()))
670 })
671 .collect();
672
673 let response = Response {
674 url: initializer["url"]
675 .as_str()
676 .ok_or_else(|| {
677 crate::error::Error::ProtocolError("Response missing url".to_string())
678 })?
679 .to_string(),
680 status,
681 status_text: initializer["statusText"].as_str().unwrap_or("").to_string(),
682 ok: (200..300).contains(&status),
683 headers,
684 };
685
686 // Update the page's URL
687 if let Ok(mut page_url) = self.url.write() {
688 *page_url = response.url().to_string();
689 }
690
691 Ok(Some(response))
692 } else {
693 // Reload returned null (e.g., data URLs, about:blank)
694 // This is a valid result, not an error
695 Ok(None)
696 }
697 }
698
699 /// Returns the first element matching the selector, or None if not found.
700 ///
701 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
702 pub async fn query_selector(
703 &self,
704 selector: &str,
705 ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
706 let frame = self.main_frame().await?;
707 frame.query_selector(selector).await
708 }
709
710 /// Returns all elements matching the selector.
711 ///
712 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
713 pub async fn query_selector_all(
714 &self,
715 selector: &str,
716 ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
717 let frame = self.main_frame().await?;
718 frame.query_selector_all(selector).await
719 }
720
721 /// Takes a screenshot of the page and returns the image bytes.
722 ///
723 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
724 pub async fn screenshot(
725 &self,
726 options: Option<crate::protocol::ScreenshotOptions>,
727 ) -> Result<Vec<u8>> {
728 let params = if let Some(opts) = options {
729 opts.to_json()
730 } else {
731 // Default to PNG with required timeout
732 serde_json::json!({
733 "type": "png",
734 "timeout": crate::DEFAULT_TIMEOUT_MS
735 })
736 };
737
738 #[derive(Deserialize)]
739 struct ScreenshotResponse {
740 binary: String,
741 }
742
743 let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
744
745 // Decode base64 to bytes
746 let bytes = base64::prelude::BASE64_STANDARD
747 .decode(&response.binary)
748 .map_err(|e| {
749 crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
750 })?;
751
752 Ok(bytes)
753 }
754
755 /// Takes a screenshot and saves it to a file, also returning the bytes.
756 ///
757 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
758 pub async fn screenshot_to_file(
759 &self,
760 path: &std::path::Path,
761 options: Option<crate::protocol::ScreenshotOptions>,
762 ) -> Result<Vec<u8>> {
763 // Get the screenshot bytes
764 let bytes = self.screenshot(options).await?;
765
766 // Write to file
767 tokio::fs::write(path, &bytes).await.map_err(|e| {
768 crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
769 })?;
770
771 Ok(bytes)
772 }
773
774 /// Evaluates JavaScript in the page context (without return value).
775 ///
776 /// Executes the provided JavaScript expression or function within the page's
777 /// context without returning a value.
778 ///
779 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
780 pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
781 // Delegate to the main frame
782 let frame = self.main_frame().await?;
783 frame.frame_evaluate_expression(expression).await
784 }
785
786 /// Evaluates JavaScript in the page context with optional arguments.
787 ///
788 /// Executes the provided JavaScript expression or function within the page's
789 /// context and returns the result. The return value must be JSON-serializable.
790 ///
791 /// # Arguments
792 ///
793 /// * `expression` - JavaScript code to evaluate
794 /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
795 ///
796 /// # Returns
797 ///
798 /// The result as a `serde_json::Value`
799 ///
800 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
801 pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
802 &self,
803 expression: &str,
804 arg: Option<&T>,
805 ) -> Result<U> {
806 // Delegate to the main frame
807 let frame = self.main_frame().await?;
808 let result = frame.evaluate(expression, arg).await?;
809 serde_json::from_value(result).map_err(Error::from)
810 }
811
812 /// Evaluates a JavaScript expression and returns the result as a String.
813 ///
814 /// # Arguments
815 ///
816 /// * `expression` - JavaScript code to evaluate
817 ///
818 /// # Returns
819 ///
820 /// The result converted to a String
821 ///
822 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
823 pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
824 let frame = self.main_frame().await?;
825 frame.frame_evaluate_expression_value(expression).await
826 }
827
828 /// Registers a route handler for network interception.
829 ///
830 /// When a request matches the specified pattern, the handler will be called
831 /// with a Route object that can abort, continue, or fulfill the request.
832 ///
833 /// # Arguments
834 ///
835 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
836 /// * `handler` - Async closure that handles the route
837 ///
838 /// See: <https://playwright.dev/docs/api/class-page#page-route>
839 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
840 where
841 F: Fn(Route) -> Fut + Send + Sync + 'static,
842 Fut: Future<Output = Result<()>> + Send + 'static,
843 {
844 // 1. Wrap handler in Arc with type erasure
845 let handler =
846 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
847
848 // 2. Store in handlers list
849 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
850 pattern: pattern.to_string(),
851 handler,
852 });
853
854 // 3. Enable network interception via protocol
855 self.enable_network_interception().await?;
856
857 Ok(())
858 }
859
860 /// Updates network interception patterns for this page
861 async fn enable_network_interception(&self) -> Result<()> {
862 // Collect all patterns from registered handlers
863 // Each pattern must be an object with "glob" field
864 let patterns: Vec<serde_json::Value> = self
865 .route_handlers
866 .lock()
867 .unwrap()
868 .iter()
869 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
870 .collect();
871
872 // Send protocol command to update network interception patterns
873 // Follows playwright-python's approach
874 self.channel()
875 .send_no_result(
876 "setNetworkInterceptionPatterns",
877 serde_json::json!({
878 "patterns": patterns
879 }),
880 )
881 .await
882 }
883
884 /// Handles a route event from the protocol
885 ///
886 /// Called by on_event when a "route" event is received
887 async fn on_route_event(&self, route: Route) {
888 let handlers = self.route_handlers.lock().unwrap().clone();
889 let url = route.request().url().to_string();
890
891 // Find matching handler (last registered wins)
892 for entry in handlers.iter().rev() {
893 // Use glob pattern matching
894 if Self::matches_pattern(&entry.pattern, &url) {
895 let handler = entry.handler.clone();
896 // Execute handler and wait for completion
897 // This ensures fulfill/continue/abort completes before browser continues
898 if let Err(e) = handler(route).await {
899 tracing::warn!("Route handler error: {}", e);
900 }
901 break;
902 }
903 }
904 }
905
906 /// Checks if a URL matches a glob pattern
907 ///
908 /// Supports standard glob patterns:
909 /// - `*` matches any characters except `/`
910 /// - `**` matches any characters including `/`
911 /// - `?` matches a single character
912 fn matches_pattern(pattern: &str, url: &str) -> bool {
913 use glob::Pattern;
914
915 // Try to compile the glob pattern
916 match Pattern::new(pattern) {
917 Ok(glob_pattern) => glob_pattern.matches(url),
918 Err(_) => {
919 // If pattern is invalid, fall back to exact string match
920 pattern == url
921 }
922 }
923 }
924
925 /// Registers a download event handler.
926 ///
927 /// The handler will be called when a download is triggered by the page.
928 /// Downloads occur when the page initiates a file download (e.g., clicking a link
929 /// with the download attribute, or a server response with Content-Disposition: attachment).
930 ///
931 /// # Arguments
932 ///
933 /// * `handler` - Async closure that receives the Download object
934 ///
935 /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
936 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
937 where
938 F: Fn(Download) -> Fut + Send + Sync + 'static,
939 Fut: Future<Output = Result<()>> + Send + 'static,
940 {
941 // Wrap handler with type erasure
942 let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
943 Box::pin(handler(download))
944 });
945
946 // Store handler
947 self.download_handlers.lock().unwrap().push(handler);
948
949 Ok(())
950 }
951
952 /// Registers a dialog event handler.
953 ///
954 /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
955 /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
956 ///
957 /// # Arguments
958 ///
959 /// * `handler` - Async closure that receives the Dialog object
960 ///
961 /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
962 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
963 where
964 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
965 Fut: Future<Output = Result<()>> + Send + 'static,
966 {
967 // Wrap handler with type erasure
968 let handler =
969 Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
970
971 // Store handler
972 self.dialog_handlers.lock().unwrap().push(handler);
973
974 // Dialog events are auto-emitted (no subscription needed)
975
976 Ok(())
977 }
978
979 /// Handles a download event from the protocol
980 async fn on_download_event(&self, download: Download) {
981 let handlers = self.download_handlers.lock().unwrap().clone();
982
983 for handler in handlers {
984 if let Err(e) = handler(download.clone()).await {
985 tracing::warn!("Download handler error: {}", e);
986 }
987 }
988 }
989
990 /// Handles a dialog event from the protocol
991 async fn on_dialog_event(&self, dialog: Dialog) {
992 let handlers = self.dialog_handlers.lock().unwrap().clone();
993
994 for handler in handlers {
995 if let Err(e) = handler(dialog.clone()).await {
996 tracing::warn!("Dialog handler error: {}", e);
997 }
998 }
999 }
1000
1001 /// Triggers dialog event (called by BrowserContext when dialog events arrive)
1002 ///
1003 /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
1004 /// This method is public so BrowserContext can forward dialog events.
1005 pub async fn trigger_dialog_event(&self, dialog: Dialog) {
1006 self.on_dialog_event(dialog).await;
1007 }
1008
1009 /// Adds a `<style>` tag into the page with the desired content.
1010 ///
1011 /// # Arguments
1012 ///
1013 /// * `options` - Style tag options (content, url, or path)
1014 ///
1015 /// # Returns
1016 ///
1017 /// Returns an ElementHandle pointing to the injected `<style>` tag
1018 ///
1019 /// # Example
1020 ///
1021 /// ```no_run
1022 /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
1023 /// # #[tokio::main]
1024 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1025 /// # let playwright = Playwright::launch().await?;
1026 /// # let browser = playwright.chromium().launch().await?;
1027 /// # let context = browser.new_context().await?;
1028 /// # let page = context.new_page().await?;
1029 /// use playwright_rs::protocol::AddStyleTagOptions;
1030 ///
1031 /// // With inline CSS
1032 /// page.add_style_tag(
1033 /// AddStyleTagOptions::builder()
1034 /// .content("body { background-color: red; }")
1035 /// .build()
1036 /// ).await?;
1037 ///
1038 /// // With external URL
1039 /// page.add_style_tag(
1040 /// AddStyleTagOptions::builder()
1041 /// .url("https://example.com/style.css")
1042 /// .build()
1043 /// ).await?;
1044 ///
1045 /// // From file
1046 /// page.add_style_tag(
1047 /// AddStyleTagOptions::builder()
1048 /// .path("./styles/custom.css")
1049 /// .build()
1050 /// ).await?;
1051 /// # Ok(())
1052 /// # }
1053 /// ```
1054 ///
1055 /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1056 pub async fn add_style_tag(
1057 &self,
1058 options: AddStyleTagOptions,
1059 ) -> Result<Arc<crate::protocol::ElementHandle>> {
1060 let frame = self.main_frame().await?;
1061 frame.add_style_tag(options).await
1062 }
1063
1064 /// Adds a script which would be evaluated in one of the following scenarios:
1065 /// - Whenever the page is navigated
1066 /// - Whenever a child frame is attached or navigated
1067 ///
1068 /// The script is evaluated after the document was created but before any of its scripts were run.
1069 ///
1070 /// # Arguments
1071 ///
1072 /// * `script` - JavaScript code to be injected into the page
1073 ///
1074 /// # Example
1075 ///
1076 /// ```no_run
1077 /// # use playwright_rs::protocol::Playwright;
1078 /// # #[tokio::main]
1079 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1080 /// # let playwright = Playwright::launch().await?;
1081 /// # let browser = playwright.chromium().launch().await?;
1082 /// # let context = browser.new_context().await?;
1083 /// # let page = context.new_page().await?;
1084 /// page.add_init_script("window.injected = 123;").await?;
1085 /// # Ok(())
1086 /// # }
1087 /// ```
1088 ///
1089 /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
1090 pub async fn add_init_script(&self, script: &str) -> Result<()> {
1091 self.channel()
1092 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
1093 .await
1094 }
1095}
1096
1097impl ChannelOwner for Page {
1098 fn guid(&self) -> &str {
1099 self.base.guid()
1100 }
1101
1102 fn type_name(&self) -> &str {
1103 self.base.type_name()
1104 }
1105
1106 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
1107 self.base.parent()
1108 }
1109
1110 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
1111 self.base.connection()
1112 }
1113
1114 fn initializer(&self) -> &Value {
1115 self.base.initializer()
1116 }
1117
1118 fn channel(&self) -> &Channel {
1119 self.base.channel()
1120 }
1121
1122 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
1123 self.base.dispose(reason)
1124 }
1125
1126 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
1127 self.base.adopt(child)
1128 }
1129
1130 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
1131 self.base.add_child(guid, child)
1132 }
1133
1134 fn remove_child(&self, guid: &str) {
1135 self.base.remove_child(guid)
1136 }
1137
1138 fn on_event(&self, method: &str, params: Value) {
1139 match method {
1140 "navigated" => {
1141 // Update URL when page navigates
1142 if let Some(url_value) = params.get("url") {
1143 if let Some(url_str) = url_value.as_str() {
1144 if let Ok(mut url) = self.url.write() {
1145 *url = url_str.to_string();
1146 }
1147 }
1148 }
1149 }
1150 "route" => {
1151 // Handle network routing event
1152 if let Some(route_guid) = params
1153 .get("route")
1154 .and_then(|v| v.get("guid"))
1155 .and_then(|v| v.as_str())
1156 {
1157 // Get the Route object from connection's registry
1158 let connection = self.connection();
1159 let route_guid_owned = route_guid.to_string();
1160 let self_clone = self.clone();
1161
1162 tokio::spawn(async move {
1163 // Wait for Route object to be created
1164 let route_arc = match connection.get_object(&route_guid_owned).await {
1165 Ok(obj) => obj,
1166 Err(e) => {
1167 tracing::warn!("Failed to get route object: {}", e);
1168 return;
1169 }
1170 };
1171
1172 // Downcast to Route
1173 let route = match route_arc.as_any().downcast_ref::<Route>() {
1174 Some(r) => r.clone(),
1175 None => {
1176 tracing::warn!("Failed to downcast to Route");
1177 return;
1178 }
1179 };
1180
1181 // Call the route handler and wait for completion
1182 self_clone.on_route_event(route).await;
1183 });
1184 }
1185 }
1186 "download" => {
1187 // Handle download event
1188 // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
1189 let url = params
1190 .get("url")
1191 .and_then(|v| v.as_str())
1192 .unwrap_or("")
1193 .to_string();
1194
1195 let suggested_filename = params
1196 .get("suggestedFilename")
1197 .and_then(|v| v.as_str())
1198 .unwrap_or("")
1199 .to_string();
1200
1201 if let Some(artifact_guid) = params
1202 .get("artifact")
1203 .and_then(|v| v.get("guid"))
1204 .and_then(|v| v.as_str())
1205 {
1206 let connection = self.connection();
1207 let artifact_guid_owned = artifact_guid.to_string();
1208 let self_clone = self.clone();
1209
1210 tokio::spawn(async move {
1211 // Wait for Artifact object to be created
1212 let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
1213 Ok(obj) => obj,
1214 Err(e) => {
1215 tracing::warn!("Failed to get artifact object: {}", e);
1216 return;
1217 }
1218 };
1219
1220 // Create Download wrapper from Artifact + event params
1221 let download =
1222 Download::from_artifact(artifact_arc, url, suggested_filename);
1223
1224 // Call the download handlers
1225 self_clone.on_download_event(download).await;
1226 });
1227 }
1228 }
1229 "dialog" => {
1230 // Dialog events are handled by BrowserContext and forwarded to Page
1231 // This case should not be reached, but keeping for completeness
1232 }
1233 _ => {
1234 // Other events will be handled in future phases
1235 // Events: load, domcontentloaded, close, crash, etc.
1236 }
1237 }
1238 }
1239
1240 fn was_collected(&self) -> bool {
1241 self.base.was_collected()
1242 }
1243
1244 fn as_any(&self) -> &dyn Any {
1245 self
1246 }
1247}
1248
1249impl std::fmt::Debug for Page {
1250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1251 f.debug_struct("Page")
1252 .field("guid", &self.guid())
1253 .field("url", &self.url())
1254 .finish()
1255 }
1256}
1257
1258/// Options for page.goto() and page.reload()
1259#[derive(Debug, Clone)]
1260pub struct GotoOptions {
1261 /// Maximum operation time in milliseconds
1262 pub timeout: Option<std::time::Duration>,
1263 /// When to consider operation succeeded
1264 pub wait_until: Option<WaitUntil>,
1265}
1266
1267impl GotoOptions {
1268 /// Creates new GotoOptions with default values
1269 pub fn new() -> Self {
1270 Self {
1271 timeout: None,
1272 wait_until: None,
1273 }
1274 }
1275
1276 /// Sets the timeout
1277 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
1278 self.timeout = Some(timeout);
1279 self
1280 }
1281
1282 /// Sets the wait_until option
1283 pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
1284 self.wait_until = Some(wait_until);
1285 self
1286 }
1287}
1288
1289impl Default for GotoOptions {
1290 fn default() -> Self {
1291 Self::new()
1292 }
1293}
1294
1295/// When to consider navigation succeeded
1296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1297pub enum WaitUntil {
1298 /// Consider operation to be finished when the `load` event is fired
1299 Load,
1300 /// Consider operation to be finished when the `DOMContentLoaded` event is fired
1301 DomContentLoaded,
1302 /// Consider operation to be finished when there are no network connections for at least 500ms
1303 NetworkIdle,
1304 /// Consider operation to be finished when the commit event is fired
1305 Commit,
1306}
1307
1308impl WaitUntil {
1309 pub(crate) fn as_str(&self) -> &'static str {
1310 match self {
1311 WaitUntil::Load => "load",
1312 WaitUntil::DomContentLoaded => "domcontentloaded",
1313 WaitUntil::NetworkIdle => "networkidle",
1314 WaitUntil::Commit => "commit",
1315 }
1316 }
1317}
1318
1319/// Options for adding a style tag to the page
1320///
1321/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1322#[derive(Debug, Clone, Default)]
1323pub struct AddStyleTagOptions {
1324 /// Raw CSS content to inject
1325 pub content: Option<String>,
1326 /// URL of the `<link>` tag to add
1327 pub url: Option<String>,
1328 /// Path to a CSS file to inject
1329 pub path: Option<String>,
1330}
1331
1332impl AddStyleTagOptions {
1333 /// Creates a new builder for AddStyleTagOptions
1334 pub fn builder() -> AddStyleTagOptionsBuilder {
1335 AddStyleTagOptionsBuilder::default()
1336 }
1337
1338 /// Validates that at least one option is specified
1339 pub(crate) fn validate(&self) -> Result<()> {
1340 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
1341 return Err(Error::InvalidArgument(
1342 "At least one of content, url, or path must be specified".to_string(),
1343 ));
1344 }
1345 Ok(())
1346 }
1347}
1348
1349/// Builder for AddStyleTagOptions
1350#[derive(Debug, Clone, Default)]
1351pub struct AddStyleTagOptionsBuilder {
1352 content: Option<String>,
1353 url: Option<String>,
1354 path: Option<String>,
1355}
1356
1357impl AddStyleTagOptionsBuilder {
1358 /// Sets the CSS content to inject
1359 pub fn content(mut self, content: impl Into<String>) -> Self {
1360 self.content = Some(content.into());
1361 self
1362 }
1363
1364 /// Sets the URL of the stylesheet
1365 pub fn url(mut self, url: impl Into<String>) -> Self {
1366 self.url = Some(url.into());
1367 self
1368 }
1369
1370 /// Sets the path to a CSS file
1371 pub fn path(mut self, path: impl Into<String>) -> Self {
1372 self.path = Some(path.into());
1373 self
1374 }
1375
1376 /// Builds the AddStyleTagOptions
1377 pub fn build(self) -> AddStyleTagOptions {
1378 AddStyleTagOptions {
1379 content: self.content,
1380 url: self.url,
1381 path: self.path,
1382 }
1383 }
1384}
1385
1386/// Response from navigation operations
1387#[derive(Debug, Clone)]
1388pub struct Response {
1389 /// URL of the response
1390 pub url: String,
1391 /// HTTP status code
1392 pub status: u16,
1393 /// HTTP status text
1394 pub status_text: String,
1395 /// Whether the response was successful (status 200-299)
1396 pub ok: bool,
1397 /// Response headers
1398 pub headers: std::collections::HashMap<String, String>,
1399}
1400
1401impl Response {
1402 /// Returns the URL of the response
1403 pub fn url(&self) -> &str {
1404 &self.url
1405 }
1406
1407 /// Returns the HTTP status code
1408 pub fn status(&self) -> u16 {
1409 self.status
1410 }
1411
1412 /// Returns the HTTP status text
1413 pub fn status_text(&self) -> &str {
1414 &self.status_text
1415 }
1416
1417 /// Returns whether the response was successful (status 200-299)
1418 pub fn ok(&self) -> bool {
1419 self.ok
1420 }
1421
1422 /// Returns the response headers
1423 pub fn headers(&self) -> &std::collections::HashMap<String, String> {
1424 &self.headers
1425 }
1426}