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 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.
775 ///
776 /// Executes the provided JavaScript expression or function within the page's
777 /// context and returns the result. The return value must be JSON-serializable.
778 ///
779 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
780 pub async fn evaluate(&self, expression: &str) -> Result<()> {
781 // Delegate to the main frame, matching playwright-python's behavior
782 let frame = self.main_frame().await?;
783 frame.frame_evaluate_expression(expression).await
784 }
785
786 /// Evaluates a JavaScript expression and returns the result as a String.
787 ///
788 /// # Arguments
789 ///
790 /// * `expression` - JavaScript code to evaluate
791 ///
792 /// # Returns
793 ///
794 /// The result converted to a String
795 ///
796 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
797 pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
798 let frame = self.main_frame().await?;
799 frame.frame_evaluate_expression_value(expression).await
800 }
801
802 /// Registers a route handler for network interception.
803 ///
804 /// When a request matches the specified pattern, the handler will be called
805 /// with a Route object that can abort, continue, or fulfill the request.
806 ///
807 /// # Arguments
808 ///
809 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
810 /// * `handler` - Async closure that handles the route
811 ///
812 /// See: <https://playwright.dev/docs/api/class-page#page-route>
813 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
814 where
815 F: Fn(Route) -> Fut + Send + Sync + 'static,
816 Fut: Future<Output = Result<()>> + Send + 'static,
817 {
818 // 1. Wrap handler in Arc with type erasure
819 let handler =
820 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
821
822 // 2. Store in handlers list
823 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
824 pattern: pattern.to_string(),
825 handler,
826 });
827
828 // 3. Enable network interception via protocol
829 self.enable_network_interception().await?;
830
831 Ok(())
832 }
833
834 /// Updates network interception patterns for this page
835 async fn enable_network_interception(&self) -> Result<()> {
836 // Collect all patterns from registered handlers
837 // Each pattern must be an object with "glob" field
838 let patterns: Vec<serde_json::Value> = self
839 .route_handlers
840 .lock()
841 .unwrap()
842 .iter()
843 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
844 .collect();
845
846 // Send protocol command to update network interception patterns
847 // Follows playwright-python's approach
848 self.channel()
849 .send_no_result(
850 "setNetworkInterceptionPatterns",
851 serde_json::json!({
852 "patterns": patterns
853 }),
854 )
855 .await
856 }
857
858 /// Handles a route event from the protocol
859 ///
860 /// Called by on_event when a "route" event is received
861 async fn on_route_event(&self, route: Route) {
862 let handlers = self.route_handlers.lock().unwrap().clone();
863 let url = route.request().url().to_string();
864
865 // Find matching handler (last registered wins)
866 for entry in handlers.iter().rev() {
867 // Use glob pattern matching
868 if Self::matches_pattern(&entry.pattern, &url) {
869 let handler = entry.handler.clone();
870 // Execute handler and wait for completion
871 // This ensures fulfill/continue/abort completes before browser continues
872 if let Err(e) = handler(route).await {
873 tracing::warn!("Route handler error: {}", e);
874 }
875 break;
876 }
877 }
878 }
879
880 /// Checks if a URL matches a glob pattern
881 ///
882 /// Supports standard glob patterns:
883 /// - `*` matches any characters except `/`
884 /// - `**` matches any characters including `/`
885 /// - `?` matches a single character
886 fn matches_pattern(pattern: &str, url: &str) -> bool {
887 use glob::Pattern;
888
889 // Try to compile the glob pattern
890 match Pattern::new(pattern) {
891 Ok(glob_pattern) => glob_pattern.matches(url),
892 Err(_) => {
893 // If pattern is invalid, fall back to exact string match
894 pattern == url
895 }
896 }
897 }
898
899 /// Registers a download event handler.
900 ///
901 /// The handler will be called when a download is triggered by the page.
902 /// Downloads occur when the page initiates a file download (e.g., clicking a link
903 /// with the download attribute, or a server response with Content-Disposition: attachment).
904 ///
905 /// # Arguments
906 ///
907 /// * `handler` - Async closure that receives the Download object
908 ///
909 /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
910 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
911 where
912 F: Fn(Download) -> Fut + Send + Sync + 'static,
913 Fut: Future<Output = Result<()>> + Send + 'static,
914 {
915 // Wrap handler with type erasure
916 let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
917 Box::pin(handler(download))
918 });
919
920 // Store handler
921 self.download_handlers.lock().unwrap().push(handler);
922
923 Ok(())
924 }
925
926 /// Registers a dialog event handler.
927 ///
928 /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
929 /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
930 ///
931 /// # Arguments
932 ///
933 /// * `handler` - Async closure that receives the Dialog object
934 ///
935 /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
936 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
937 where
938 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
939 Fut: Future<Output = Result<()>> + Send + 'static,
940 {
941 // Wrap handler with type erasure
942 let handler =
943 Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
944
945 // Store handler
946 self.dialog_handlers.lock().unwrap().push(handler);
947
948 // Dialog events are auto-emitted (no subscription needed)
949
950 Ok(())
951 }
952
953 /// Handles a download event from the protocol
954 async fn on_download_event(&self, download: Download) {
955 let handlers = self.download_handlers.lock().unwrap().clone();
956
957 for handler in handlers {
958 if let Err(e) = handler(download.clone()).await {
959 tracing::warn!("Download handler error: {}", e);
960 }
961 }
962 }
963
964 /// Handles a dialog event from the protocol
965 async fn on_dialog_event(&self, dialog: Dialog) {
966 let handlers = self.dialog_handlers.lock().unwrap().clone();
967
968 for handler in handlers {
969 if let Err(e) = handler(dialog.clone()).await {
970 tracing::warn!("Dialog handler error: {}", e);
971 }
972 }
973 }
974
975 /// Triggers dialog event (called by BrowserContext when dialog events arrive)
976 ///
977 /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
978 /// This method is public so BrowserContext can forward dialog events.
979 pub async fn trigger_dialog_event(&self, dialog: Dialog) {
980 self.on_dialog_event(dialog).await;
981 }
982
983 /// Adds a `<style>` tag into the page with the desired content.
984 ///
985 /// # Arguments
986 ///
987 /// * `options` - Style tag options (content, url, or path)
988 ///
989 /// # Returns
990 ///
991 /// Returns an ElementHandle pointing to the injected `<style>` tag
992 ///
993 /// # Example
994 ///
995 /// ```no_run
996 /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
997 /// # #[tokio::main]
998 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
999 /// # let playwright = Playwright::launch().await?;
1000 /// # let browser = playwright.chromium().launch().await?;
1001 /// # let context = browser.new_context().await?;
1002 /// # let page = context.new_page().await?;
1003 /// use playwright_rs::protocol::AddStyleTagOptions;
1004 ///
1005 /// // With inline CSS
1006 /// page.add_style_tag(
1007 /// AddStyleTagOptions::builder()
1008 /// .content("body { background-color: red; }")
1009 /// .build()
1010 /// ).await?;
1011 ///
1012 /// // With external URL
1013 /// page.add_style_tag(
1014 /// AddStyleTagOptions::builder()
1015 /// .url("https://example.com/style.css")
1016 /// .build()
1017 /// ).await?;
1018 ///
1019 /// // From file
1020 /// page.add_style_tag(
1021 /// AddStyleTagOptions::builder()
1022 /// .path("./styles/custom.css")
1023 /// .build()
1024 /// ).await?;
1025 /// # Ok(())
1026 /// # }
1027 /// ```
1028 ///
1029 /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1030 pub async fn add_style_tag(
1031 &self,
1032 options: AddStyleTagOptions,
1033 ) -> Result<Arc<crate::protocol::ElementHandle>> {
1034 let frame = self.main_frame().await?;
1035 frame.add_style_tag(options).await
1036 }
1037
1038 /// Adds a script which would be evaluated in one of the following scenarios:
1039 /// - Whenever the page is navigated
1040 /// - Whenever a child frame is attached or navigated
1041 ///
1042 /// The script is evaluated after the document was created but before any of its scripts were run.
1043 ///
1044 /// # Arguments
1045 ///
1046 /// * `script` - JavaScript code to be injected into the page
1047 ///
1048 /// # Example
1049 ///
1050 /// ```no_run
1051 /// # use playwright_rs::protocol::Playwright;
1052 /// # #[tokio::main]
1053 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1054 /// # let playwright = Playwright::launch().await?;
1055 /// # let browser = playwright.chromium().launch().await?;
1056 /// # let context = browser.new_context().await?;
1057 /// # let page = context.new_page().await?;
1058 /// page.add_init_script("window.injected = 123;").await?;
1059 /// # Ok(())
1060 /// # }
1061 /// ```
1062 ///
1063 /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
1064 pub async fn add_init_script(&self, script: &str) -> Result<()> {
1065 self.channel()
1066 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
1067 .await
1068 }
1069}
1070
1071impl ChannelOwner for Page {
1072 fn guid(&self) -> &str {
1073 self.base.guid()
1074 }
1075
1076 fn type_name(&self) -> &str {
1077 self.base.type_name()
1078 }
1079
1080 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
1081 self.base.parent()
1082 }
1083
1084 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
1085 self.base.connection()
1086 }
1087
1088 fn initializer(&self) -> &Value {
1089 self.base.initializer()
1090 }
1091
1092 fn channel(&self) -> &Channel {
1093 self.base.channel()
1094 }
1095
1096 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
1097 self.base.dispose(reason)
1098 }
1099
1100 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
1101 self.base.adopt(child)
1102 }
1103
1104 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
1105 self.base.add_child(guid, child)
1106 }
1107
1108 fn remove_child(&self, guid: &str) {
1109 self.base.remove_child(guid)
1110 }
1111
1112 fn on_event(&self, method: &str, params: Value) {
1113 match method {
1114 "navigated" => {
1115 // Update URL when page navigates
1116 if let Some(url_value) = params.get("url") {
1117 if let Some(url_str) = url_value.as_str() {
1118 if let Ok(mut url) = self.url.write() {
1119 *url = url_str.to_string();
1120 }
1121 }
1122 }
1123 }
1124 "route" => {
1125 // Handle network routing event
1126 if let Some(route_guid) = params
1127 .get("route")
1128 .and_then(|v| v.get("guid"))
1129 .and_then(|v| v.as_str())
1130 {
1131 // Get the Route object from connection's registry
1132 let connection = self.connection();
1133 let route_guid_owned = route_guid.to_string();
1134 let self_clone = self.clone();
1135
1136 tokio::spawn(async move {
1137 // Wait for Route object to be created
1138 let route_arc = match connection.get_object(&route_guid_owned).await {
1139 Ok(obj) => obj,
1140 Err(e) => {
1141 tracing::warn!("Failed to get route object: {}", e);
1142 return;
1143 }
1144 };
1145
1146 // Downcast to Route
1147 let route = match route_arc.as_any().downcast_ref::<Route>() {
1148 Some(r) => r.clone(),
1149 None => {
1150 tracing::warn!("Failed to downcast to Route");
1151 return;
1152 }
1153 };
1154
1155 // Call the route handler and wait for completion
1156 self_clone.on_route_event(route).await;
1157 });
1158 }
1159 }
1160 "download" => {
1161 // Handle download event
1162 // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
1163 let url = params
1164 .get("url")
1165 .and_then(|v| v.as_str())
1166 .unwrap_or("")
1167 .to_string();
1168
1169 let suggested_filename = params
1170 .get("suggestedFilename")
1171 .and_then(|v| v.as_str())
1172 .unwrap_or("")
1173 .to_string();
1174
1175 if let Some(artifact_guid) = params
1176 .get("artifact")
1177 .and_then(|v| v.get("guid"))
1178 .and_then(|v| v.as_str())
1179 {
1180 let connection = self.connection();
1181 let artifact_guid_owned = artifact_guid.to_string();
1182 let self_clone = self.clone();
1183
1184 tokio::spawn(async move {
1185 // Wait for Artifact object to be created
1186 let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
1187 Ok(obj) => obj,
1188 Err(e) => {
1189 tracing::warn!("Failed to get artifact object: {}", e);
1190 return;
1191 }
1192 };
1193
1194 // Create Download wrapper from Artifact + event params
1195 let download =
1196 Download::from_artifact(artifact_arc, url, suggested_filename);
1197
1198 // Call the download handlers
1199 self_clone.on_download_event(download).await;
1200 });
1201 }
1202 }
1203 "dialog" => {
1204 // Dialog events are handled by BrowserContext and forwarded to Page
1205 // This case should not be reached, but keeping for completeness
1206 }
1207 _ => {
1208 // Other events will be handled in future phases
1209 // Events: load, domcontentloaded, close, crash, etc.
1210 }
1211 }
1212 }
1213
1214 fn was_collected(&self) -> bool {
1215 self.base.was_collected()
1216 }
1217
1218 fn as_any(&self) -> &dyn Any {
1219 self
1220 }
1221}
1222
1223impl std::fmt::Debug for Page {
1224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1225 f.debug_struct("Page")
1226 .field("guid", &self.guid())
1227 .field("url", &self.url())
1228 .finish()
1229 }
1230}
1231
1232/// Options for page.goto() and page.reload()
1233#[derive(Debug, Clone)]
1234pub struct GotoOptions {
1235 /// Maximum operation time in milliseconds
1236 pub timeout: Option<std::time::Duration>,
1237 /// When to consider operation succeeded
1238 pub wait_until: Option<WaitUntil>,
1239}
1240
1241impl GotoOptions {
1242 /// Creates new GotoOptions with default values
1243 pub fn new() -> Self {
1244 Self {
1245 timeout: None,
1246 wait_until: None,
1247 }
1248 }
1249
1250 /// Sets the timeout
1251 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
1252 self.timeout = Some(timeout);
1253 self
1254 }
1255
1256 /// Sets the wait_until option
1257 pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
1258 self.wait_until = Some(wait_until);
1259 self
1260 }
1261}
1262
1263impl Default for GotoOptions {
1264 fn default() -> Self {
1265 Self::new()
1266 }
1267}
1268
1269/// When to consider navigation succeeded
1270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1271pub enum WaitUntil {
1272 /// Consider operation to be finished when the `load` event is fired
1273 Load,
1274 /// Consider operation to be finished when the `DOMContentLoaded` event is fired
1275 DomContentLoaded,
1276 /// Consider operation to be finished when there are no network connections for at least 500ms
1277 NetworkIdle,
1278 /// Consider operation to be finished when the commit event is fired
1279 Commit,
1280}
1281
1282impl WaitUntil {
1283 pub(crate) fn as_str(&self) -> &'static str {
1284 match self {
1285 WaitUntil::Load => "load",
1286 WaitUntil::DomContentLoaded => "domcontentloaded",
1287 WaitUntil::NetworkIdle => "networkidle",
1288 WaitUntil::Commit => "commit",
1289 }
1290 }
1291}
1292
1293/// Options for adding a style tag to the page
1294///
1295/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1296#[derive(Debug, Clone, Default)]
1297pub struct AddStyleTagOptions {
1298 /// Raw CSS content to inject
1299 pub content: Option<String>,
1300 /// URL of the `<link>` tag to add
1301 pub url: Option<String>,
1302 /// Path to a CSS file to inject
1303 pub path: Option<String>,
1304}
1305
1306impl AddStyleTagOptions {
1307 /// Creates a new builder for AddStyleTagOptions
1308 pub fn builder() -> AddStyleTagOptionsBuilder {
1309 AddStyleTagOptionsBuilder::default()
1310 }
1311
1312 /// Validates that at least one option is specified
1313 pub(crate) fn validate(&self) -> Result<()> {
1314 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
1315 return Err(Error::InvalidArgument(
1316 "At least one of content, url, or path must be specified".to_string(),
1317 ));
1318 }
1319 Ok(())
1320 }
1321}
1322
1323/// Builder for AddStyleTagOptions
1324#[derive(Debug, Clone, Default)]
1325pub struct AddStyleTagOptionsBuilder {
1326 content: Option<String>,
1327 url: Option<String>,
1328 path: Option<String>,
1329}
1330
1331impl AddStyleTagOptionsBuilder {
1332 /// Sets the CSS content to inject
1333 pub fn content(mut self, content: impl Into<String>) -> Self {
1334 self.content = Some(content.into());
1335 self
1336 }
1337
1338 /// Sets the URL of the stylesheet
1339 pub fn url(mut self, url: impl Into<String>) -> Self {
1340 self.url = Some(url.into());
1341 self
1342 }
1343
1344 /// Sets the path to a CSS file
1345 pub fn path(mut self, path: impl Into<String>) -> Self {
1346 self.path = Some(path.into());
1347 self
1348 }
1349
1350 /// Builds the AddStyleTagOptions
1351 pub fn build(self) -> AddStyleTagOptions {
1352 AddStyleTagOptions {
1353 content: self.content,
1354 url: self.url,
1355 path: self.path,
1356 }
1357 }
1358}
1359
1360/// Response from navigation operations
1361#[derive(Debug, Clone)]
1362pub struct Response {
1363 /// URL of the response
1364 pub url: String,
1365 /// HTTP status code
1366 pub status: u16,
1367 /// HTTP status text
1368 pub status_text: String,
1369 /// Whether the response was successful (status 200-299)
1370 pub ok: bool,
1371 /// Response headers
1372 pub headers: std::collections::HashMap<String, String>,
1373}
1374
1375impl Response {
1376 /// Returns the URL of the response
1377 pub fn url(&self) -> &str {
1378 &self.url
1379 }
1380
1381 /// Returns the HTTP status code
1382 pub fn status(&self) -> u16 {
1383 self.status
1384 }
1385
1386 /// Returns the HTTP status text
1387 pub fn status_text(&self) -> &str {
1388 &self.status_text
1389 }
1390
1391 /// Returns whether the response was successful (status 200-299)
1392 pub fn ok(&self) -> bool {
1393 self.ok
1394 }
1395
1396 /// Returns the response headers
1397 pub fn headers(&self) -> &std::collections::HashMap<String, String> {
1398 &self.headers
1399 }
1400}