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::browser_context::Viewport;
8use crate::protocol::{Dialog, Download, Request, ResponseObject, Route, WebSocket, Worker};
9use crate::server::channel::Channel;
10use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
11use crate::server::connection::{ConnectionExt, downcast_parent};
12use base64::Engine;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::any::Any;
16use std::collections::HashMap;
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::sync::{Arc, Mutex, RwLock};
21
22/// Page represents a web page within a browser context.
23///
24/// A Page is created when you call `BrowserContext::new_page()` or `Browser::new_page()`.
25/// Each page is an isolated tab/window within its parent context.
26///
27/// Initially, pages are navigated to "about:blank". Use navigation methods
28/// Use navigation methods to navigate to URLs.
29///
30/// # Example
31///
32/// ```ignore
33/// use playwright_rs::protocol::{
34/// Playwright, ScreenshotOptions, ScreenshotType, AddStyleTagOptions, AddScriptTagOptions,
35/// EmulateMediaOptions, Media, ColorScheme, Viewport,
36/// };
37/// use std::path::PathBuf;
38///
39/// #[tokio::main]
40/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
41/// let playwright = Playwright::launch().await?;
42/// let browser = playwright.chromium().launch().await?;
43/// let page = browser.new_page().await?;
44///
45/// // Demonstrate url() - initially at about:blank
46/// assert_eq!(page.url(), "about:blank");
47///
48/// // Demonstrate goto() - navigate to a page
49/// let html = r#"<!DOCTYPE html>
50/// <html>
51/// <head><title>Test Page</title></head>
52/// <body>
53/// <h1 id="heading">Hello World</h1>
54/// <p>First paragraph</p>
55/// <p>Second paragraph</p>
56/// <button onclick="alert('Alert!')">Alert</button>
57/// <a href="data:text/plain,file" download="test.txt">Download</a>
58/// </body>
59/// </html>
60/// "#;
61/// // Data URLs may not return a response (this is normal)
62/// let _response = page.goto(&format!("data:text/html,{}", html), None).await?;
63///
64/// // Demonstrate title()
65/// let title = page.title().await?;
66/// assert_eq!(title, "Test Page");
67///
68/// // Demonstrate content() - returns full HTML including DOCTYPE
69/// let content = page.content().await?;
70/// assert!(content.contains("<!DOCTYPE html>") || content.to_lowercase().contains("<!doctype html>"));
71/// assert!(content.contains("<title>Test Page</title>"));
72/// assert!(content.contains("Hello World"));
73///
74/// // Demonstrate locator()
75/// let heading = page.locator("#heading").await;
76/// let text = heading.text_content().await?;
77/// assert_eq!(text, Some("Hello World".to_string()));
78///
79/// // Demonstrate query_selector()
80/// let element = page.query_selector("h1").await?;
81/// assert!(element.is_some(), "Should find the h1 element");
82///
83/// // Demonstrate query_selector_all()
84/// let paragraphs = page.query_selector_all("p").await?;
85/// assert_eq!(paragraphs.len(), 2);
86///
87/// // Demonstrate evaluate()
88/// page.evaluate::<(), ()>("console.log('Hello from Playwright!')", None).await?;
89///
90/// // Demonstrate evaluate_value()
91/// let result = page.evaluate_value("1 + 1").await?;
92/// assert_eq!(result, "2");
93///
94/// // Demonstrate screenshot()
95/// let bytes = page.screenshot(None).await?;
96/// assert!(!bytes.is_empty());
97///
98/// // Demonstrate screenshot_to_file()
99/// let temp_dir = std::env::temp_dir();
100/// let path = temp_dir.join("playwright_doctest_screenshot.png");
101/// let bytes = page.screenshot_to_file(&path, Some(
102/// ScreenshotOptions::builder()
103/// .screenshot_type(ScreenshotType::Png)
104/// .build()
105/// )).await?;
106/// assert!(!bytes.is_empty());
107///
108/// // Demonstrate reload()
109/// // Data URLs may not return a response on reload (this is normal)
110/// let _response = page.reload(None).await?;
111///
112/// // Demonstrate route() - network interception
113/// page.route("**/*.png", |route| async move {
114/// route.abort(None).await
115/// }).await?;
116///
117/// // Demonstrate on_download() - download handler
118/// page.on_download(|download| async move {
119/// println!("Download started: {}", download.url());
120/// Ok(())
121/// }).await?;
122///
123/// // Demonstrate on_dialog() - dialog handler
124/// page.on_dialog(|dialog| async move {
125/// println!("Dialog: {} - {}", dialog.type_(), dialog.message());
126/// dialog.accept(None).await
127/// }).await?;
128///
129/// // Demonstrate add_style_tag() - inject CSS
130/// page.add_style_tag(
131/// AddStyleTagOptions::builder()
132/// .content("body { background-color: blue; }")
133/// .build()
134/// ).await?;
135///
136/// // Demonstrate set_extra_http_headers() - set page-level headers
137/// let mut headers = std::collections::HashMap::new();
138/// headers.insert("x-custom-header".to_string(), "value".to_string());
139/// page.set_extra_http_headers(headers).await?;
140///
141/// // Demonstrate emulate_media() - emulate print media type
142/// page.emulate_media(Some(
143/// EmulateMediaOptions::builder()
144/// .media(Media::Print)
145/// .color_scheme(ColorScheme::Dark)
146/// .build()
147/// )).await?;
148///
149/// // Demonstrate add_script_tag() - inject a script
150/// page.add_script_tag(Some(
151/// AddScriptTagOptions::builder()
152/// .content("window.injectedByScriptTag = true;")
153/// .build()
154/// )).await?;
155///
156/// // Demonstrate pdf() - generate PDF (Chromium only)
157/// let pdf_bytes = page.pdf(None).await?;
158/// assert!(!pdf_bytes.is_empty());
159///
160/// // Demonstrate set_viewport_size() - responsive testing
161/// let mobile_viewport = Viewport {
162/// width: 375,
163/// height: 667,
164/// };
165/// page.set_viewport_size(mobile_viewport).await?;
166///
167/// // Demonstrate close()
168/// page.close().await?;
169///
170/// browser.close().await?;
171/// Ok(())
172/// }
173/// ```
174///
175/// See: <https://playwright.dev/docs/api/class-page>
176#[derive(Clone)]
177pub struct Page {
178 base: ChannelOwnerImpl,
179 /// Current URL of the page
180 /// Wrapped in RwLock to allow updates from events
181 url: Arc<RwLock<String>>,
182 /// GUID of the main frame
183 main_frame_guid: Arc<str>,
184 /// Cached reference to the main frame for synchronous URL access
185 /// This is populated after the first call to main_frame()
186 cached_main_frame: Arc<Mutex<Option<crate::protocol::Frame>>>,
187 /// Route handlers for network interception
188 route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
189 /// Download event handlers
190 download_handlers: Arc<Mutex<Vec<DownloadHandler>>>,
191 /// Dialog event handlers
192 dialog_handlers: Arc<Mutex<Vec<DialogHandler>>>,
193 /// Request event handlers
194 request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
195 /// Request finished event handlers
196 request_finished_handlers: Arc<Mutex<Vec<RequestHandler>>>,
197 /// Request failed event handlers
198 request_failed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
199 /// Response event handlers
200 response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
201 /// WebSocket event handlers
202 websocket_handlers: Arc<Mutex<Vec<WebSocketHandler>>>,
203 /// Current viewport size (None when no_viewport is set).
204 /// Updated by set_viewport_size().
205 viewport: Arc<RwLock<Option<Viewport>>>,
206 /// Whether this page has been closed.
207 /// Set to true when close() is called or a "close" event is received.
208 is_closed: Arc<AtomicBool>,
209 /// Default timeout for actions (milliseconds), stored as f64 bits.
210 default_timeout_ms: Arc<AtomicU64>,
211 /// Default timeout for navigation operations (milliseconds), stored as f64 bits.
212 default_navigation_timeout_ms: Arc<AtomicU64>,
213 /// Page-level binding callbacks registered via expose_function / expose_binding
214 binding_callbacks: Arc<Mutex<HashMap<String, PageBindingCallback>>>,
215 /// Console event handlers
216 console_handlers: Arc<Mutex<Vec<ConsoleHandler>>>,
217 /// FileChooser event handlers
218 filechooser_handlers: Arc<Mutex<Vec<FileChooserHandler>>>,
219 /// One-shot senders waiting for the next "fileChooser" event (expect_file_chooser)
220 filechooser_waiters:
221 Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::FileChooser>>>>,
222 /// One-shot senders waiting for the next "popup" event (expect_popup)
223 popup_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Page>>>>,
224 /// One-shot senders waiting for the next "download" event (expect_download)
225 download_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Download>>>>,
226 /// One-shot senders waiting for the next "response" event (expect_response)
227 response_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<ResponseObject>>>>,
228 /// One-shot senders waiting for the next "request" event (expect_request)
229 request_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Request>>>>,
230 /// One-shot senders waiting for the next "console" event (expect_console_message)
231 console_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::ConsoleMessage>>>>,
232 /// close event handlers (fires when page is closed)
233 close_handlers: Arc<Mutex<Vec<CloseHandler>>>,
234 /// load event handlers (fires when page fully loads)
235 load_handlers: Arc<Mutex<Vec<LoadHandler>>>,
236 /// crash event handlers (fires when page crashes)
237 crash_handlers: Arc<Mutex<Vec<CrashHandler>>>,
238 /// pageError event handlers (fires on uncaught JS exceptions)
239 pageerror_handlers: Arc<Mutex<Vec<PageErrorHandler>>>,
240 /// popup event handlers (fires when a popup window opens)
241 popup_handlers: Arc<Mutex<Vec<PopupHandler>>>,
242 /// frameAttached event handlers
243 frameattached_handlers: Arc<Mutex<Vec<FrameAttachedHandler>>>,
244 /// frameDetached event handlers
245 framedetached_handlers: Arc<Mutex<Vec<FrameDetachedHandler>>>,
246 /// frameNavigated event handlers
247 framenavigated_handlers: Arc<Mutex<Vec<FrameNavigatedHandler>>>,
248 /// worker event handlers (fires when a web worker is created in the page)
249 worker_handlers: Arc<Mutex<Vec<WorkerHandler>>>,
250}
251
252/// Type alias for boxed route handler future
253type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
254
255/// Type alias for boxed download handler future
256type DownloadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
257
258/// Type alias for boxed dialog handler future
259type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
260
261/// Type alias for boxed request handler future
262type RequestHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
263
264/// Type alias for boxed response handler future
265type ResponseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
266
267/// Type alias for boxed websocket handler future
268type WebSocketHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
269
270/// Storage for a single route handler
271#[derive(Clone)]
272struct RouteHandlerEntry {
273 pattern: String,
274 handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
275}
276
277/// Download event handler
278type DownloadHandler = Arc<dyn Fn(Download) -> DownloadHandlerFuture + Send + Sync>;
279
280/// Dialog event handler
281type DialogHandler = Arc<dyn Fn(Dialog) -> DialogHandlerFuture + Send + Sync>;
282
283/// Request event handler
284type RequestHandler = Arc<dyn Fn(Request) -> RequestHandlerFuture + Send + Sync>;
285
286/// Response event handler
287type ResponseHandler = Arc<dyn Fn(ResponseObject) -> ResponseHandlerFuture + Send + Sync>;
288
289/// WebSocket event handler
290type WebSocketHandler = Arc<dyn Fn(WebSocket) -> WebSocketHandlerFuture + Send + Sync>;
291
292/// Type alias for boxed console handler future
293type ConsoleHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
294
295/// Console event handler
296type ConsoleHandler =
297 Arc<dyn Fn(crate::protocol::ConsoleMessage) -> ConsoleHandlerFuture + Send + Sync>;
298
299/// Type alias for boxed filechooser handler future
300type FileChooserHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
301
302/// FileChooser event handler
303type FileChooserHandler =
304 Arc<dyn Fn(crate::protocol::FileChooser) -> FileChooserHandlerFuture + Send + Sync>;
305
306/// Type alias for boxed close handler future
307type CloseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
308
309/// close event handler (no arguments)
310type CloseHandler = Arc<dyn Fn() -> CloseHandlerFuture + Send + Sync>;
311
312/// Type alias for boxed load handler future
313type LoadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
314
315/// load event handler (no arguments)
316type LoadHandler = Arc<dyn Fn() -> LoadHandlerFuture + Send + Sync>;
317
318/// Type alias for boxed crash handler future
319type CrashHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
320
321/// crash event handler (no arguments)
322type CrashHandler = Arc<dyn Fn() -> CrashHandlerFuture + Send + Sync>;
323
324/// Type alias for boxed pageError handler future
325type PageErrorHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
326
327/// pageError event handler — receives the error message as a String
328type PageErrorHandler = Arc<dyn Fn(String) -> PageErrorHandlerFuture + Send + Sync>;
329
330/// Type alias for boxed popup handler future
331type PopupHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
332
333/// popup event handler — receives the new popup Page
334type PopupHandler = Arc<dyn Fn(Page) -> PopupHandlerFuture + Send + Sync>;
335
336/// Type alias for boxed frameAttached/Detached/Navigated handler future
337type FrameEventHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
338
339/// frameAttached event handler
340type FrameAttachedHandler =
341 Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
342
343/// frameDetached event handler
344type FrameDetachedHandler =
345 Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
346
347/// frameNavigated event handler
348type FrameNavigatedHandler =
349 Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
350
351/// Type alias for boxed worker handler future
352type WorkerHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
353
354/// worker event handler — receives the new Worker
355type WorkerHandler = Arc<dyn Fn(crate::protocol::Worker) -> WorkerHandlerFuture + Send + Sync>;
356
357/// Type alias for boxed page-level binding callback future
358type PageBindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
359
360/// Page-level binding callback: receives deserialized JS args, returns a JSON value
361type PageBindingCallback =
362 Arc<dyn Fn(Vec<serde_json::Value>) -> PageBindingCallbackFuture + Send + Sync>;
363
364impl Page {
365 /// Creates a new Page from protocol initialization
366 ///
367 /// This is called by the object factory when the server sends a `__create__` message
368 /// for a Page object.
369 ///
370 /// # Arguments
371 ///
372 /// * `parent` - The parent BrowserContext object
373 /// * `type_name` - The protocol type name ("Page")
374 /// * `guid` - The unique identifier for this page
375 /// * `initializer` - The initialization data from the server
376 ///
377 /// # Errors
378 ///
379 /// Returns error if initializer is malformed
380 pub fn new(
381 parent: Arc<dyn ChannelOwner>,
382 type_name: String,
383 guid: Arc<str>,
384 initializer: Value,
385 ) -> Result<Self> {
386 // Extract mainFrame GUID from initializer
387 let main_frame_guid: Arc<str> =
388 Arc::from(initializer["mainFrame"]["guid"].as_str().ok_or_else(|| {
389 crate::error::Error::ProtocolError(
390 "Page initializer missing 'mainFrame.guid' field".to_string(),
391 )
392 })?);
393
394 let base = ChannelOwnerImpl::new(
395 ParentOrConnection::Parent(parent),
396 type_name,
397 guid,
398 initializer,
399 );
400
401 // Initialize URL to about:blank
402 let url = Arc::new(RwLock::new("about:blank".to_string()));
403
404 // Initialize empty route handlers
405 let route_handlers = Arc::new(Mutex::new(Vec::new()));
406
407 // Initialize empty event handlers
408 let download_handlers = Arc::new(Mutex::new(Vec::new()));
409 let dialog_handlers = Arc::new(Mutex::new(Vec::new()));
410 let websocket_handlers = Arc::new(Mutex::new(Vec::new()));
411
412 // Initialize cached main frame as empty (will be populated on first access)
413 let cached_main_frame = Arc::new(Mutex::new(None));
414
415 // Extract viewport from initializer (may be null for no_viewport contexts)
416 let initial_viewport: Option<Viewport> =
417 base.initializer().get("viewportSize").and_then(|v| {
418 if v.is_null() {
419 None
420 } else {
421 serde_json::from_value(v.clone()).ok()
422 }
423 });
424 let viewport = Arc::new(RwLock::new(initial_viewport));
425
426 Ok(Self {
427 base,
428 url,
429 main_frame_guid,
430 cached_main_frame,
431 route_handlers,
432 download_handlers,
433 dialog_handlers,
434 request_handlers: Default::default(),
435 request_finished_handlers: Default::default(),
436 request_failed_handlers: Default::default(),
437 response_handlers: Default::default(),
438 websocket_handlers,
439 viewport,
440 is_closed: Arc::new(AtomicBool::new(false)),
441 default_timeout_ms: Arc::new(AtomicU64::new(crate::DEFAULT_TIMEOUT_MS.to_bits())),
442 default_navigation_timeout_ms: Arc::new(AtomicU64::new(
443 crate::DEFAULT_TIMEOUT_MS.to_bits(),
444 )),
445 binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
446 console_handlers: Arc::new(Mutex::new(Vec::new())),
447 filechooser_handlers: Arc::new(Mutex::new(Vec::new())),
448 filechooser_waiters: Arc::new(Mutex::new(Vec::new())),
449 popup_waiters: Arc::new(Mutex::new(Vec::new())),
450 download_waiters: Arc::new(Mutex::new(Vec::new())),
451 response_waiters: Arc::new(Mutex::new(Vec::new())),
452 request_waiters: Arc::new(Mutex::new(Vec::new())),
453 console_waiters: Arc::new(Mutex::new(Vec::new())),
454 close_handlers: Arc::new(Mutex::new(Vec::new())),
455 load_handlers: Arc::new(Mutex::new(Vec::new())),
456 crash_handlers: Arc::new(Mutex::new(Vec::new())),
457 pageerror_handlers: Arc::new(Mutex::new(Vec::new())),
458 popup_handlers: Arc::new(Mutex::new(Vec::new())),
459 frameattached_handlers: Arc::new(Mutex::new(Vec::new())),
460 framedetached_handlers: Arc::new(Mutex::new(Vec::new())),
461 framenavigated_handlers: Arc::new(Mutex::new(Vec::new())),
462 worker_handlers: Arc::new(Mutex::new(Vec::new())),
463 })
464 }
465
466 /// Returns the channel for sending protocol messages
467 ///
468 /// Used internally for sending RPC calls to the page.
469 fn channel(&self) -> &Channel {
470 self.base.channel()
471 }
472
473 /// Returns the main frame of the page.
474 ///
475 /// The main frame is where navigation and DOM operations actually happen.
476 ///
477 /// This method also wires up the back-reference from the frame to the page so that
478 /// `frame.page()`, `frame.locator()`, and `frame.get_by_*()` work correctly.
479 pub async fn main_frame(&self) -> Result<crate::protocol::Frame> {
480 // Get and downcast the Frame object from the connection's object registry
481 let frame: crate::protocol::Frame = self
482 .connection()
483 .get_typed::<crate::protocol::Frame>(&self.main_frame_guid)
484 .await?;
485
486 // Wire up the back-reference so frame.page() / frame.locator() work.
487 // This is safe to call multiple times (subsequent calls are no-ops once set).
488 frame.set_page(self.clone());
489
490 // Cache the frame for synchronous access in url()
491 if let Ok(mut cached) = self.cached_main_frame.lock() {
492 *cached = Some(frame.clone());
493 }
494
495 Ok(frame)
496 }
497
498 /// Returns the current URL of the page.
499 ///
500 /// This returns the last committed URL, including hash fragments from anchor navigation.
501 /// Initially, pages are at "about:blank".
502 ///
503 /// See: <https://playwright.dev/docs/api/class-page#page-url>
504 pub fn url(&self) -> String {
505 // Try to get URL from the cached main frame (source of truth for navigation including hashes)
506 if let Ok(cached) = self.cached_main_frame.lock()
507 && let Some(frame) = cached.as_ref()
508 {
509 return frame.url();
510 }
511
512 // Fallback to cached URL if frame not yet loaded
513 self.url.read().unwrap().clone()
514 }
515
516 /// Closes the page.
517 ///
518 /// This is a graceful operation that sends a close command to the page
519 /// and waits for it to shut down properly.
520 ///
521 /// # Errors
522 ///
523 /// Returns error if:
524 /// - Page has already been closed
525 /// - Communication with browser process fails
526 ///
527 /// See: <https://playwright.dev/docs/api/class-page#page-close>
528 pub async fn close(&self) -> Result<()> {
529 // Send close RPC to server
530 let result = self
531 .channel()
532 .send_no_result("close", serde_json::json!({}))
533 .await;
534 // Mark as closed regardless of error (best-effort)
535 self.is_closed.store(true, Ordering::Relaxed);
536 result
537 }
538
539 /// Returns whether the page has been closed.
540 ///
541 /// Returns `true` after `close()` has been called on this page, or after the
542 /// page receives a close event from the server (e.g. when the browser context
543 /// is closed).
544 ///
545 /// See: <https://playwright.dev/docs/api/class-page#page-is-closed>
546 pub fn is_closed(&self) -> bool {
547 self.is_closed.load(Ordering::Relaxed)
548 }
549
550 /// Sets the default timeout for all operations on this page.
551 ///
552 /// The timeout applies to actions such as `click`, `fill`, `locator.wait_for`, etc.
553 /// Pass `0` to disable timeouts.
554 ///
555 /// This stores the value locally so that subsequent action calls use it when
556 /// no explicit timeout is provided, and also notifies the Playwright server
557 /// so it can apply the same default on its side.
558 ///
559 /// # Arguments
560 ///
561 /// * `timeout` - Timeout in milliseconds
562 ///
563 /// See: <https://playwright.dev/docs/api/class-page#page-set-default-timeout>
564 pub async fn set_default_timeout(&self, timeout: f64) {
565 self.default_timeout_ms
566 .store(timeout.to_bits(), Ordering::Relaxed);
567 set_timeout_and_notify(self.channel(), "setDefaultTimeoutNoReply", timeout).await;
568 }
569
570 /// Sets the default timeout for navigation operations on this page.
571 ///
572 /// The timeout applies to navigation actions such as `goto`, `reload`,
573 /// `go_back`, and `go_forward`. Pass `0` to disable timeouts.
574 ///
575 /// # Arguments
576 ///
577 /// * `timeout` - Timeout in milliseconds
578 ///
579 /// See: <https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout>
580 pub async fn set_default_navigation_timeout(&self, timeout: f64) {
581 self.default_navigation_timeout_ms
582 .store(timeout.to_bits(), Ordering::Relaxed);
583 set_timeout_and_notify(
584 self.channel(),
585 "setDefaultNavigationTimeoutNoReply",
586 timeout,
587 )
588 .await;
589 }
590
591 /// Returns the current default action timeout in milliseconds.
592 pub fn default_timeout_ms(&self) -> f64 {
593 f64::from_bits(self.default_timeout_ms.load(Ordering::Relaxed))
594 }
595
596 /// Returns the current default navigation timeout in milliseconds.
597 pub fn default_navigation_timeout_ms(&self) -> f64 {
598 f64::from_bits(self.default_navigation_timeout_ms.load(Ordering::Relaxed))
599 }
600
601 /// Returns GotoOptions with the navigation timeout filled in if not already set.
602 ///
603 /// Used internally to ensure the page's configured default navigation timeout
604 /// is used when the caller does not provide an explicit timeout.
605 fn with_navigation_timeout(&self, options: Option<GotoOptions>) -> GotoOptions {
606 let nav_timeout = self.default_navigation_timeout_ms();
607 match options {
608 Some(opts) if opts.timeout.is_some() => opts,
609 Some(mut opts) => {
610 opts.timeout = Some(std::time::Duration::from_millis(nav_timeout as u64));
611 opts
612 }
613 None => GotoOptions {
614 timeout: Some(std::time::Duration::from_millis(nav_timeout as u64)),
615 wait_until: None,
616 },
617 }
618 }
619
620 /// Returns all frames in the page, including the main frame.
621 ///
622 /// Currently returns only the main (top-level) frame. Iframe enumeration
623 /// is not yet implemented and will be added in a future release.
624 ///
625 /// # Errors
626 ///
627 /// Returns error if:
628 /// - Page has been closed
629 /// - Communication with browser process fails
630 ///
631 /// See: <https://playwright.dev/docs/api/class-page#page-frames>
632 pub async fn frames(&self) -> Result<Vec<crate::protocol::Frame>> {
633 // Start with the main frame
634 let main = self.main_frame().await?;
635 Ok(vec![main])
636 }
637
638 /// Navigates to the specified URL.
639 ///
640 /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
641 /// about:blank). This matches Playwright's behavior across all language bindings.
642 ///
643 /// # Arguments
644 ///
645 /// * `url` - The URL to navigate to
646 /// * `options` - Optional navigation options (timeout, wait_until)
647 ///
648 /// # Errors
649 ///
650 /// Returns error if:
651 /// - URL is invalid
652 /// - Navigation timeout (default 30s)
653 /// - Network error
654 ///
655 /// See: <https://playwright.dev/docs/api/class-page#page-goto>
656 pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
657 // Inject the page-level navigation timeout when no explicit timeout is given
658 let options = self.with_navigation_timeout(options);
659
660 // Delegate to main frame
661 let frame = self.main_frame().await.map_err(|e| match e {
662 Error::TargetClosed { context, .. } => Error::TargetClosed {
663 target_type: "Page".to_string(),
664 context,
665 },
666 other => other,
667 })?;
668
669 let response = frame.goto(url, Some(options)).await.map_err(|e| match e {
670 Error::TargetClosed { context, .. } => Error::TargetClosed {
671 target_type: "Page".to_string(),
672 context,
673 },
674 other => other,
675 })?;
676
677 // Update the page's URL if we got a response
678 if let Some(ref resp) = response
679 && let Ok(mut page_url) = self.url.write()
680 {
681 *page_url = resp.url().to_string();
682 }
683
684 Ok(response)
685 }
686
687 /// Returns the browser context that the page belongs to.
688 pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
689 downcast_parent::<crate::protocol::BrowserContext>(self)
690 .ok_or_else(|| Error::ProtocolError("Page parent is not a BrowserContext".to_string()))
691 }
692
693 /// Pauses script execution.
694 ///
695 /// Playwright will stop executing the script and wait for the user to either press
696 /// "Resume" in the page overlay or in the debugger.
697 ///
698 /// See: <https://playwright.dev/docs/api/class-page#page-pause>
699 pub async fn pause(&self) -> Result<()> {
700 self.context()?.pause().await
701 }
702
703 /// Returns the page's title.
704 ///
705 /// See: <https://playwright.dev/docs/api/class-page#page-title>
706 pub async fn title(&self) -> Result<String> {
707 // Delegate to main frame
708 let frame = self.main_frame().await?;
709 frame.title().await
710 }
711
712 /// Returns the full HTML content of the page, including the DOCTYPE.
713 ///
714 /// This method retrieves the complete HTML markup of the page,
715 /// including the doctype declaration and all DOM elements.
716 ///
717 /// See: <https://playwright.dev/docs/api/class-page#page-content>
718 pub async fn content(&self) -> Result<String> {
719 // Delegate to main frame
720 let frame = self.main_frame().await?;
721 frame.content().await
722 }
723
724 /// Sets the content of the page.
725 ///
726 /// See: <https://playwright.dev/docs/api/class-page#page-set-content>
727 pub async fn set_content(&self, html: &str, options: Option<GotoOptions>) -> Result<()> {
728 let frame = self.main_frame().await?;
729 frame.set_content(html, options).await
730 }
731
732 /// Waits for the required load state to be reached.
733 ///
734 /// This resolves when the page reaches a required load state, `load` by default.
735 /// The navigation must have been committed when this method is called. If the current
736 /// document has already reached the required state, resolves immediately.
737 ///
738 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-load-state>
739 pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
740 let frame = self.main_frame().await?;
741 frame.wait_for_load_state(state).await
742 }
743
744 /// Waits for the main frame to navigate to a URL matching the given string or glob pattern.
745 ///
746 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-url>
747 pub async fn wait_for_url(&self, url: &str, options: Option<GotoOptions>) -> Result<()> {
748 let frame = self.main_frame().await?;
749 frame.wait_for_url(url, options).await
750 }
751
752 /// Creates a locator for finding elements on the page.
753 ///
754 /// Locators are the central piece of Playwright's auto-waiting and retry-ability.
755 /// They don't execute queries until an action is performed.
756 ///
757 /// # Arguments
758 ///
759 /// * `selector` - CSS selector or other locating strategy
760 ///
761 /// See: <https://playwright.dev/docs/api/class-page#page-locator>
762 pub async fn locator(&self, selector: &str) -> crate::protocol::Locator {
763 // Get the main frame
764 let frame = self.main_frame().await.expect("Main frame should exist");
765
766 crate::protocol::Locator::new(Arc::new(frame), selector.to_string(), self.clone())
767 }
768
769 /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) for an iframe on this page.
770 ///
771 /// The `selector` identifies the iframe element (e.g., `"iframe[name='content']"`).
772 ///
773 /// See: <https://playwright.dev/docs/api/class-page#page-frame-locator>
774 pub async fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
775 let frame = self.main_frame().await.expect("Main frame should exist");
776 crate::protocol::FrameLocator::new(Arc::new(frame), selector.to_string(), self.clone())
777 }
778
779 /// Returns a locator that matches elements containing the given text.
780 ///
781 /// By default, matching is case-insensitive and searches for a substring.
782 /// Set `exact` to `true` for case-sensitive exact matching.
783 ///
784 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-text>
785 pub async fn get_by_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
786 self.locator(&crate::protocol::locator::get_by_text_selector(text, exact))
787 .await
788 }
789
790 /// Returns a locator that matches elements by their associated label text.
791 ///
792 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-label>
793 pub async fn get_by_label(&self, text: &str, exact: bool) -> crate::protocol::Locator {
794 self.locator(&crate::protocol::locator::get_by_label_selector(
795 text, exact,
796 ))
797 .await
798 }
799
800 /// Returns a locator that matches elements by their placeholder text.
801 ///
802 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-placeholder>
803 pub async fn get_by_placeholder(&self, text: &str, exact: bool) -> crate::protocol::Locator {
804 self.locator(&crate::protocol::locator::get_by_placeholder_selector(
805 text, exact,
806 ))
807 .await
808 }
809
810 /// Returns a locator that matches elements by their alt text.
811 ///
812 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-alt-text>
813 pub async fn get_by_alt_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
814 self.locator(&crate::protocol::locator::get_by_alt_text_selector(
815 text, exact,
816 ))
817 .await
818 }
819
820 /// Returns a locator that matches elements by their title attribute.
821 ///
822 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-title>
823 pub async fn get_by_title(&self, text: &str, exact: bool) -> crate::protocol::Locator {
824 self.locator(&crate::protocol::locator::get_by_title_selector(
825 text, exact,
826 ))
827 .await
828 }
829
830 /// Returns a locator that matches elements by their test ID attribute.
831 ///
832 /// By default, uses the `data-testid` attribute. Call
833 /// [`playwright.selectors().set_test_id_attribute()`](crate::protocol::Selectors::set_test_id_attribute)
834 /// to change the attribute name.
835 ///
836 /// Always uses exact matching (case-sensitive).
837 ///
838 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-test-id>
839 pub async fn get_by_test_id(&self, test_id: &str) -> crate::protocol::Locator {
840 let attr = self.connection().selectors().test_id_attribute();
841 self.locator(&crate::protocol::locator::get_by_test_id_selector_with_attr(test_id, &attr))
842 .await
843 }
844
845 /// Returns a locator that matches elements by their ARIA role.
846 ///
847 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
848 pub async fn get_by_role(
849 &self,
850 role: crate::protocol::locator::AriaRole,
851 options: Option<crate::protocol::locator::GetByRoleOptions>,
852 ) -> crate::protocol::Locator {
853 self.locator(&crate::protocol::locator::get_by_role_selector(
854 role, options,
855 ))
856 .await
857 }
858
859 /// Returns the keyboard instance for low-level keyboard control.
860 ///
861 /// See: <https://playwright.dev/docs/api/class-page#page-keyboard>
862 pub fn keyboard(&self) -> crate::protocol::Keyboard {
863 crate::protocol::Keyboard::new(self.clone())
864 }
865
866 /// Returns the mouse instance for low-level mouse control.
867 ///
868 /// See: <https://playwright.dev/docs/api/class-page#page-mouse>
869 pub fn mouse(&self) -> crate::protocol::Mouse {
870 crate::protocol::Mouse::new(self.clone())
871 }
872
873 // Internal keyboard methods (called by Keyboard struct)
874
875 pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
876 self.channel()
877 .send_no_result(
878 "keyboardDown",
879 serde_json::json!({
880 "key": key
881 }),
882 )
883 .await
884 }
885
886 pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
887 self.channel()
888 .send_no_result(
889 "keyboardUp",
890 serde_json::json!({
891 "key": key
892 }),
893 )
894 .await
895 }
896
897 pub(crate) async fn keyboard_press(
898 &self,
899 key: &str,
900 options: Option<crate::protocol::KeyboardOptions>,
901 ) -> Result<()> {
902 let mut params = serde_json::json!({
903 "key": key
904 });
905
906 if let Some(opts) = options {
907 let opts_json = opts.to_json();
908 if let Some(obj) = params.as_object_mut()
909 && let Some(opts_obj) = opts_json.as_object()
910 {
911 obj.extend(opts_obj.clone());
912 }
913 }
914
915 self.channel().send_no_result("keyboardPress", params).await
916 }
917
918 pub(crate) async fn keyboard_type(
919 &self,
920 text: &str,
921 options: Option<crate::protocol::KeyboardOptions>,
922 ) -> Result<()> {
923 let mut params = serde_json::json!({
924 "text": text
925 });
926
927 if let Some(opts) = options {
928 let opts_json = opts.to_json();
929 if let Some(obj) = params.as_object_mut()
930 && let Some(opts_obj) = opts_json.as_object()
931 {
932 obj.extend(opts_obj.clone());
933 }
934 }
935
936 self.channel().send_no_result("keyboardType", params).await
937 }
938
939 pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
940 self.channel()
941 .send_no_result(
942 "keyboardInsertText",
943 serde_json::json!({
944 "text": text
945 }),
946 )
947 .await
948 }
949
950 // Internal mouse methods (called by Mouse struct)
951
952 pub(crate) async fn mouse_move(
953 &self,
954 x: i32,
955 y: i32,
956 options: Option<crate::protocol::MouseOptions>,
957 ) -> Result<()> {
958 let mut params = serde_json::json!({
959 "x": x,
960 "y": y
961 });
962
963 if let Some(opts) = options {
964 let opts_json = opts.to_json();
965 if let Some(obj) = params.as_object_mut()
966 && let Some(opts_obj) = opts_json.as_object()
967 {
968 obj.extend(opts_obj.clone());
969 }
970 }
971
972 self.channel().send_no_result("mouseMove", params).await
973 }
974
975 pub(crate) async fn mouse_click(
976 &self,
977 x: i32,
978 y: i32,
979 options: Option<crate::protocol::MouseOptions>,
980 ) -> Result<()> {
981 let mut params = serde_json::json!({
982 "x": x,
983 "y": y
984 });
985
986 if let Some(opts) = options {
987 let opts_json = opts.to_json();
988 if let Some(obj) = params.as_object_mut()
989 && let Some(opts_obj) = opts_json.as_object()
990 {
991 obj.extend(opts_obj.clone());
992 }
993 }
994
995 self.channel().send_no_result("mouseClick", params).await
996 }
997
998 pub(crate) async fn mouse_dblclick(
999 &self,
1000 x: i32,
1001 y: i32,
1002 options: Option<crate::protocol::MouseOptions>,
1003 ) -> Result<()> {
1004 let mut params = serde_json::json!({
1005 "x": x,
1006 "y": y,
1007 "clickCount": 2
1008 });
1009
1010 if let Some(opts) = options {
1011 let opts_json = opts.to_json();
1012 if let Some(obj) = params.as_object_mut()
1013 && let Some(opts_obj) = opts_json.as_object()
1014 {
1015 obj.extend(opts_obj.clone());
1016 }
1017 }
1018
1019 self.channel().send_no_result("mouseClick", params).await
1020 }
1021
1022 pub(crate) async fn mouse_down(
1023 &self,
1024 options: Option<crate::protocol::MouseOptions>,
1025 ) -> Result<()> {
1026 let mut params = serde_json::json!({});
1027
1028 if let Some(opts) = options {
1029 let opts_json = opts.to_json();
1030 if let Some(obj) = params.as_object_mut()
1031 && let Some(opts_obj) = opts_json.as_object()
1032 {
1033 obj.extend(opts_obj.clone());
1034 }
1035 }
1036
1037 self.channel().send_no_result("mouseDown", params).await
1038 }
1039
1040 pub(crate) async fn mouse_up(
1041 &self,
1042 options: Option<crate::protocol::MouseOptions>,
1043 ) -> Result<()> {
1044 let mut params = serde_json::json!({});
1045
1046 if let Some(opts) = options {
1047 let opts_json = opts.to_json();
1048 if let Some(obj) = params.as_object_mut()
1049 && let Some(opts_obj) = opts_json.as_object()
1050 {
1051 obj.extend(opts_obj.clone());
1052 }
1053 }
1054
1055 self.channel().send_no_result("mouseUp", params).await
1056 }
1057
1058 pub(crate) async fn mouse_wheel(&self, delta_x: i32, delta_y: i32) -> Result<()> {
1059 self.channel()
1060 .send_no_result(
1061 "mouseWheel",
1062 serde_json::json!({
1063 "deltaX": delta_x,
1064 "deltaY": delta_y
1065 }),
1066 )
1067 .await
1068 }
1069
1070 /// Reloads the current page.
1071 ///
1072 /// # Arguments
1073 ///
1074 /// * `options` - Optional reload options (timeout, wait_until)
1075 ///
1076 /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
1077 /// about:blank). This matches Playwright's behavior across all language bindings.
1078 ///
1079 /// See: <https://playwright.dev/docs/api/class-page#page-reload>
1080 pub async fn reload(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1081 self.navigate_history("reload", options).await
1082 }
1083
1084 /// Navigates to the previous page in history.
1085 ///
1086 /// Returns the main resource response. In case of multiple server redirects, the navigation
1087 /// will resolve with the response of the last redirect. If can not go back, returns `None`.
1088 ///
1089 /// See: <https://playwright.dev/docs/api/class-page#page-go-back>
1090 pub async fn go_back(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1091 self.navigate_history("goBack", options).await
1092 }
1093
1094 /// Navigates to the next page in history.
1095 ///
1096 /// Returns the main resource response. In case of multiple server redirects, the navigation
1097 /// will resolve with the response of the last redirect. If can not go forward, returns `None`.
1098 ///
1099 /// See: <https://playwright.dev/docs/api/class-page#page-go-forward>
1100 pub async fn go_forward(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1101 self.navigate_history("goForward", options).await
1102 }
1103
1104 /// Shared implementation for reload, go_back and go_forward.
1105 async fn navigate_history(
1106 &self,
1107 method: &str,
1108 options: Option<GotoOptions>,
1109 ) -> Result<Option<Response>> {
1110 // Inject the page-level navigation timeout when no explicit timeout is given
1111 let opts = self.with_navigation_timeout(options);
1112 let mut params = serde_json::json!({});
1113
1114 // opts.timeout is always Some(...) because with_navigation_timeout guarantees it
1115 if let Some(timeout) = opts.timeout {
1116 params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
1117 } else {
1118 params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1119 }
1120 if let Some(wait_until) = opts.wait_until {
1121 params["waitUntil"] = serde_json::json!(wait_until.as_str());
1122 }
1123
1124 #[derive(Deserialize)]
1125 struct NavigationResponse {
1126 response: Option<ResponseReference>,
1127 }
1128
1129 #[derive(Deserialize)]
1130 struct ResponseReference {
1131 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
1132 guid: Arc<str>,
1133 }
1134
1135 let result: NavigationResponse = self.channel().send(method, params).await?;
1136
1137 if let Some(response_ref) = result.response {
1138 let response_arc = {
1139 let mut attempts = 0;
1140 let max_attempts = 20;
1141 loop {
1142 match self.connection().get_object(&response_ref.guid).await {
1143 Ok(obj) => break obj,
1144 Err(_) if attempts < max_attempts => {
1145 attempts += 1;
1146 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1147 }
1148 Err(e) => return Err(e),
1149 }
1150 }
1151 };
1152
1153 let initializer = response_arc.initializer();
1154
1155 let status = initializer["status"].as_u64().ok_or_else(|| {
1156 crate::error::Error::ProtocolError("Response missing status".to_string())
1157 })? as u16;
1158
1159 let headers = initializer["headers"]
1160 .as_array()
1161 .ok_or_else(|| {
1162 crate::error::Error::ProtocolError("Response missing headers".to_string())
1163 })?
1164 .iter()
1165 .filter_map(|h| {
1166 let name = h["name"].as_str()?;
1167 let value = h["value"].as_str()?;
1168 Some((name.to_string(), value.to_string()))
1169 })
1170 .collect();
1171
1172 let response = Response::new(
1173 initializer["url"]
1174 .as_str()
1175 .ok_or_else(|| {
1176 crate::error::Error::ProtocolError("Response missing url".to_string())
1177 })?
1178 .to_string(),
1179 status,
1180 initializer["statusText"].as_str().unwrap_or("").to_string(),
1181 headers,
1182 Some(response_arc),
1183 );
1184
1185 if let Ok(mut page_url) = self.url.write() {
1186 *page_url = response.url().to_string();
1187 }
1188
1189 Ok(Some(response))
1190 } else {
1191 Ok(None)
1192 }
1193 }
1194
1195 /// Returns the first element matching the selector, or None if not found.
1196 ///
1197 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
1198 pub async fn query_selector(
1199 &self,
1200 selector: &str,
1201 ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
1202 let frame = self.main_frame().await?;
1203 frame.query_selector(selector).await
1204 }
1205
1206 /// Returns all elements matching the selector.
1207 ///
1208 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
1209 pub async fn query_selector_all(
1210 &self,
1211 selector: &str,
1212 ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
1213 let frame = self.main_frame().await?;
1214 frame.query_selector_all(selector).await
1215 }
1216
1217 /// Takes a screenshot of the page and returns the image bytes.
1218 ///
1219 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1220 pub async fn screenshot(
1221 &self,
1222 options: Option<crate::protocol::ScreenshotOptions>,
1223 ) -> Result<Vec<u8>> {
1224 let params = if let Some(opts) = options {
1225 opts.to_json()
1226 } else {
1227 // Default to PNG with required timeout
1228 serde_json::json!({
1229 "type": "png",
1230 "timeout": crate::DEFAULT_TIMEOUT_MS
1231 })
1232 };
1233
1234 #[derive(Deserialize)]
1235 struct ScreenshotResponse {
1236 binary: String,
1237 }
1238
1239 let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
1240
1241 // Decode base64 to bytes
1242 let bytes = base64::prelude::BASE64_STANDARD
1243 .decode(&response.binary)
1244 .map_err(|e| {
1245 crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
1246 })?;
1247
1248 Ok(bytes)
1249 }
1250
1251 /// Takes a screenshot and saves it to a file, also returning the bytes.
1252 ///
1253 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1254 pub async fn screenshot_to_file(
1255 &self,
1256 path: &std::path::Path,
1257 options: Option<crate::protocol::ScreenshotOptions>,
1258 ) -> Result<Vec<u8>> {
1259 // Get the screenshot bytes
1260 let bytes = self.screenshot(options).await?;
1261
1262 // Write to file
1263 tokio::fs::write(path, &bytes).await.map_err(|e| {
1264 crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
1265 })?;
1266
1267 Ok(bytes)
1268 }
1269
1270 /// Evaluates JavaScript in the page context (without return value).
1271 ///
1272 /// Executes the provided JavaScript expression or function within the page's
1273 /// context without returning a value.
1274 ///
1275 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1276 pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
1277 // Delegate to the main frame
1278 let frame = self.main_frame().await?;
1279 frame.frame_evaluate_expression(expression).await
1280 }
1281
1282 /// Evaluates JavaScript in the page context with optional arguments.
1283 ///
1284 /// Executes the provided JavaScript expression or function within the page's
1285 /// context and returns the result. The return value must be JSON-serializable.
1286 ///
1287 /// # Arguments
1288 ///
1289 /// * `expression` - JavaScript code to evaluate
1290 /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1291 ///
1292 /// # Returns
1293 ///
1294 /// The result as a `serde_json::Value`
1295 ///
1296 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1297 pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
1298 &self,
1299 expression: &str,
1300 arg: Option<&T>,
1301 ) -> Result<U> {
1302 // Delegate to the main frame
1303 let frame = self.main_frame().await?;
1304 let result = frame.evaluate(expression, arg).await?;
1305 serde_json::from_value(result).map_err(Error::from)
1306 }
1307
1308 /// Evaluates a JavaScript expression and returns the result as a String.
1309 ///
1310 /// # Arguments
1311 ///
1312 /// * `expression` - JavaScript code to evaluate
1313 ///
1314 /// # Returns
1315 ///
1316 /// The result converted to a String
1317 ///
1318 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1319 pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
1320 let frame = self.main_frame().await?;
1321 frame.frame_evaluate_expression_value(expression).await
1322 }
1323
1324 /// Registers a route handler for network interception.
1325 ///
1326 /// When a request matches the specified pattern, the handler will be called
1327 /// with a Route object that can abort, continue, or fulfill the request.
1328 ///
1329 /// # Arguments
1330 ///
1331 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
1332 /// * `handler` - Async closure that handles the route
1333 ///
1334 /// See: <https://playwright.dev/docs/api/class-page#page-route>
1335 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1336 where
1337 F: Fn(Route) -> Fut + Send + Sync + 'static,
1338 Fut: Future<Output = Result<()>> + Send + 'static,
1339 {
1340 // 1. Wrap handler in Arc with type erasure
1341 let handler =
1342 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1343
1344 // 2. Store in handlers list
1345 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1346 pattern: pattern.to_string(),
1347 handler,
1348 });
1349
1350 // 3. Enable network interception via protocol
1351 self.enable_network_interception().await?;
1352
1353 Ok(())
1354 }
1355
1356 /// Updates network interception patterns for this page
1357 async fn enable_network_interception(&self) -> Result<()> {
1358 // Collect all patterns from registered handlers
1359 // Each pattern must be an object with "glob" field
1360 let patterns: Vec<serde_json::Value> = self
1361 .route_handlers
1362 .lock()
1363 .unwrap()
1364 .iter()
1365 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1366 .collect();
1367
1368 // Send protocol command to update network interception patterns
1369 // Follows playwright-python's approach
1370 self.channel()
1371 .send_no_result(
1372 "setNetworkInterceptionPatterns",
1373 serde_json::json!({
1374 "patterns": patterns
1375 }),
1376 )
1377 .await
1378 }
1379
1380 /// Removes route handler(s) matching the given URL pattern.
1381 ///
1382 /// # Arguments
1383 ///
1384 /// * `pattern` - URL pattern to remove handlers for
1385 ///
1386 /// See: <https://playwright.dev/docs/api/class-page#page-unroute>
1387 pub async fn unroute(&self, pattern: &str) -> Result<()> {
1388 self.route_handlers
1389 .lock()
1390 .unwrap()
1391 .retain(|entry| entry.pattern != pattern);
1392 self.enable_network_interception().await
1393 }
1394
1395 /// Removes all registered route handlers.
1396 ///
1397 /// # Arguments
1398 ///
1399 /// * `behavior` - Optional behavior for in-flight handlers
1400 ///
1401 /// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
1402 pub async fn unroute_all(
1403 &self,
1404 _behavior: Option<crate::protocol::route::UnrouteBehavior>,
1405 ) -> Result<()> {
1406 self.route_handlers.lock().unwrap().clear();
1407 self.enable_network_interception().await
1408 }
1409
1410 /// Handles a route event from the protocol
1411 ///
1412 /// Called by on_event when a "route" event is received.
1413 /// Supports handler chaining via `route.fallback()` — if a handler calls
1414 /// `fallback()` instead of `continue_()`, `abort()`, or `fulfill()`, the
1415 /// next matching handler in the chain is tried.
1416 async fn on_route_event(&self, route: Route) {
1417 let handlers = self.route_handlers.lock().unwrap().clone();
1418 let url = route.request().url().to_string();
1419
1420 // Find matching handler (last registered wins, with fallback chaining)
1421 for entry in handlers.iter().rev() {
1422 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
1423 let handler = entry.handler.clone();
1424 if let Err(e) = handler(route.clone()).await {
1425 tracing::warn!("Route handler error: {}", e);
1426 break;
1427 }
1428 // If handler called fallback(), try the next matching handler
1429 if !route.was_handled() {
1430 continue;
1431 }
1432 break;
1433 }
1434 }
1435 }
1436
1437 /// Registers a download event handler.
1438 ///
1439 /// The handler will be called when a download is triggered by the page.
1440 /// Downloads occur when the page initiates a file download (e.g., clicking a link
1441 /// with the download attribute, or a server response with Content-Disposition: attachment).
1442 ///
1443 /// # Arguments
1444 ///
1445 /// * `handler` - Async closure that receives the Download object
1446 ///
1447 /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
1448 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1449 where
1450 F: Fn(Download) -> Fut + Send + Sync + 'static,
1451 Fut: Future<Output = Result<()>> + Send + 'static,
1452 {
1453 // Wrap handler with type erasure
1454 let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
1455 Box::pin(handler(download))
1456 });
1457
1458 // Store handler
1459 self.download_handlers.lock().unwrap().push(handler);
1460
1461 Ok(())
1462 }
1463
1464 /// Registers a dialog event handler.
1465 ///
1466 /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
1467 /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
1468 ///
1469 /// # Arguments
1470 ///
1471 /// * `handler` - Async closure that receives the Dialog object
1472 ///
1473 /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
1474 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1475 where
1476 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1477 Fut: Future<Output = Result<()>> + Send + 'static,
1478 {
1479 // Wrap handler with type erasure
1480 let handler =
1481 Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
1482
1483 // Store handler
1484 self.dialog_handlers.lock().unwrap().push(handler);
1485
1486 // Dialog events are auto-emitted (no subscription needed)
1487
1488 Ok(())
1489 }
1490
1491 /// Registers a console event handler.
1492 ///
1493 /// The handler is called whenever the page emits a JavaScript console message
1494 /// (e.g. `console.log`, `console.error`, `console.warn`, etc.).
1495 ///
1496 /// The server only sends console events after the first handler is registered
1497 /// (subscription is managed automatically).
1498 ///
1499 /// # Arguments
1500 ///
1501 /// * `handler` - Async closure that receives the [`ConsoleMessage`](crate::protocol::ConsoleMessage)
1502 ///
1503 /// See: <https://playwright.dev/docs/api/class-page#page-event-console>
1504 pub async fn on_console<F, Fut>(&self, handler: F) -> Result<()>
1505 where
1506 F: Fn(crate::protocol::ConsoleMessage) -> Fut + Send + Sync + 'static,
1507 Fut: Future<Output = Result<()>> + Send + 'static,
1508 {
1509 let handler = Arc::new(
1510 move |msg: crate::protocol::ConsoleMessage| -> ConsoleHandlerFuture {
1511 Box::pin(handler(msg))
1512 },
1513 );
1514
1515 let needs_subscription = {
1516 let handlers = self.console_handlers.lock().unwrap();
1517 let waiters = self.console_waiters.lock().unwrap();
1518 handlers.is_empty() && waiters.is_empty()
1519 };
1520 if needs_subscription {
1521 _ = self.channel().update_subscription("console", true).await;
1522 }
1523 self.console_handlers.lock().unwrap().push(handler);
1524
1525 Ok(())
1526 }
1527
1528 /// Registers a handler for file chooser events.
1529 ///
1530 /// The handler is called whenever the page opens a file chooser dialog
1531 /// (e.g. when the user clicks an `<input type="file">` element).
1532 ///
1533 /// Use [`FileChooser::set_files`](crate::protocol::FileChooser::set_files) inside
1534 /// the handler to satisfy the file chooser without OS-level interaction.
1535 ///
1536 /// The server only sends `"fileChooser"` events after the first handler is
1537 /// registered (subscription is managed automatically via `updateSubscription`).
1538 ///
1539 /// # Arguments
1540 ///
1541 /// * `handler` - Async closure that receives a [`FileChooser`](crate::protocol::FileChooser)
1542 ///
1543 /// # Example
1544 ///
1545 /// ```ignore
1546 /// page.on_filechooser(|chooser| async move {
1547 /// chooser.set_files(&[std::path::PathBuf::from("/tmp/file.txt")]).await
1548 /// }).await?;
1549 /// ```
1550 ///
1551 /// See: <https://playwright.dev/docs/api/class-page#page-event-file-chooser>
1552 pub async fn on_filechooser<F, Fut>(&self, handler: F) -> Result<()>
1553 where
1554 F: Fn(crate::protocol::FileChooser) -> Fut + Send + Sync + 'static,
1555 Fut: Future<Output = Result<()>> + Send + 'static,
1556 {
1557 let handler = Arc::new(
1558 move |chooser: crate::protocol::FileChooser| -> FileChooserHandlerFuture {
1559 Box::pin(handler(chooser))
1560 },
1561 );
1562
1563 let needs_subscription = {
1564 let handlers = self.filechooser_handlers.lock().unwrap();
1565 let waiters = self.filechooser_waiters.lock().unwrap();
1566 handlers.is_empty() && waiters.is_empty()
1567 };
1568 if needs_subscription {
1569 _ = self
1570 .channel()
1571 .update_subscription("fileChooser", true)
1572 .await;
1573 }
1574 self.filechooser_handlers.lock().unwrap().push(handler);
1575
1576 Ok(())
1577 }
1578
1579 /// Creates a one-shot waiter that resolves when the next file chooser opens.
1580 ///
1581 /// The waiter **must** be created before the action that triggers the file
1582 /// chooser to avoid a race condition.
1583 ///
1584 /// # Arguments
1585 ///
1586 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1587 ///
1588 /// # Errors
1589 ///
1590 /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if the file chooser
1591 /// does not open within the timeout.
1592 ///
1593 /// # Example
1594 ///
1595 /// ```ignore
1596 /// // Set up waiter BEFORE triggering the file chooser
1597 /// let waiter = page.expect_file_chooser(None).await?;
1598 /// page.locator("input[type=file]").await.click(None).await?;
1599 /// let chooser = waiter.wait().await?;
1600 /// chooser.set_files(&[PathBuf::from("/tmp/file.txt")]).await?;
1601 /// ```
1602 ///
1603 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1604 pub async fn expect_file_chooser(
1605 &self,
1606 timeout: Option<f64>,
1607 ) -> Result<crate::protocol::EventWaiter<crate::protocol::FileChooser>> {
1608 let (tx, rx) = tokio::sync::oneshot::channel();
1609
1610 let needs_subscription = {
1611 let handlers = self.filechooser_handlers.lock().unwrap();
1612 let waiters = self.filechooser_waiters.lock().unwrap();
1613 handlers.is_empty() && waiters.is_empty()
1614 };
1615 if needs_subscription {
1616 _ = self
1617 .channel()
1618 .update_subscription("fileChooser", true)
1619 .await;
1620 }
1621 self.filechooser_waiters.lock().unwrap().push(tx);
1622
1623 Ok(crate::protocol::EventWaiter::new(
1624 rx,
1625 timeout.or(Some(30_000.0)),
1626 ))
1627 }
1628
1629 /// Creates a one-shot waiter that resolves when the next popup window opens.
1630 ///
1631 /// The waiter **must** be created before the action that opens the popup to
1632 /// avoid a race condition.
1633 ///
1634 /// # Arguments
1635 ///
1636 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1637 ///
1638 /// # Errors
1639 ///
1640 /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no popup
1641 /// opens within the timeout.
1642 ///
1643 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1644 pub async fn expect_popup(
1645 &self,
1646 timeout: Option<f64>,
1647 ) -> Result<crate::protocol::EventWaiter<Page>> {
1648 let (tx, rx) = tokio::sync::oneshot::channel();
1649 self.popup_waiters.lock().unwrap().push(tx);
1650 Ok(crate::protocol::EventWaiter::new(
1651 rx,
1652 timeout.or(Some(30_000.0)),
1653 ))
1654 }
1655
1656 /// Creates a one-shot waiter that resolves when the next download starts.
1657 ///
1658 /// The waiter **must** be created before the action that triggers the download
1659 /// to avoid a race condition.
1660 ///
1661 /// # Arguments
1662 ///
1663 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1664 ///
1665 /// # Errors
1666 ///
1667 /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no download
1668 /// starts within the timeout.
1669 ///
1670 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1671 pub async fn expect_download(
1672 &self,
1673 timeout: Option<f64>,
1674 ) -> Result<crate::protocol::EventWaiter<Download>> {
1675 let (tx, rx) = tokio::sync::oneshot::channel();
1676 self.download_waiters.lock().unwrap().push(tx);
1677 Ok(crate::protocol::EventWaiter::new(
1678 rx,
1679 timeout.or(Some(30_000.0)),
1680 ))
1681 }
1682
1683 /// Creates a one-shot waiter that resolves when the next network response is received.
1684 ///
1685 /// The waiter **must** be created before the action that triggers the response
1686 /// to avoid a race condition.
1687 ///
1688 /// # Arguments
1689 ///
1690 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1691 ///
1692 /// # Errors
1693 ///
1694 /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no response
1695 /// arrives within the timeout.
1696 ///
1697 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1698 pub async fn expect_response(
1699 &self,
1700 timeout: Option<f64>,
1701 ) -> Result<crate::protocol::EventWaiter<ResponseObject>> {
1702 let (tx, rx) = tokio::sync::oneshot::channel();
1703
1704 let needs_subscription = {
1705 let handlers = self.response_handlers.lock().unwrap();
1706 let waiters = self.response_waiters.lock().unwrap();
1707 handlers.is_empty() && waiters.is_empty()
1708 };
1709 if needs_subscription {
1710 _ = self.channel().update_subscription("response", true).await;
1711 }
1712 self.response_waiters.lock().unwrap().push(tx);
1713
1714 Ok(crate::protocol::EventWaiter::new(
1715 rx,
1716 timeout.or(Some(30_000.0)),
1717 ))
1718 }
1719
1720 /// Creates a one-shot waiter that resolves when the next network request is issued.
1721 ///
1722 /// The waiter **must** be created before the action that issues the request
1723 /// to avoid a race condition.
1724 ///
1725 /// # Arguments
1726 ///
1727 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1728 ///
1729 /// # Errors
1730 ///
1731 /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no request
1732 /// is issued within the timeout.
1733 ///
1734 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1735 pub async fn expect_request(
1736 &self,
1737 timeout: Option<f64>,
1738 ) -> Result<crate::protocol::EventWaiter<Request>> {
1739 let (tx, rx) = tokio::sync::oneshot::channel();
1740
1741 let needs_subscription = {
1742 let handlers = self.request_handlers.lock().unwrap();
1743 let waiters = self.request_waiters.lock().unwrap();
1744 handlers.is_empty() && waiters.is_empty()
1745 };
1746 if needs_subscription {
1747 _ = self.channel().update_subscription("request", true).await;
1748 }
1749 self.request_waiters.lock().unwrap().push(tx);
1750
1751 Ok(crate::protocol::EventWaiter::new(
1752 rx,
1753 timeout.or(Some(30_000.0)),
1754 ))
1755 }
1756
1757 /// Creates a one-shot waiter that resolves when the next console message is produced.
1758 ///
1759 /// The waiter **must** be created before the action that produces the console
1760 /// message to avoid a race condition.
1761 ///
1762 /// # Arguments
1763 ///
1764 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1765 ///
1766 /// # Errors
1767 ///
1768 /// Returns [`Error::Timeout`](crate::error::Error::Timeout) if no console
1769 /// message is produced within the timeout.
1770 ///
1771 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
1772 pub async fn expect_console_message(
1773 &self,
1774 timeout: Option<f64>,
1775 ) -> Result<crate::protocol::EventWaiter<crate::protocol::ConsoleMessage>> {
1776 let (tx, rx) = tokio::sync::oneshot::channel();
1777
1778 let needs_subscription = {
1779 let handlers = self.console_handlers.lock().unwrap();
1780 let waiters = self.console_waiters.lock().unwrap();
1781 handlers.is_empty() && waiters.is_empty()
1782 };
1783 if needs_subscription {
1784 _ = self.channel().update_subscription("console", true).await;
1785 }
1786 self.console_waiters.lock().unwrap().push(tx);
1787
1788 Ok(crate::protocol::EventWaiter::new(
1789 rx,
1790 timeout.or(Some(30_000.0)),
1791 ))
1792 }
1793
1794 /// See: <https://playwright.dev/docs/api/class-page#page-event-request>
1795 pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
1796 where
1797 F: Fn(Request) -> Fut + Send + Sync + 'static,
1798 Fut: Future<Output = Result<()>> + Send + 'static,
1799 {
1800 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1801 Box::pin(handler(request))
1802 });
1803
1804 let needs_subscription = {
1805 let handlers = self.request_handlers.lock().unwrap();
1806 let waiters = self.request_waiters.lock().unwrap();
1807 handlers.is_empty() && waiters.is_empty()
1808 };
1809 if needs_subscription {
1810 _ = self.channel().update_subscription("request", true).await;
1811 }
1812 self.request_handlers.lock().unwrap().push(handler);
1813
1814 Ok(())
1815 }
1816
1817 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-finished>
1818 pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
1819 where
1820 F: Fn(Request) -> Fut + Send + Sync + 'static,
1821 Fut: Future<Output = Result<()>> + Send + 'static,
1822 {
1823 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1824 Box::pin(handler(request))
1825 });
1826
1827 let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
1828 if needs_subscription {
1829 _ = self
1830 .channel()
1831 .update_subscription("requestFinished", true)
1832 .await;
1833 }
1834 self.request_finished_handlers.lock().unwrap().push(handler);
1835
1836 Ok(())
1837 }
1838
1839 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-failed>
1840 pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
1841 where
1842 F: Fn(Request) -> Fut + Send + Sync + 'static,
1843 Fut: Future<Output = Result<()>> + Send + 'static,
1844 {
1845 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1846 Box::pin(handler(request))
1847 });
1848
1849 let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
1850 if needs_subscription {
1851 _ = self
1852 .channel()
1853 .update_subscription("requestFailed", true)
1854 .await;
1855 }
1856 self.request_failed_handlers.lock().unwrap().push(handler);
1857
1858 Ok(())
1859 }
1860
1861 /// See: <https://playwright.dev/docs/api/class-page#page-event-response>
1862 pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
1863 where
1864 F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
1865 Fut: Future<Output = Result<()>> + Send + 'static,
1866 {
1867 let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
1868 Box::pin(handler(response))
1869 });
1870
1871 let needs_subscription = {
1872 let handlers = self.response_handlers.lock().unwrap();
1873 let waiters = self.response_waiters.lock().unwrap();
1874 handlers.is_empty() && waiters.is_empty()
1875 };
1876 if needs_subscription {
1877 _ = self.channel().update_subscription("response", true).await;
1878 }
1879 self.response_handlers.lock().unwrap().push(handler);
1880
1881 Ok(())
1882 }
1883
1884 /// Adds a listener for the `websocket` event.
1885 ///
1886 /// The handler will be called when a WebSocket request is dispatched.
1887 ///
1888 /// # Arguments
1889 ///
1890 /// * `handler` - The function to call when the event occurs
1891 ///
1892 /// See: <https://playwright.dev/docs/api/class-page#page-on-websocket>
1893 pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
1894 where
1895 F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1896 Fut: Future<Output = Result<()>> + Send + 'static,
1897 {
1898 let handler =
1899 Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
1900 self.websocket_handlers.lock().unwrap().push(handler);
1901 Ok(())
1902 }
1903
1904 /// Registers a handler for the `worker` event.
1905 ///
1906 /// The handler is called when a new Web Worker is created in the page.
1907 ///
1908 /// # Arguments
1909 ///
1910 /// * `handler` - Async closure called with the new [`Worker`] object
1911 ///
1912 /// See: <https://playwright.dev/docs/api/class-page#page-event-worker>
1913 pub async fn on_worker<F, Fut>(&self, handler: F) -> Result<()>
1914 where
1915 F: Fn(Worker) -> Fut + Send + Sync + 'static,
1916 Fut: Future<Output = Result<()>> + Send + 'static,
1917 {
1918 let handler = Arc::new(move |w: Worker| -> WorkerHandlerFuture { Box::pin(handler(w)) });
1919 self.worker_handlers.lock().unwrap().push(handler);
1920 Ok(())
1921 }
1922
1923 /// Registers a handler for the `close` event.
1924 ///
1925 /// The handler is called when the page is closed, either by calling `page.close()`,
1926 /// by the browser context being closed, or when the browser process exits.
1927 ///
1928 /// # Arguments
1929 ///
1930 /// * `handler` - Async closure called with no arguments when the page closes
1931 ///
1932 /// See: <https://playwright.dev/docs/api/class-page#page-event-close>
1933 pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
1934 where
1935 F: Fn() -> Fut + Send + Sync + 'static,
1936 Fut: Future<Output = Result<()>> + Send + 'static,
1937 {
1938 let handler = Arc::new(move || -> CloseHandlerFuture { Box::pin(handler()) });
1939 self.close_handlers.lock().unwrap().push(handler);
1940 Ok(())
1941 }
1942
1943 /// Registers a handler for the `load` event.
1944 ///
1945 /// The handler is called when the page's `load` event fires, i.e. after
1946 /// all resources including stylesheets and images have finished loading.
1947 ///
1948 /// The server only sends `"load"` events after the first handler is registered
1949 /// (subscription is managed automatically).
1950 ///
1951 /// # Arguments
1952 ///
1953 /// * `handler` - Async closure called with no arguments when the page loads
1954 ///
1955 /// See: <https://playwright.dev/docs/api/class-page#page-event-load>
1956 pub async fn on_load<F, Fut>(&self, handler: F) -> Result<()>
1957 where
1958 F: Fn() -> Fut + Send + Sync + 'static,
1959 Fut: Future<Output = Result<()>> + Send + 'static,
1960 {
1961 let handler = Arc::new(move || -> LoadHandlerFuture { Box::pin(handler()) });
1962 // "load" events come via Frame's "loadstate" event, no subscription needed.
1963 self.load_handlers.lock().unwrap().push(handler);
1964 Ok(())
1965 }
1966
1967 /// Registers a handler for the `crash` event.
1968 ///
1969 /// The handler is called when the page crashes (e.g. runs out of memory).
1970 ///
1971 /// # Arguments
1972 ///
1973 /// * `handler` - Async closure called with no arguments when the page crashes
1974 ///
1975 /// See: <https://playwright.dev/docs/api/class-page#page-event-crash>
1976 pub async fn on_crash<F, Fut>(&self, handler: F) -> Result<()>
1977 where
1978 F: Fn() -> Fut + Send + Sync + 'static,
1979 Fut: Future<Output = Result<()>> + Send + 'static,
1980 {
1981 let handler = Arc::new(move || -> CrashHandlerFuture { Box::pin(handler()) });
1982 self.crash_handlers.lock().unwrap().push(handler);
1983 Ok(())
1984 }
1985
1986 /// Registers a handler for the `pageError` event.
1987 ///
1988 /// The handler is called when an uncaught JavaScript exception is thrown in the page.
1989 /// The handler receives the error message as a `String`.
1990 ///
1991 /// The server only sends `"pageError"` events after the first handler is registered
1992 /// (subscription is managed automatically).
1993 ///
1994 /// # Arguments
1995 ///
1996 /// * `handler` - Async closure that receives the error message string
1997 ///
1998 /// See: <https://playwright.dev/docs/api/class-page#page-event-page-error>
1999 pub async fn on_pageerror<F, Fut>(&self, handler: F) -> Result<()>
2000 where
2001 F: Fn(String) -> Fut + Send + Sync + 'static,
2002 Fut: Future<Output = Result<()>> + Send + 'static,
2003 {
2004 let handler =
2005 Arc::new(move |msg: String| -> PageErrorHandlerFuture { Box::pin(handler(msg)) });
2006 // "pageError" events come via BrowserContext, no subscription needed.
2007 self.pageerror_handlers.lock().unwrap().push(handler);
2008 Ok(())
2009 }
2010
2011 /// Registers a handler for the `popup` event.
2012 ///
2013 /// The handler is called when the page opens a popup window (e.g. via `window.open()`).
2014 /// The handler receives the new popup [`Page`] object.
2015 ///
2016 /// The server only sends `"popup"` events after the first handler is registered
2017 /// (subscription is managed automatically).
2018 ///
2019 /// # Arguments
2020 ///
2021 /// * `handler` - Async closure that receives the popup Page
2022 ///
2023 /// See: <https://playwright.dev/docs/api/class-page#page-event-popup>
2024 pub async fn on_popup<F, Fut>(&self, handler: F) -> Result<()>
2025 where
2026 F: Fn(Page) -> Fut + Send + Sync + 'static,
2027 Fut: Future<Output = Result<()>> + Send + 'static,
2028 {
2029 let handler = Arc::new(move |page: Page| -> PopupHandlerFuture { Box::pin(handler(page)) });
2030 // "popup" events arrive via BrowserContext's "page" event when a page has an opener.
2031 self.popup_handlers.lock().unwrap().push(handler);
2032 Ok(())
2033 }
2034
2035 /// Registers a handler for the `frameAttached` event.
2036 ///
2037 /// The handler is called when a new frame (iframe) is attached to the page.
2038 /// The handler receives the attached [`Frame`](crate::protocol::Frame) object.
2039 ///
2040 /// # Arguments
2041 ///
2042 /// * `handler` - Async closure that receives the attached Frame
2043 ///
2044 /// See: <https://playwright.dev/docs/api/class-page#page-event-frameattached>
2045 pub async fn on_frameattached<F, Fut>(&self, handler: F) -> Result<()>
2046 where
2047 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2048 Fut: Future<Output = Result<()>> + Send + 'static,
2049 {
2050 let handler = Arc::new(
2051 move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2052 Box::pin(handler(frame))
2053 },
2054 );
2055 self.frameattached_handlers.lock().unwrap().push(handler);
2056 Ok(())
2057 }
2058
2059 /// Registers a handler for the `frameDetached` event.
2060 ///
2061 /// The handler is called when a frame (iframe) is detached from the page.
2062 /// The handler receives the detached [`Frame`](crate::protocol::Frame) object.
2063 ///
2064 /// # Arguments
2065 ///
2066 /// * `handler` - Async closure that receives the detached Frame
2067 ///
2068 /// See: <https://playwright.dev/docs/api/class-page#page-event-framedetached>
2069 pub async fn on_framedetached<F, Fut>(&self, handler: F) -> Result<()>
2070 where
2071 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2072 Fut: Future<Output = Result<()>> + Send + 'static,
2073 {
2074 let handler = Arc::new(
2075 move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2076 Box::pin(handler(frame))
2077 },
2078 );
2079 self.framedetached_handlers.lock().unwrap().push(handler);
2080 Ok(())
2081 }
2082
2083 /// Registers a handler for the `frameNavigated` event.
2084 ///
2085 /// The handler is called when a frame navigates to a new URL.
2086 /// The handler receives the navigated [`Frame`](crate::protocol::Frame) object.
2087 ///
2088 /// # Arguments
2089 ///
2090 /// * `handler` - Async closure that receives the navigated Frame
2091 ///
2092 /// See: <https://playwright.dev/docs/api/class-page#page-event-framenavigated>
2093 pub async fn on_framenavigated<F, Fut>(&self, handler: F) -> Result<()>
2094 where
2095 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2096 Fut: Future<Output = Result<()>> + Send + 'static,
2097 {
2098 let handler = Arc::new(
2099 move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2100 Box::pin(handler(frame))
2101 },
2102 );
2103 self.framenavigated_handlers.lock().unwrap().push(handler);
2104 Ok(())
2105 }
2106
2107 /// Exposes a Rust function to this page as `window[name]` in JavaScript.
2108 ///
2109 /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
2110 /// server fires a `bindingCall` event on the **page** channel that invokes
2111 /// `callback` with the deserialized arguments. The return value is sent back
2112 /// to JS so the `await window[name](…)` expression resolves with it.
2113 ///
2114 /// The binding is page-scoped and not visible to other pages in the same context.
2115 ///
2116 /// # Arguments
2117 ///
2118 /// * `name` – JavaScript identifier that will be available as `window[name]`.
2119 /// * `callback` – Async closure called with `Vec<serde_json::Value>` (JS arguments)
2120 /// returning `serde_json::Value` (the result).
2121 ///
2122 /// # Errors
2123 ///
2124 /// Returns error if:
2125 /// - The page has been closed.
2126 /// - Communication with the browser process fails.
2127 ///
2128 /// See: <https://playwright.dev/docs/api/class-page#page-expose-function>
2129 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
2130 where
2131 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2132 Fut: Future<Output = serde_json::Value> + Send + 'static,
2133 {
2134 self.expose_binding_internal(name, false, callback).await
2135 }
2136
2137 /// Exposes a Rust function to this page as `window[name]` in JavaScript,
2138 /// with `needsHandle: true`.
2139 ///
2140 /// Identical to [`expose_function`](Self::expose_function) but the Playwright
2141 /// server passes the first argument as a `JSHandle` object rather than a plain
2142 /// value.
2143 ///
2144 /// # Arguments
2145 ///
2146 /// * `name` – JavaScript identifier.
2147 /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
2148 ///
2149 /// # Errors
2150 ///
2151 /// Returns error if:
2152 /// - The page has been closed.
2153 /// - Communication with the browser process fails.
2154 ///
2155 /// See: <https://playwright.dev/docs/api/class-page#page-expose-binding>
2156 pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
2157 where
2158 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2159 Fut: Future<Output = serde_json::Value> + Send + 'static,
2160 {
2161 self.expose_binding_internal(name, true, callback).await
2162 }
2163
2164 /// Internal implementation shared by page-level expose_function and expose_binding.
2165 ///
2166 /// Both `expose_function` and `expose_binding` use `needsHandle: false` because
2167 /// the current implementation does not support JSHandle objects. Using
2168 /// `needsHandle: true` would cause the Playwright server to wrap the first
2169 /// argument as a `JSHandle`, which requires a JSHandle protocol object that
2170 /// is not yet implemented.
2171 async fn expose_binding_internal<F, Fut>(
2172 &self,
2173 name: &str,
2174 _needs_handle: bool,
2175 callback: F,
2176 ) -> Result<()>
2177 where
2178 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2179 Fut: Future<Output = serde_json::Value> + Send + 'static,
2180 {
2181 let callback: PageBindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
2182 Box::pin(callback(args)) as PageBindingCallbackFuture
2183 });
2184
2185 // Store callback before sending RPC (avoids race with early bindingCall events)
2186 self.binding_callbacks
2187 .lock()
2188 .unwrap()
2189 .insert(name.to_string(), callback);
2190
2191 // Tell the Playwright server to inject window[name] into this page.
2192 // Always use needsHandle: false — see note above.
2193 self.channel()
2194 .send_no_result(
2195 "exposeBinding",
2196 serde_json::json!({ "name": name, "needsHandle": false }),
2197 )
2198 .await
2199 }
2200
2201 /// Handles a download event from the protocol
2202 async fn on_download_event(&self, download: Download) {
2203 let handlers = self.download_handlers.lock().unwrap().clone();
2204
2205 for handler in handlers {
2206 if let Err(e) = handler(download.clone()).await {
2207 tracing::warn!("Download handler error: {}", e);
2208 }
2209 }
2210 // Notify the first expect_download() waiter (FIFO order)
2211 if let Some(tx) = self.download_waiters.lock().unwrap().pop() {
2212 let _ = tx.send(download);
2213 }
2214 }
2215
2216 /// Handles a dialog event from the protocol
2217 async fn on_dialog_event(&self, dialog: Dialog) {
2218 let handlers = self.dialog_handlers.lock().unwrap().clone();
2219
2220 for handler in handlers {
2221 if let Err(e) = handler(dialog.clone()).await {
2222 tracing::warn!("Dialog handler error: {}", e);
2223 }
2224 }
2225 }
2226
2227 async fn on_request_event(&self, request: Request) {
2228 let handlers = self.request_handlers.lock().unwrap().clone();
2229
2230 for handler in handlers {
2231 if let Err(e) = handler(request.clone()).await {
2232 tracing::warn!("Request handler error: {}", e);
2233 }
2234 }
2235 // Notify the first expect_request() waiter (FIFO order)
2236 if let Some(tx) = self.request_waiters.lock().unwrap().pop() {
2237 let _ = tx.send(request);
2238 }
2239 }
2240
2241 async fn on_request_failed_event(&self, request: Request) {
2242 let handlers = self.request_failed_handlers.lock().unwrap().clone();
2243
2244 for handler in handlers {
2245 if let Err(e) = handler(request.clone()).await {
2246 tracing::warn!("RequestFailed handler error: {}", e);
2247 }
2248 }
2249 }
2250
2251 async fn on_request_finished_event(&self, request: Request) {
2252 let handlers = self.request_finished_handlers.lock().unwrap().clone();
2253
2254 for handler in handlers {
2255 if let Err(e) = handler(request.clone()).await {
2256 tracing::warn!("RequestFinished handler error: {}", e);
2257 }
2258 }
2259 }
2260
2261 async fn on_response_event(&self, response: ResponseObject) {
2262 let handlers = self.response_handlers.lock().unwrap().clone();
2263
2264 for handler in handlers {
2265 if let Err(e) = handler(response.clone()).await {
2266 tracing::warn!("Response handler error: {}", e);
2267 }
2268 }
2269 // Notify the first expect_response() waiter (FIFO order)
2270 if let Some(tx) = self.response_waiters.lock().unwrap().pop() {
2271 let _ = tx.send(response);
2272 }
2273 }
2274
2275 /// Triggers dialog event (called by BrowserContext when dialog events arrive)
2276 ///
2277 /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
2278 /// This method is public so BrowserContext can forward dialog events.
2279 pub async fn trigger_dialog_event(&self, dialog: Dialog) {
2280 self.on_dialog_event(dialog).await;
2281 }
2282
2283 /// Triggers request event (called by BrowserContext when request events arrive)
2284 pub(crate) async fn trigger_request_event(&self, request: Request) {
2285 self.on_request_event(request).await;
2286 }
2287
2288 pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
2289 self.on_request_finished_event(request).await;
2290 }
2291
2292 pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
2293 self.on_request_failed_event(request).await;
2294 }
2295
2296 /// Triggers response event (called by BrowserContext when response events arrive)
2297 pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
2298 self.on_response_event(response).await;
2299 }
2300
2301 /// Triggers console event (called by BrowserContext when console events arrive).
2302 ///
2303 /// The BrowserContext receives all `"console"` events, constructs the
2304 /// [`ConsoleMessage`](crate::protocol::ConsoleMessage), dispatches to
2305 /// context-level handlers, then calls this method to forward to page-level handlers.
2306 pub(crate) async fn trigger_console_event(&self, msg: crate::protocol::ConsoleMessage) {
2307 self.on_console_event(msg).await;
2308 }
2309
2310 async fn on_console_event(&self, msg: crate::protocol::ConsoleMessage) {
2311 // Notify the first expect_console_message() waiter (FIFO order)
2312 if let Some(tx) = self.console_waiters.lock().unwrap().pop() {
2313 let _ = tx.send(msg.clone());
2314 }
2315 let handlers = self.console_handlers.lock().unwrap().clone();
2316 for handler in handlers {
2317 if let Err(e) = handler(msg.clone()).await {
2318 tracing::warn!("Console handler error: {}", e);
2319 }
2320 }
2321 }
2322
2323 /// Dispatches a FileChooser event to registered handlers and one-shot waiters.
2324 async fn on_filechooser_event(&self, chooser: crate::protocol::FileChooser) {
2325 // Dispatch to persistent handlers
2326 let handlers = self.filechooser_handlers.lock().unwrap().clone();
2327 for handler in handlers {
2328 if let Err(e) = handler(chooser.clone()).await {
2329 tracing::warn!("FileChooser handler error: {}", e);
2330 }
2331 }
2332
2333 // Notify the first expect_file_chooser() waiter (FIFO order)
2334 if let Some(tx) = self.filechooser_waiters.lock().unwrap().pop() {
2335 let _ = tx.send(chooser);
2336 }
2337 }
2338
2339 /// Triggers load event (called by Frame when loadstate "load" is added)
2340 pub(crate) async fn trigger_load_event(&self) {
2341 self.on_load_event().await;
2342 }
2343
2344 /// Triggers pageError event (called by BrowserContext when pageError arrives)
2345 pub(crate) async fn trigger_pageerror_event(&self, message: String) {
2346 self.on_pageerror_event(message).await;
2347 }
2348
2349 /// Triggers popup event (called by BrowserContext when a page is opened with an opener)
2350 pub(crate) async fn trigger_popup_event(&self, popup: Page) {
2351 self.on_popup_event(popup).await;
2352 }
2353
2354 /// Triggers frameNavigated event (called by Frame when "navigated" is received)
2355 pub(crate) async fn trigger_framenavigated_event(&self, frame: crate::protocol::Frame) {
2356 self.on_framenavigated_event(frame).await;
2357 }
2358
2359 async fn on_close_event(&self) {
2360 let handlers = self.close_handlers.lock().unwrap().clone();
2361 for handler in handlers {
2362 if let Err(e) = handler().await {
2363 tracing::warn!("Close handler error: {}", e);
2364 }
2365 }
2366 }
2367
2368 async fn on_load_event(&self) {
2369 let handlers = self.load_handlers.lock().unwrap().clone();
2370 for handler in handlers {
2371 if let Err(e) = handler().await {
2372 tracing::warn!("Load handler error: {}", e);
2373 }
2374 }
2375 }
2376
2377 async fn on_crash_event(&self) {
2378 let handlers = self.crash_handlers.lock().unwrap().clone();
2379 for handler in handlers {
2380 if let Err(e) = handler().await {
2381 tracing::warn!("Crash handler error: {}", e);
2382 }
2383 }
2384 }
2385
2386 async fn on_pageerror_event(&self, message: String) {
2387 let handlers = self.pageerror_handlers.lock().unwrap().clone();
2388 for handler in handlers {
2389 if let Err(e) = handler(message.clone()).await {
2390 tracing::warn!("PageError handler error: {}", e);
2391 }
2392 }
2393 }
2394
2395 async fn on_popup_event(&self, popup: Page) {
2396 let handlers = self.popup_handlers.lock().unwrap().clone();
2397 for handler in handlers {
2398 if let Err(e) = handler(popup.clone()).await {
2399 tracing::warn!("Popup handler error: {}", e);
2400 }
2401 }
2402 // Notify the first expect_popup() waiter (FIFO order)
2403 if let Some(tx) = self.popup_waiters.lock().unwrap().pop() {
2404 let _ = tx.send(popup);
2405 }
2406 }
2407
2408 async fn on_frameattached_event(&self, frame: crate::protocol::Frame) {
2409 let handlers = self.frameattached_handlers.lock().unwrap().clone();
2410 for handler in handlers {
2411 if let Err(e) = handler(frame.clone()).await {
2412 tracing::warn!("FrameAttached handler error: {}", e);
2413 }
2414 }
2415 }
2416
2417 async fn on_framedetached_event(&self, frame: crate::protocol::Frame) {
2418 let handlers = self.framedetached_handlers.lock().unwrap().clone();
2419 for handler in handlers {
2420 if let Err(e) = handler(frame.clone()).await {
2421 tracing::warn!("FrameDetached handler error: {}", e);
2422 }
2423 }
2424 }
2425
2426 async fn on_framenavigated_event(&self, frame: crate::protocol::Frame) {
2427 let handlers = self.framenavigated_handlers.lock().unwrap().clone();
2428 for handler in handlers {
2429 if let Err(e) = handler(frame.clone()).await {
2430 tracing::warn!("FrameNavigated handler error: {}", e);
2431 }
2432 }
2433 }
2434
2435 /// Adds a `<style>` tag into the page with the desired content.
2436 ///
2437 /// # Arguments
2438 ///
2439 /// * `options` - Style tag options (content, url, or path)
2440 ///
2441 /// # Returns
2442 ///
2443 /// Returns an ElementHandle pointing to the injected `<style>` tag
2444 ///
2445 /// # Example
2446 ///
2447 /// ```no_run
2448 /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
2449 /// # #[tokio::main]
2450 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2451 /// # let playwright = Playwright::launch().await?;
2452 /// # let browser = playwright.chromium().launch().await?;
2453 /// # let context = browser.new_context().await?;
2454 /// # let page = context.new_page().await?;
2455 /// use playwright_rs::protocol::AddStyleTagOptions;
2456 ///
2457 /// // With inline CSS
2458 /// page.add_style_tag(
2459 /// AddStyleTagOptions::builder()
2460 /// .content("body { background-color: red; }")
2461 /// .build()
2462 /// ).await?;
2463 ///
2464 /// // With external URL
2465 /// page.add_style_tag(
2466 /// AddStyleTagOptions::builder()
2467 /// .url("https://example.com/style.css")
2468 /// .build()
2469 /// ).await?;
2470 ///
2471 /// // From file
2472 /// page.add_style_tag(
2473 /// AddStyleTagOptions::builder()
2474 /// .path("./styles/custom.css")
2475 /// .build()
2476 /// ).await?;
2477 /// # Ok(())
2478 /// # }
2479 /// ```
2480 ///
2481 /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
2482 pub async fn add_style_tag(
2483 &self,
2484 options: AddStyleTagOptions,
2485 ) -> Result<Arc<crate::protocol::ElementHandle>> {
2486 let frame = self.main_frame().await?;
2487 frame.add_style_tag(options).await
2488 }
2489
2490 /// Adds a script which would be evaluated in one of the following scenarios:
2491 /// - Whenever the page is navigated
2492 /// - Whenever a child frame is attached or navigated
2493 ///
2494 /// The script is evaluated after the document was created but before any of its scripts were run.
2495 ///
2496 /// # Arguments
2497 ///
2498 /// * `script` - JavaScript code to be injected into the page
2499 ///
2500 /// # Example
2501 ///
2502 /// ```no_run
2503 /// # use playwright_rs::protocol::Playwright;
2504 /// # #[tokio::main]
2505 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2506 /// # let playwright = Playwright::launch().await?;
2507 /// # let browser = playwright.chromium().launch().await?;
2508 /// # let context = browser.new_context().await?;
2509 /// # let page = context.new_page().await?;
2510 /// page.add_init_script("window.injected = 123;").await?;
2511 /// # Ok(())
2512 /// # }
2513 /// ```
2514 ///
2515 /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
2516 pub async fn add_init_script(&self, script: &str) -> Result<()> {
2517 self.channel()
2518 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
2519 .await
2520 }
2521
2522 /// Sets the viewport size for the page.
2523 ///
2524 /// This method allows dynamic resizing of the viewport after page creation,
2525 /// useful for testing responsive layouts at different screen sizes.
2526 ///
2527 /// # Arguments
2528 ///
2529 /// * `viewport` - The viewport dimensions (width and height in pixels)
2530 ///
2531 /// # Example
2532 ///
2533 /// ```no_run
2534 /// # use playwright_rs::protocol::{Playwright, Viewport};
2535 /// # #[tokio::main]
2536 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2537 /// # let playwright = Playwright::launch().await?;
2538 /// # let browser = playwright.chromium().launch().await?;
2539 /// # let page = browser.new_page().await?;
2540 /// // Set viewport to mobile size
2541 /// let mobile = Viewport {
2542 /// width: 375,
2543 /// height: 667,
2544 /// };
2545 /// page.set_viewport_size(mobile).await?;
2546 ///
2547 /// // Later, test desktop layout
2548 /// let desktop = Viewport {
2549 /// width: 1920,
2550 /// height: 1080,
2551 /// };
2552 /// page.set_viewport_size(desktop).await?;
2553 /// # Ok(())
2554 /// # }
2555 /// ```
2556 ///
2557 /// # Errors
2558 ///
2559 /// Returns error if:
2560 /// - Page has been closed
2561 /// - Communication with browser process fails
2562 ///
2563 /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
2564 pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
2565 // Store the new viewport locally so viewport_size() can reflect the change
2566 if let Ok(mut guard) = self.viewport.write() {
2567 *guard = Some(viewport.clone());
2568 }
2569 self.channel()
2570 .send_no_result(
2571 "setViewportSize",
2572 serde_json::json!({ "viewportSize": viewport }),
2573 )
2574 .await
2575 }
2576
2577 /// Brings this page to the front (activates the tab).
2578 ///
2579 /// Activates the page in the browser, making it the focused tab. This is
2580 /// useful in multi-page tests to ensure actions target the correct page.
2581 ///
2582 /// # Errors
2583 ///
2584 /// Returns error if:
2585 /// - Page has been closed
2586 /// - Communication with browser process fails
2587 ///
2588 /// See: <https://playwright.dev/docs/api/class-page#page-bring-to-front>
2589 pub async fn bring_to_front(&self) -> Result<()> {
2590 self.channel()
2591 .send_no_result("bringToFront", serde_json::json!({}))
2592 .await
2593 }
2594
2595 /// Sets extra HTTP headers that will be sent with every request from this page.
2596 ///
2597 /// These headers are sent in addition to headers set on the browser context via
2598 /// `BrowserContext::set_extra_http_headers()`. Page-level headers take precedence
2599 /// over context-level headers when names conflict.
2600 ///
2601 /// # Arguments
2602 ///
2603 /// * `headers` - Map of header names to values.
2604 ///
2605 /// # Errors
2606 ///
2607 /// Returns error if:
2608 /// - Page has been closed
2609 /// - Communication with browser process fails
2610 ///
2611 /// See: <https://playwright.dev/docs/api/class-page#page-set-extra-http-headers>
2612 pub async fn set_extra_http_headers(
2613 &self,
2614 headers: std::collections::HashMap<String, String>,
2615 ) -> Result<()> {
2616 // Playwright protocol expects an array of {name, value} objects
2617 // This RPC is sent on the Page channel (not the Frame channel)
2618 let headers_array: Vec<serde_json::Value> = headers
2619 .into_iter()
2620 .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
2621 .collect();
2622 self.channel()
2623 .send_no_result(
2624 "setExtraHTTPHeaders",
2625 serde_json::json!({ "headers": headers_array }),
2626 )
2627 .await
2628 }
2629
2630 /// Emulates media features for the page.
2631 ///
2632 /// This method allows emulating CSS media features such as `media`, `color-scheme`,
2633 /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
2634 ///
2635 /// To reset a specific feature to the browser default, use the `NoOverride` variant.
2636 ///
2637 /// # Arguments
2638 ///
2639 /// * `options` - Optional emulation options. If `None`, this is a no-op.
2640 ///
2641 /// # Example
2642 ///
2643 /// ```no_run
2644 /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
2645 /// # #[tokio::main]
2646 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2647 /// # let playwright = Playwright::launch().await?;
2648 /// # let browser = playwright.chromium().launch().await?;
2649 /// # let page = browser.new_page().await?;
2650 /// // Emulate print media
2651 /// page.emulate_media(Some(
2652 /// EmulateMediaOptions::builder()
2653 /// .media(Media::Print)
2654 /// .build()
2655 /// )).await?;
2656 ///
2657 /// // Emulate dark color scheme
2658 /// page.emulate_media(Some(
2659 /// EmulateMediaOptions::builder()
2660 /// .color_scheme(ColorScheme::Dark)
2661 /// .build()
2662 /// )).await?;
2663 /// # Ok(())
2664 /// # }
2665 /// ```
2666 ///
2667 /// # Errors
2668 ///
2669 /// Returns error if:
2670 /// - Page has been closed
2671 /// - Communication with browser process fails
2672 ///
2673 /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2674 pub async fn emulate_media(&self, options: Option<EmulateMediaOptions>) -> Result<()> {
2675 let mut params = serde_json::json!({});
2676
2677 if let Some(opts) = options {
2678 if let Some(media) = opts.media {
2679 params["media"] = serde_json::to_value(media).map_err(|e| {
2680 crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
2681 })?;
2682 }
2683 if let Some(color_scheme) = opts.color_scheme {
2684 params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
2685 crate::error::Error::ProtocolError(format!(
2686 "Failed to serialize colorScheme: {}",
2687 e
2688 ))
2689 })?;
2690 }
2691 if let Some(reduced_motion) = opts.reduced_motion {
2692 params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
2693 crate::error::Error::ProtocolError(format!(
2694 "Failed to serialize reducedMotion: {}",
2695 e
2696 ))
2697 })?;
2698 }
2699 if let Some(forced_colors) = opts.forced_colors {
2700 params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
2701 crate::error::Error::ProtocolError(format!(
2702 "Failed to serialize forcedColors: {}",
2703 e
2704 ))
2705 })?;
2706 }
2707 }
2708
2709 self.channel().send_no_result("emulateMedia", params).await
2710 }
2711
2712 /// Generates a PDF of the page and returns it as bytes.
2713 ///
2714 /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
2715 /// not supported in Firefox or WebKit.
2716 ///
2717 /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
2718 /// saved to that file.
2719 ///
2720 /// # Arguments
2721 ///
2722 /// * `options` - Optional PDF generation options
2723 ///
2724 /// # Example
2725 ///
2726 /// ```no_run
2727 /// # use playwright_rs::protocol::Playwright;
2728 /// # #[tokio::main]
2729 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2730 /// # let playwright = Playwright::launch().await?;
2731 /// # let browser = playwright.chromium().launch().await?;
2732 /// # let page = browser.new_page().await?;
2733 /// let pdf_bytes = page.pdf(None).await?;
2734 /// assert!(!pdf_bytes.is_empty());
2735 /// # Ok(())
2736 /// # }
2737 /// ```
2738 ///
2739 /// # Errors
2740 ///
2741 /// Returns error if:
2742 /// - The browser is not Chromium (PDF only supported in Chromium)
2743 /// - Page has been closed
2744 /// - Communication with browser process fails
2745 ///
2746 /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2747 pub async fn pdf(&self, options: Option<PdfOptions>) -> Result<Vec<u8>> {
2748 let mut params = serde_json::json!({});
2749 let mut save_path: Option<std::path::PathBuf> = None;
2750
2751 if let Some(opts) = options {
2752 // Capture the file path before consuming opts
2753 save_path = opts.path;
2754
2755 if let Some(scale) = opts.scale {
2756 params["scale"] = serde_json::json!(scale);
2757 }
2758 if let Some(v) = opts.display_header_footer {
2759 params["displayHeaderFooter"] = serde_json::json!(v);
2760 }
2761 if let Some(v) = opts.header_template {
2762 params["headerTemplate"] = serde_json::json!(v);
2763 }
2764 if let Some(v) = opts.footer_template {
2765 params["footerTemplate"] = serde_json::json!(v);
2766 }
2767 if let Some(v) = opts.print_background {
2768 params["printBackground"] = serde_json::json!(v);
2769 }
2770 if let Some(v) = opts.landscape {
2771 params["landscape"] = serde_json::json!(v);
2772 }
2773 if let Some(v) = opts.page_ranges {
2774 params["pageRanges"] = serde_json::json!(v);
2775 }
2776 if let Some(v) = opts.format {
2777 params["format"] = serde_json::json!(v);
2778 }
2779 if let Some(v) = opts.width {
2780 params["width"] = serde_json::json!(v);
2781 }
2782 if let Some(v) = opts.height {
2783 params["height"] = serde_json::json!(v);
2784 }
2785 if let Some(v) = opts.prefer_css_page_size {
2786 params["preferCSSPageSize"] = serde_json::json!(v);
2787 }
2788 if let Some(margin) = opts.margin {
2789 params["margin"] = serde_json::to_value(margin).map_err(|e| {
2790 crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
2791 })?;
2792 }
2793 }
2794
2795 #[derive(Deserialize)]
2796 struct PdfResponse {
2797 pdf: String,
2798 }
2799
2800 let response: PdfResponse = self.channel().send("pdf", params).await?;
2801
2802 // Decode base64 to bytes
2803 let pdf_bytes = base64::engine::general_purpose::STANDARD
2804 .decode(&response.pdf)
2805 .map_err(|e| {
2806 crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
2807 })?;
2808
2809 // If a path was specified, save the PDF to disk as well
2810 if let Some(path) = save_path {
2811 tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
2812 crate::error::Error::InvalidArgument(format!(
2813 "Failed to write PDF to '{}': {}",
2814 path.display(),
2815 e
2816 ))
2817 })?;
2818 }
2819
2820 Ok(pdf_bytes)
2821 }
2822
2823 /// Adds a `<script>` tag into the page with the desired URL or content.
2824 ///
2825 /// # Arguments
2826 ///
2827 /// * `options` - Optional script tag options (content, url, or path).
2828 /// If `None`, returns an error because no source is specified.
2829 ///
2830 /// At least one of `content`, `url`, or `path` must be provided.
2831 ///
2832 /// # Example
2833 ///
2834 /// ```no_run
2835 /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
2836 /// # #[tokio::main]
2837 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2838 /// # let playwright = Playwright::launch().await?;
2839 /// # let browser = playwright.chromium().launch().await?;
2840 /// # let context = browser.new_context().await?;
2841 /// # let page = context.new_page().await?;
2842 /// // With inline JavaScript
2843 /// page.add_script_tag(Some(
2844 /// AddScriptTagOptions::builder()
2845 /// .content("window.myVar = 42;")
2846 /// .build()
2847 /// )).await?;
2848 ///
2849 /// // With external URL
2850 /// page.add_script_tag(Some(
2851 /// AddScriptTagOptions::builder()
2852 /// .url("https://example.com/script.js")
2853 /// .build()
2854 /// )).await?;
2855 /// # Ok(())
2856 /// # }
2857 /// ```
2858 ///
2859 /// # Errors
2860 ///
2861 /// Returns error if:
2862 /// - `options` is `None` or no content/url/path is specified
2863 /// - Page has been closed
2864 /// - Script loading fails (e.g., invalid URL)
2865 ///
2866 /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
2867 pub async fn add_script_tag(
2868 &self,
2869 options: Option<AddScriptTagOptions>,
2870 ) -> Result<Arc<crate::protocol::ElementHandle>> {
2871 let opts = options.ok_or_else(|| {
2872 Error::InvalidArgument(
2873 "At least one of content, url, or path must be specified".to_string(),
2874 )
2875 })?;
2876 let frame = self.main_frame().await?;
2877 frame.add_script_tag(opts).await
2878 }
2879
2880 /// Returns the current viewport size of the page, or `None` if no viewport is set.
2881 ///
2882 /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
2883 /// returns the dimensions configured at context creation time or updated via
2884 /// `set_viewport_size()`.
2885 ///
2886 /// # Example
2887 ///
2888 /// ```ignore
2889 /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
2890 /// # #[tokio::main]
2891 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2892 /// # let playwright = Playwright::launch().await?;
2893 /// # let browser = playwright.chromium().launch().await?;
2894 /// let context = browser.new_context_with_options(
2895 /// BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
2896 /// ).await?;
2897 /// let page = context.new_page().await?;
2898 /// let size = page.viewport_size().expect("Viewport should be set");
2899 /// assert_eq!(size.width, 1280);
2900 /// assert_eq!(size.height, 720);
2901 /// # Ok(())
2902 /// # }
2903 /// ```
2904 ///
2905 /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
2906 pub fn viewport_size(&self) -> Option<Viewport> {
2907 self.viewport.read().ok()?.clone()
2908 }
2909}
2910
2911impl ChannelOwner for Page {
2912 fn guid(&self) -> &str {
2913 self.base.guid()
2914 }
2915
2916 fn type_name(&self) -> &str {
2917 self.base.type_name()
2918 }
2919
2920 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2921 self.base.parent()
2922 }
2923
2924 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2925 self.base.connection()
2926 }
2927
2928 fn initializer(&self) -> &Value {
2929 self.base.initializer()
2930 }
2931
2932 fn channel(&self) -> &Channel {
2933 self.base.channel()
2934 }
2935
2936 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2937 self.base.dispose(reason)
2938 }
2939
2940 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2941 self.base.adopt(child)
2942 }
2943
2944 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2945 self.base.add_child(guid, child)
2946 }
2947
2948 fn remove_child(&self, guid: &str) {
2949 self.base.remove_child(guid)
2950 }
2951
2952 fn on_event(&self, method: &str, params: Value) {
2953 match method {
2954 "navigated" => {
2955 // Update URL when page navigates
2956 if let Some(url_value) = params.get("url")
2957 && let Some(url_str) = url_value.as_str()
2958 && let Ok(mut url) = self.url.write()
2959 {
2960 *url = url_str.to_string();
2961 }
2962 }
2963 "route" => {
2964 // Handle network routing event
2965 if let Some(route_guid) = params
2966 .get("route")
2967 .and_then(|v| v.get("guid"))
2968 .and_then(|v| v.as_str())
2969 {
2970 // Get the Route object from connection's registry
2971 let connection = self.connection();
2972 let route_guid_owned = route_guid.to_string();
2973 let self_clone = self.clone();
2974
2975 tokio::spawn(async move {
2976 // Get and downcast Route object
2977 let route: Route =
2978 match connection.get_typed::<Route>(&route_guid_owned).await {
2979 Ok(r) => r,
2980 Err(e) => {
2981 tracing::warn!("Failed to get route object: {}", e);
2982 return;
2983 }
2984 };
2985
2986 // Set APIRequestContext on the route for fetch() support.
2987 // Page's parent is BrowserContext, which has the request context.
2988 if let Some(ctx) =
2989 downcast_parent::<crate::protocol::BrowserContext>(&self_clone)
2990 && let Ok(api_ctx) = ctx.request().await
2991 {
2992 route.set_api_request_context(api_ctx);
2993 }
2994
2995 // Call the route handler and wait for completion
2996 self_clone.on_route_event(route).await;
2997 });
2998 }
2999 }
3000 "download" => {
3001 // Handle download event
3002 // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
3003 let url = params
3004 .get("url")
3005 .and_then(|v| v.as_str())
3006 .unwrap_or("")
3007 .to_string();
3008
3009 let suggested_filename = params
3010 .get("suggestedFilename")
3011 .and_then(|v| v.as_str())
3012 .unwrap_or("")
3013 .to_string();
3014
3015 if let Some(artifact_guid) = params
3016 .get("artifact")
3017 .and_then(|v| v.get("guid"))
3018 .and_then(|v| v.as_str())
3019 {
3020 let connection = self.connection();
3021 let artifact_guid_owned = artifact_guid.to_string();
3022 let self_clone = self.clone();
3023
3024 tokio::spawn(async move {
3025 // Wait for Artifact object to be created
3026 let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
3027 Ok(obj) => obj,
3028 Err(e) => {
3029 tracing::warn!("Failed to get artifact object: {}", e);
3030 return;
3031 }
3032 };
3033
3034 // Create Download wrapper from Artifact + event params
3035 let download = Download::from_artifact(
3036 artifact_arc,
3037 url,
3038 suggested_filename,
3039 self_clone.clone(),
3040 );
3041
3042 // Call the download handlers
3043 self_clone.on_download_event(download).await;
3044 });
3045 }
3046 }
3047 "dialog" => {
3048 // Dialog events are handled by BrowserContext and forwarded to Page
3049 // This case should not be reached, but keeping for completeness
3050 }
3051 "webSocket" => {
3052 if let Some(ws_guid) = params
3053 .get("webSocket")
3054 .and_then(|v| v.get("guid"))
3055 .and_then(|v| v.as_str())
3056 {
3057 let connection = self.connection();
3058 let ws_guid_owned = ws_guid.to_string();
3059 let self_clone = self.clone();
3060
3061 tokio::spawn(async move {
3062 // Get and downcast WebSocket object
3063 let ws: WebSocket =
3064 match connection.get_typed::<WebSocket>(&ws_guid_owned).await {
3065 Ok(ws) => ws,
3066 Err(e) => {
3067 tracing::warn!("Failed to get WebSocket object: {}", e);
3068 return;
3069 }
3070 };
3071
3072 // Call handlers
3073 let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
3074 for handler in handlers {
3075 let ws_clone = ws.clone();
3076 tokio::spawn(async move {
3077 if let Err(e) = handler(ws_clone).await {
3078 tracing::error!("Error in websocket handler: {}", e);
3079 }
3080 });
3081 }
3082 });
3083 }
3084 }
3085 "worker" => {
3086 // A new Web Worker was created in the page.
3087 // Event format: {worker: {guid: "Worker@..."}}
3088 if let Some(worker_guid) = params
3089 .get("worker")
3090 .and_then(|v| v.get("guid"))
3091 .and_then(|v| v.as_str())
3092 {
3093 let connection = self.connection();
3094 let worker_guid_owned = worker_guid.to_string();
3095 let self_clone = self.clone();
3096
3097 tokio::spawn(async move {
3098 let worker: Worker =
3099 match connection.get_typed::<Worker>(&worker_guid_owned).await {
3100 Ok(w) => w,
3101 Err(e) => {
3102 tracing::warn!("Failed to get Worker object: {}", e);
3103 return;
3104 }
3105 };
3106
3107 let handlers = self_clone.worker_handlers.lock().unwrap().clone();
3108 for handler in handlers {
3109 let worker_clone = worker.clone();
3110 tokio::spawn(async move {
3111 if let Err(e) = handler(worker_clone).await {
3112 tracing::error!("Error in worker handler: {}", e);
3113 }
3114 });
3115 }
3116 });
3117 }
3118 }
3119 "bindingCall" => {
3120 // A JS caller on this page invoked a page-level exposed function.
3121 // Event format: {binding: {guid: "..."}}
3122 if let Some(binding_guid) = params
3123 .get("binding")
3124 .and_then(|v| v.get("guid"))
3125 .and_then(|v| v.as_str())
3126 {
3127 let connection = self.connection();
3128 let binding_guid_owned = binding_guid.to_string();
3129 let binding_callbacks = self.binding_callbacks.clone();
3130
3131 tokio::spawn(async move {
3132 let binding_call: crate::protocol::BindingCall = match connection
3133 .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
3134 .await
3135 {
3136 Ok(bc) => bc,
3137 Err(e) => {
3138 tracing::warn!("Failed to get BindingCall object: {}", e);
3139 return;
3140 }
3141 };
3142
3143 let name = binding_call.name().to_string();
3144
3145 // Look up page-level callback
3146 let callback = {
3147 let callbacks = binding_callbacks.lock().unwrap();
3148 callbacks.get(&name).cloned()
3149 };
3150
3151 let Some(callback) = callback else {
3152 // No page-level handler — the context-level handler on
3153 // BrowserContext::on_event("bindingCall") will handle it.
3154 return;
3155 };
3156
3157 // Deserialize args from Playwright protocol format
3158 let raw_args = binding_call.args();
3159 let args = crate::protocol::browser_context::BrowserContext::deserialize_binding_args_pub(raw_args);
3160
3161 // Call callback and serialize result
3162 let result_value = callback(args).await;
3163 let serialized =
3164 crate::protocol::evaluate_conversion::serialize_argument(&result_value);
3165
3166 if let Err(e) = binding_call.resolve(serialized).await {
3167 tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
3168 }
3169 });
3170 }
3171 }
3172 "fileChooser" => {
3173 // FileChooser event: sent when an <input type="file"> is interacted with.
3174 // Event params: {element: {guid: "..."}, isMultiple: bool}
3175 let is_multiple = params
3176 .get("isMultiple")
3177 .and_then(|v| v.as_bool())
3178 .unwrap_or(false);
3179
3180 if let Some(element_guid) = params
3181 .get("element")
3182 .and_then(|v| v.get("guid"))
3183 .and_then(|v| v.as_str())
3184 {
3185 let connection = self.connection();
3186 let element_guid_owned = element_guid.to_string();
3187 let self_clone = self.clone();
3188
3189 tokio::spawn(async move {
3190 let element: crate::protocol::ElementHandle = match connection
3191 .get_typed::<crate::protocol::ElementHandle>(&element_guid_owned)
3192 .await
3193 {
3194 Ok(e) => e,
3195 Err(err) => {
3196 tracing::warn!(
3197 "Failed to get ElementHandle for fileChooser: {}",
3198 err
3199 );
3200 return;
3201 }
3202 };
3203
3204 let chooser = crate::protocol::FileChooser::new(
3205 self_clone.clone(),
3206 std::sync::Arc::new(element),
3207 is_multiple,
3208 );
3209
3210 self_clone.on_filechooser_event(chooser).await;
3211 });
3212 }
3213 }
3214 "close" => {
3215 // Server-initiated close (e.g. context was closed)
3216 self.is_closed.store(true, Ordering::Relaxed);
3217 // Dispatch close handlers
3218 let self_clone = self.clone();
3219 tokio::spawn(async move {
3220 self_clone.on_close_event().await;
3221 });
3222 }
3223 "load" => {
3224 let self_clone = self.clone();
3225 tokio::spawn(async move {
3226 self_clone.on_load_event().await;
3227 });
3228 }
3229 "crash" => {
3230 let self_clone = self.clone();
3231 tokio::spawn(async move {
3232 self_clone.on_crash_event().await;
3233 });
3234 }
3235 "pageError" => {
3236 // params: {"error": {"message": "...", "stack": "..."}}
3237 let message = params
3238 .get("error")
3239 .and_then(|e| e.get("message"))
3240 .and_then(|m| m.as_str())
3241 .unwrap_or("")
3242 .to_string();
3243 let self_clone = self.clone();
3244 tokio::spawn(async move {
3245 self_clone.on_pageerror_event(message).await;
3246 });
3247 }
3248 // "popup" is forwarded from BrowserContext::on_event when a "page" event
3249 // is received for a page that has an opener. No direct "popup" event on Page.
3250 "frameAttached" => {
3251 // params: {"frame": {"guid": "..."}}
3252 if let Some(frame_guid) = params
3253 .get("frame")
3254 .and_then(|v| v.get("guid"))
3255 .and_then(|v| v.as_str())
3256 {
3257 let connection = self.connection();
3258 let frame_guid_owned = frame_guid.to_string();
3259 let self_clone = self.clone();
3260
3261 tokio::spawn(async move {
3262 let frame: crate::protocol::Frame = match connection
3263 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
3264 .await
3265 {
3266 Ok(f) => f,
3267 Err(e) => {
3268 tracing::warn!("Failed to get Frame for frameAttached: {}", e);
3269 return;
3270 }
3271 };
3272 self_clone.on_frameattached_event(frame).await;
3273 });
3274 }
3275 }
3276 "frameDetached" => {
3277 // params: {"frame": {"guid": "..."}}
3278 if let Some(frame_guid) = params
3279 .get("frame")
3280 .and_then(|v| v.get("guid"))
3281 .and_then(|v| v.as_str())
3282 {
3283 let connection = self.connection();
3284 let frame_guid_owned = frame_guid.to_string();
3285 let self_clone = self.clone();
3286
3287 tokio::spawn(async move {
3288 let frame: crate::protocol::Frame = match connection
3289 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
3290 .await
3291 {
3292 Ok(f) => f,
3293 Err(e) => {
3294 tracing::warn!("Failed to get Frame for frameDetached: {}", e);
3295 return;
3296 }
3297 };
3298 self_clone.on_framedetached_event(frame).await;
3299 });
3300 }
3301 }
3302 "frameNavigated" => {
3303 // params: {"frame": {"guid": "..."}}
3304 // Note: frameNavigated may also contain url, name, etc. at top level
3305 // The frame guid is in the "frame" field (same as attached/detached)
3306 if let Some(frame_guid) = params
3307 .get("frame")
3308 .and_then(|v| v.get("guid"))
3309 .and_then(|v| v.as_str())
3310 {
3311 let connection = self.connection();
3312 let frame_guid_owned = frame_guid.to_string();
3313 let self_clone = self.clone();
3314
3315 tokio::spawn(async move {
3316 let frame: crate::protocol::Frame = match connection
3317 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
3318 .await
3319 {
3320 Ok(f) => f,
3321 Err(e) => {
3322 tracing::warn!("Failed to get Frame for frameNavigated: {}", e);
3323 return;
3324 }
3325 };
3326 self_clone.on_framenavigated_event(frame).await;
3327 });
3328 }
3329 }
3330 _ => {
3331 // Other events not yet handled
3332 }
3333 }
3334 }
3335
3336 fn was_collected(&self) -> bool {
3337 self.base.was_collected()
3338 }
3339
3340 fn as_any(&self) -> &dyn Any {
3341 self
3342 }
3343}
3344
3345impl std::fmt::Debug for Page {
3346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3347 f.debug_struct("Page")
3348 .field("guid", &self.guid())
3349 .field("url", &self.url())
3350 .finish()
3351 }
3352}
3353
3354/// Options for page.goto() and page.reload()
3355#[derive(Debug, Clone)]
3356pub struct GotoOptions {
3357 /// Maximum operation time in milliseconds
3358 pub timeout: Option<std::time::Duration>,
3359 /// When to consider operation succeeded
3360 pub wait_until: Option<WaitUntil>,
3361}
3362
3363impl GotoOptions {
3364 /// Creates new GotoOptions with default values
3365 pub fn new() -> Self {
3366 Self {
3367 timeout: None,
3368 wait_until: None,
3369 }
3370 }
3371
3372 /// Sets the timeout
3373 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
3374 self.timeout = Some(timeout);
3375 self
3376 }
3377
3378 /// Sets the wait_until option
3379 pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
3380 self.wait_until = Some(wait_until);
3381 self
3382 }
3383}
3384
3385impl Default for GotoOptions {
3386 fn default() -> Self {
3387 Self::new()
3388 }
3389}
3390
3391/// When to consider navigation succeeded
3392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3393pub enum WaitUntil {
3394 /// Consider operation to be finished when the `load` event is fired
3395 Load,
3396 /// Consider operation to be finished when the `DOMContentLoaded` event is fired
3397 DomContentLoaded,
3398 /// Consider operation to be finished when there are no network connections for at least 500ms
3399 NetworkIdle,
3400 /// Consider operation to be finished when the commit event is fired
3401 Commit,
3402}
3403
3404impl WaitUntil {
3405 pub(crate) fn as_str(&self) -> &'static str {
3406 match self {
3407 WaitUntil::Load => "load",
3408 WaitUntil::DomContentLoaded => "domcontentloaded",
3409 WaitUntil::NetworkIdle => "networkidle",
3410 WaitUntil::Commit => "commit",
3411 }
3412 }
3413}
3414
3415/// Options for adding a style tag to the page
3416///
3417/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
3418#[derive(Debug, Clone, Default)]
3419pub struct AddStyleTagOptions {
3420 /// Raw CSS content to inject
3421 pub content: Option<String>,
3422 /// URL of the `<link>` tag to add
3423 pub url: Option<String>,
3424 /// Path to a CSS file to inject
3425 pub path: Option<String>,
3426}
3427
3428impl AddStyleTagOptions {
3429 /// Creates a new builder for AddStyleTagOptions
3430 pub fn builder() -> AddStyleTagOptionsBuilder {
3431 AddStyleTagOptionsBuilder::default()
3432 }
3433
3434 /// Validates that at least one option is specified
3435 pub(crate) fn validate(&self) -> Result<()> {
3436 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
3437 return Err(Error::InvalidArgument(
3438 "At least one of content, url, or path must be specified".to_string(),
3439 ));
3440 }
3441 Ok(())
3442 }
3443}
3444
3445/// Builder for AddStyleTagOptions
3446#[derive(Debug, Clone, Default)]
3447pub struct AddStyleTagOptionsBuilder {
3448 content: Option<String>,
3449 url: Option<String>,
3450 path: Option<String>,
3451}
3452
3453impl AddStyleTagOptionsBuilder {
3454 /// Sets the CSS content to inject
3455 pub fn content(mut self, content: impl Into<String>) -> Self {
3456 self.content = Some(content.into());
3457 self
3458 }
3459
3460 /// Sets the URL of the stylesheet
3461 pub fn url(mut self, url: impl Into<String>) -> Self {
3462 self.url = Some(url.into());
3463 self
3464 }
3465
3466 /// Sets the path to a CSS file
3467 pub fn path(mut self, path: impl Into<String>) -> Self {
3468 self.path = Some(path.into());
3469 self
3470 }
3471
3472 /// Builds the AddStyleTagOptions
3473 pub fn build(self) -> AddStyleTagOptions {
3474 AddStyleTagOptions {
3475 content: self.content,
3476 url: self.url,
3477 path: self.path,
3478 }
3479 }
3480}
3481
3482// ============================================================================
3483// AddScriptTagOptions
3484// ============================================================================
3485
3486/// Options for adding a `<script>` tag to the page.
3487///
3488/// At least one of `content`, `url`, or `path` must be specified.
3489///
3490/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
3491#[derive(Debug, Clone, Default)]
3492pub struct AddScriptTagOptions {
3493 /// Raw JavaScript content to inject
3494 pub content: Option<String>,
3495 /// URL of the `<script>` tag to add
3496 pub url: Option<String>,
3497 /// Path to a JavaScript file to inject (file contents will be read and sent as content)
3498 pub path: Option<String>,
3499 /// Script type attribute (e.g., `"module"`)
3500 pub type_: Option<String>,
3501}
3502
3503impl AddScriptTagOptions {
3504 /// Creates a new builder for AddScriptTagOptions
3505 pub fn builder() -> AddScriptTagOptionsBuilder {
3506 AddScriptTagOptionsBuilder::default()
3507 }
3508
3509 /// Validates that at least one option is specified
3510 pub(crate) fn validate(&self) -> Result<()> {
3511 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
3512 return Err(Error::InvalidArgument(
3513 "At least one of content, url, or path must be specified".to_string(),
3514 ));
3515 }
3516 Ok(())
3517 }
3518}
3519
3520/// Builder for AddScriptTagOptions
3521#[derive(Debug, Clone, Default)]
3522pub struct AddScriptTagOptionsBuilder {
3523 content: Option<String>,
3524 url: Option<String>,
3525 path: Option<String>,
3526 type_: Option<String>,
3527}
3528
3529impl AddScriptTagOptionsBuilder {
3530 /// Sets the JavaScript content to inject
3531 pub fn content(mut self, content: impl Into<String>) -> Self {
3532 self.content = Some(content.into());
3533 self
3534 }
3535
3536 /// Sets the URL of the script to load
3537 pub fn url(mut self, url: impl Into<String>) -> Self {
3538 self.url = Some(url.into());
3539 self
3540 }
3541
3542 /// Sets the path to a JavaScript file to inject
3543 pub fn path(mut self, path: impl Into<String>) -> Self {
3544 self.path = Some(path.into());
3545 self
3546 }
3547
3548 /// Sets the script type attribute (e.g., `"module"`)
3549 pub fn type_(mut self, type_: impl Into<String>) -> Self {
3550 self.type_ = Some(type_.into());
3551 self
3552 }
3553
3554 /// Builds the AddScriptTagOptions
3555 pub fn build(self) -> AddScriptTagOptions {
3556 AddScriptTagOptions {
3557 content: self.content,
3558 url: self.url,
3559 path: self.path,
3560 type_: self.type_,
3561 }
3562 }
3563}
3564
3565// ============================================================================
3566// EmulateMediaOptions and related enums
3567// ============================================================================
3568
3569/// Media type for `page.emulate_media()`.
3570///
3571/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3572#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3573#[serde(rename_all = "lowercase")]
3574pub enum Media {
3575 /// Emulate screen media type
3576 Screen,
3577 /// Emulate print media type
3578 Print,
3579 /// Reset media emulation to browser default (sends `"no-override"` to protocol)
3580 #[serde(rename = "no-override")]
3581 NoOverride,
3582}
3583
3584/// Preferred color scheme for `page.emulate_media()`.
3585///
3586/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3587#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3588pub enum ColorScheme {
3589 /// Emulate light color scheme
3590 #[serde(rename = "light")]
3591 Light,
3592 /// Emulate dark color scheme
3593 #[serde(rename = "dark")]
3594 Dark,
3595 /// Emulate no preference for color scheme
3596 #[serde(rename = "no-preference")]
3597 NoPreference,
3598 /// Reset color scheme to browser default
3599 #[serde(rename = "no-override")]
3600 NoOverride,
3601}
3602
3603/// Reduced motion preference for `page.emulate_media()`.
3604///
3605/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3607pub enum ReducedMotion {
3608 /// Emulate reduced motion preference
3609 #[serde(rename = "reduce")]
3610 Reduce,
3611 /// Emulate no preference for reduced motion
3612 #[serde(rename = "no-preference")]
3613 NoPreference,
3614 /// Reset reduced motion to browser default
3615 #[serde(rename = "no-override")]
3616 NoOverride,
3617}
3618
3619/// Forced colors preference for `page.emulate_media()`.
3620///
3621/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3622#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3623pub enum ForcedColors {
3624 /// Emulate active forced colors
3625 #[serde(rename = "active")]
3626 Active,
3627 /// Emulate no forced colors
3628 #[serde(rename = "none")]
3629 None_,
3630 /// Reset forced colors to browser default
3631 #[serde(rename = "no-override")]
3632 NoOverride,
3633}
3634
3635/// Options for `page.emulate_media()`.
3636///
3637/// All fields are optional. Fields that are `None` are omitted from the protocol
3638/// message (meaning they are not changed). To reset a field to browser default,
3639/// use the `NoOverride` variant.
3640///
3641/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3642#[derive(Debug, Clone, Default)]
3643pub struct EmulateMediaOptions {
3644 /// Media type to emulate (screen, print, or no-override)
3645 pub media: Option<Media>,
3646 /// Color scheme preference to emulate
3647 pub color_scheme: Option<ColorScheme>,
3648 /// Reduced motion preference to emulate
3649 pub reduced_motion: Option<ReducedMotion>,
3650 /// Forced colors preference to emulate
3651 pub forced_colors: Option<ForcedColors>,
3652}
3653
3654impl EmulateMediaOptions {
3655 /// Creates a new builder for EmulateMediaOptions
3656 pub fn builder() -> EmulateMediaOptionsBuilder {
3657 EmulateMediaOptionsBuilder::default()
3658 }
3659}
3660
3661/// Builder for EmulateMediaOptions
3662#[derive(Debug, Clone, Default)]
3663pub struct EmulateMediaOptionsBuilder {
3664 media: Option<Media>,
3665 color_scheme: Option<ColorScheme>,
3666 reduced_motion: Option<ReducedMotion>,
3667 forced_colors: Option<ForcedColors>,
3668}
3669
3670impl EmulateMediaOptionsBuilder {
3671 /// Sets the media type to emulate
3672 pub fn media(mut self, media: Media) -> Self {
3673 self.media = Some(media);
3674 self
3675 }
3676
3677 /// Sets the color scheme preference
3678 pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
3679 self.color_scheme = Some(color_scheme);
3680 self
3681 }
3682
3683 /// Sets the reduced motion preference
3684 pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
3685 self.reduced_motion = Some(reduced_motion);
3686 self
3687 }
3688
3689 /// Sets the forced colors preference
3690 pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
3691 self.forced_colors = Some(forced_colors);
3692 self
3693 }
3694
3695 /// Builds the EmulateMediaOptions
3696 pub fn build(self) -> EmulateMediaOptions {
3697 EmulateMediaOptions {
3698 media: self.media,
3699 color_scheme: self.color_scheme,
3700 reduced_motion: self.reduced_motion,
3701 forced_colors: self.forced_colors,
3702 }
3703 }
3704}
3705
3706// ============================================================================
3707// PdfOptions
3708// ============================================================================
3709
3710/// Margin options for PDF generation.
3711///
3712/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
3713#[derive(Debug, Clone, Default, Serialize)]
3714pub struct PdfMargin {
3715 /// Top margin (e.g. `"1in"`)
3716 #[serde(skip_serializing_if = "Option::is_none")]
3717 pub top: Option<String>,
3718 /// Right margin
3719 #[serde(skip_serializing_if = "Option::is_none")]
3720 pub right: Option<String>,
3721 /// Bottom margin
3722 #[serde(skip_serializing_if = "Option::is_none")]
3723 pub bottom: Option<String>,
3724 /// Left margin
3725 #[serde(skip_serializing_if = "Option::is_none")]
3726 pub left: Option<String>,
3727}
3728
3729/// Options for generating a PDF from a page.
3730///
3731/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
3732/// Firefox or WebKit will result in an error.
3733///
3734/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
3735#[derive(Debug, Clone, Default)]
3736pub struct PdfOptions {
3737 /// If specified, the PDF will also be saved to this file path.
3738 pub path: Option<std::path::PathBuf>,
3739 /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
3740 pub scale: Option<f64>,
3741 /// Whether to display header and footer (default false).
3742 pub display_header_footer: Option<bool>,
3743 /// HTML template for the print header. Should be valid HTML.
3744 pub header_template: Option<String>,
3745 /// HTML template for the print footer.
3746 pub footer_template: Option<String>,
3747 /// Whether to print background graphics (default false).
3748 pub print_background: Option<bool>,
3749 /// Paper orientation — `true` for landscape (default false).
3750 pub landscape: Option<bool>,
3751 /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
3752 pub page_ranges: Option<String>,
3753 /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
3754 pub format: Option<String>,
3755 /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
3756 pub width: Option<String>,
3757 /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
3758 pub height: Option<String>,
3759 /// Whether or not to prefer page size as defined by CSS.
3760 pub prefer_css_page_size: Option<bool>,
3761 /// Paper margins, defaulting to none.
3762 pub margin: Option<PdfMargin>,
3763}
3764
3765impl PdfOptions {
3766 /// Creates a new builder for PdfOptions
3767 pub fn builder() -> PdfOptionsBuilder {
3768 PdfOptionsBuilder::default()
3769 }
3770}
3771
3772/// Builder for PdfOptions
3773#[derive(Debug, Clone, Default)]
3774pub struct PdfOptionsBuilder {
3775 path: Option<std::path::PathBuf>,
3776 scale: Option<f64>,
3777 display_header_footer: Option<bool>,
3778 header_template: Option<String>,
3779 footer_template: Option<String>,
3780 print_background: Option<bool>,
3781 landscape: Option<bool>,
3782 page_ranges: Option<String>,
3783 format: Option<String>,
3784 width: Option<String>,
3785 height: Option<String>,
3786 prefer_css_page_size: Option<bool>,
3787 margin: Option<PdfMargin>,
3788}
3789
3790impl PdfOptionsBuilder {
3791 /// Sets the file path for saving the PDF
3792 pub fn path(mut self, path: std::path::PathBuf) -> Self {
3793 self.path = Some(path);
3794 self
3795 }
3796
3797 /// Sets the scale of the webpage rendering
3798 pub fn scale(mut self, scale: f64) -> Self {
3799 self.scale = Some(scale);
3800 self
3801 }
3802
3803 /// Sets whether to display header and footer
3804 pub fn display_header_footer(mut self, display: bool) -> Self {
3805 self.display_header_footer = Some(display);
3806 self
3807 }
3808
3809 /// Sets the HTML template for the print header
3810 pub fn header_template(mut self, template: impl Into<String>) -> Self {
3811 self.header_template = Some(template.into());
3812 self
3813 }
3814
3815 /// Sets the HTML template for the print footer
3816 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
3817 self.footer_template = Some(template.into());
3818 self
3819 }
3820
3821 /// Sets whether to print background graphics
3822 pub fn print_background(mut self, print: bool) -> Self {
3823 self.print_background = Some(print);
3824 self
3825 }
3826
3827 /// Sets whether to use landscape orientation
3828 pub fn landscape(mut self, landscape: bool) -> Self {
3829 self.landscape = Some(landscape);
3830 self
3831 }
3832
3833 /// Sets the page ranges to print
3834 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
3835 self.page_ranges = Some(ranges.into());
3836 self
3837 }
3838
3839 /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
3840 pub fn format(mut self, format: impl Into<String>) -> Self {
3841 self.format = Some(format.into());
3842 self
3843 }
3844
3845 /// Sets the paper width
3846 pub fn width(mut self, width: impl Into<String>) -> Self {
3847 self.width = Some(width.into());
3848 self
3849 }
3850
3851 /// Sets the paper height
3852 pub fn height(mut self, height: impl Into<String>) -> Self {
3853 self.height = Some(height.into());
3854 self
3855 }
3856
3857 /// Sets whether to prefer page size as defined by CSS
3858 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
3859 self.prefer_css_page_size = Some(prefer);
3860 self
3861 }
3862
3863 /// Sets the paper margins
3864 pub fn margin(mut self, margin: PdfMargin) -> Self {
3865 self.margin = Some(margin);
3866 self
3867 }
3868
3869 /// Builds the PdfOptions
3870 pub fn build(self) -> PdfOptions {
3871 PdfOptions {
3872 path: self.path,
3873 scale: self.scale,
3874 display_header_footer: self.display_header_footer,
3875 header_template: self.header_template,
3876 footer_template: self.footer_template,
3877 print_background: self.print_background,
3878 landscape: self.landscape,
3879 page_ranges: self.page_ranges,
3880 format: self.format,
3881 width: self.width,
3882 height: self.height,
3883 prefer_css_page_size: self.prefer_css_page_size,
3884 margin: self.margin,
3885 }
3886 }
3887}
3888
3889/// Response from navigation operations.
3890///
3891/// Returned from `page.goto()`, `page.reload()`, `page.go_back()`, and similar
3892/// navigation methods. Provides access to the HTTP response status, headers, and body.
3893///
3894/// See: <https://playwright.dev/docs/api/class-response>
3895#[derive(Clone)]
3896pub struct Response {
3897 url: String,
3898 status: u16,
3899 status_text: String,
3900 ok: bool,
3901 headers: std::collections::HashMap<String, String>,
3902 /// Reference to the backing channel owner for RPC calls (body, rawHeaders, etc.)
3903 /// Stored as the generic trait object so it can be downcast to ResponseObject when needed.
3904 response_channel_owner: Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
3905}
3906
3907impl Response {
3908 /// Creates a new Response from protocol data.
3909 ///
3910 /// This is used internally when constructing a Response from the protocol
3911 /// initializer (e.g., after `goto` or `reload`).
3912 pub(crate) fn new(
3913 url: String,
3914 status: u16,
3915 status_text: String,
3916 headers: std::collections::HashMap<String, String>,
3917 response_channel_owner: Option<
3918 std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>,
3919 >,
3920 ) -> Self {
3921 Self {
3922 url,
3923 status,
3924 status_text,
3925 ok: (200..300).contains(&status),
3926 headers,
3927 response_channel_owner,
3928 }
3929 }
3930}
3931
3932impl Response {
3933 /// Returns the URL of the response.
3934 ///
3935 /// See: <https://playwright.dev/docs/api/class-response#response-url>
3936 pub fn url(&self) -> &str {
3937 &self.url
3938 }
3939
3940 /// Returns the HTTP status code.
3941 ///
3942 /// See: <https://playwright.dev/docs/api/class-response#response-status>
3943 pub fn status(&self) -> u16 {
3944 self.status
3945 }
3946
3947 /// Returns the HTTP status text.
3948 ///
3949 /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
3950 pub fn status_text(&self) -> &str {
3951 &self.status_text
3952 }
3953
3954 /// Returns whether the response was successful (status 200-299).
3955 ///
3956 /// See: <https://playwright.dev/docs/api/class-response#response-ok>
3957 pub fn ok(&self) -> bool {
3958 self.ok
3959 }
3960
3961 /// Returns the response headers as a HashMap.
3962 ///
3963 /// Note: these are the headers from the protocol initializer. For the full
3964 /// raw headers (including duplicates), use `headers_array()` or `all_headers()`.
3965 ///
3966 /// See: <https://playwright.dev/docs/api/class-response#response-headers>
3967 pub fn headers(&self) -> &std::collections::HashMap<String, String> {
3968 &self.headers
3969 }
3970
3971 /// Returns the [`Request`] that triggered this response.
3972 ///
3973 /// Navigates the protocol object hierarchy: ResponseObject → parent (Request).
3974 ///
3975 /// See: <https://playwright.dev/docs/api/class-response#response-request>
3976 pub fn request(&self) -> Option<crate::protocol::Request> {
3977 let owner = self.response_channel_owner.as_ref()?;
3978 downcast_parent::<crate::protocol::Request>(&**owner)
3979 }
3980
3981 /// Returns the [`Frame`](crate::protocol::Frame) that initiated the request for this response.
3982 ///
3983 /// Navigates the protocol object hierarchy: ResponseObject → Request → Frame.
3984 ///
3985 /// See: <https://playwright.dev/docs/api/class-response#response-frame>
3986 pub fn frame(&self) -> Option<crate::protocol::Frame> {
3987 let request = self.request()?;
3988 request.frame()
3989 }
3990
3991 /// Returns the backing `ResponseObject`, or an error if unavailable.
3992 pub(crate) fn response_object(&self) -> crate::error::Result<crate::protocol::ResponseObject> {
3993 let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
3994 crate::error::Error::ProtocolError(
3995 "Response has no backing protocol object".to_string(),
3996 )
3997 })?;
3998 arc.as_any()
3999 .downcast_ref::<crate::protocol::ResponseObject>()
4000 .cloned()
4001 .ok_or_else(|| crate::error::Error::TypeMismatch {
4002 guid: arc.guid().to_string(),
4003 expected: "ResponseObject".to_string(),
4004 actual: arc.type_name().to_string(),
4005 })
4006 }
4007
4008 /// Returns TLS/SSL security details for HTTPS connections, or `None` for HTTP.
4009 ///
4010 /// See: <https://playwright.dev/docs/api/class-response#response-security-details>
4011 pub async fn security_details(
4012 &self,
4013 ) -> crate::error::Result<Option<crate::protocol::response::SecurityDetails>> {
4014 self.response_object()?.security_details().await
4015 }
4016
4017 /// Returns the server's IP address and port, or `None`.
4018 ///
4019 /// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
4020 pub async fn server_addr(
4021 &self,
4022 ) -> crate::error::Result<Option<crate::protocol::response::RemoteAddr>> {
4023 self.response_object()?.server_addr().await
4024 }
4025
4026 /// Waits for this response to finish loading.
4027 ///
4028 /// For responses obtained from navigation methods (`goto`, `reload`), the response
4029 /// is already finished when returned. For responses from `on_response` handlers,
4030 /// the body may still be loading.
4031 ///
4032 /// See: <https://playwright.dev/docs/api/class-response#response-finished>
4033 pub async fn finished(&self) -> crate::error::Result<()> {
4034 // The Playwright protocol dispatches `requestFinished` as a separate event
4035 // rather than exposing a `finished` RPC method on Response.
4036 // For responses from goto/reload, the response is already complete.
4037 // TODO: For on_response handlers, implement proper waiting via requestFinished event.
4038 Ok(())
4039 }
4040
4041 /// Returns the response body as raw bytes.
4042 ///
4043 /// Makes an RPC call to the Playwright server to fetch the response body.
4044 ///
4045 /// # Errors
4046 ///
4047 /// Returns an error if:
4048 /// - No backing protocol object is available (edge case)
4049 /// - The RPC call to the server fails
4050 /// - The base64 response cannot be decoded
4051 ///
4052 /// See: <https://playwright.dev/docs/api/class-response#response-body>
4053 pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
4054 self.response_object()?.body().await
4055 }
4056
4057 /// Returns the response body as a UTF-8 string.
4058 ///
4059 /// Calls `body()` then converts bytes to a UTF-8 string.
4060 ///
4061 /// # Errors
4062 ///
4063 /// Returns an error if:
4064 /// - `body()` fails
4065 /// - The body is not valid UTF-8
4066 ///
4067 /// See: <https://playwright.dev/docs/api/class-response#response-text>
4068 pub async fn text(&self) -> crate::error::Result<String> {
4069 let bytes = self.body().await?;
4070 String::from_utf8(bytes).map_err(|e| {
4071 crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
4072 })
4073 }
4074
4075 /// Parses the response body as JSON and deserializes it into type `T`.
4076 ///
4077 /// Calls `text()` then uses `serde_json` to deserialize the body.
4078 ///
4079 /// # Errors
4080 ///
4081 /// Returns an error if:
4082 /// - `text()` fails
4083 /// - The body is not valid JSON or doesn't match the expected type
4084 ///
4085 /// See: <https://playwright.dev/docs/api/class-response#response-json>
4086 pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
4087 let text = self.text().await?;
4088 serde_json::from_str(&text).map_err(|e| {
4089 crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
4090 })
4091 }
4092
4093 /// Returns all response headers as name-value pairs, preserving duplicates.
4094 ///
4095 /// Makes an RPC call for `"rawHeaders"` which returns the complete header list.
4096 ///
4097 /// # Errors
4098 ///
4099 /// Returns an error if:
4100 /// - No backing protocol object is available (edge case)
4101 /// - The RPC call to the server fails
4102 ///
4103 /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
4104 pub async fn headers_array(
4105 &self,
4106 ) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
4107 self.response_object()?.raw_headers().await
4108 }
4109
4110 /// Returns all response headers merged into a HashMap with lowercase keys.
4111 ///
4112 /// When multiple headers have the same name, their values are joined with `, `.
4113 /// This matches the behavior of `response.allHeaders()` in other Playwright bindings.
4114 ///
4115 /// # Errors
4116 ///
4117 /// Returns an error if:
4118 /// - No backing protocol object is available (edge case)
4119 /// - The RPC call to the server fails
4120 ///
4121 /// See: <https://playwright.dev/docs/api/class-response#response-all-headers>
4122 pub async fn all_headers(
4123 &self,
4124 ) -> crate::error::Result<std::collections::HashMap<String, String>> {
4125 let entries = self.headers_array().await?;
4126 let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
4127 for entry in entries {
4128 let key = entry.name.to_lowercase();
4129 map.entry(key)
4130 .and_modify(|v| {
4131 v.push_str(", ");
4132 v.push_str(&entry.value);
4133 })
4134 .or_insert(entry.value);
4135 }
4136 Ok(map)
4137 }
4138
4139 /// Returns the value for a single response header, or `None` if not present.
4140 ///
4141 /// The lookup is case-insensitive.
4142 ///
4143 /// # Errors
4144 ///
4145 /// Returns an error if:
4146 /// - No backing protocol object is available (edge case)
4147 /// - The RPC call to the server fails
4148 ///
4149 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
4150 /// Returns the value for a single response header, or `None` if not present.
4151 ///
4152 /// The lookup is case-insensitive. When multiple headers share the same name,
4153 /// their values are joined with `, ` (matching Playwright's behavior).
4154 ///
4155 /// Uses the raw headers from the server for accurate results.
4156 ///
4157 /// # Errors
4158 ///
4159 /// Returns an error if the underlying `headers_array()` RPC call fails.
4160 ///
4161 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
4162 pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
4163 let entries = self.headers_array().await?;
4164 let name_lower = name.to_lowercase();
4165 let mut values: Vec<String> = entries
4166 .into_iter()
4167 .filter(|h| h.name.to_lowercase() == name_lower)
4168 .map(|h| h.value)
4169 .collect();
4170
4171 if values.is_empty() {
4172 Ok(None)
4173 } else if values.len() == 1 {
4174 Ok(Some(values.remove(0)))
4175 } else {
4176 Ok(Some(values.join(", ")))
4177 }
4178 }
4179}
4180
4181impl std::fmt::Debug for Response {
4182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4183 f.debug_struct("Response")
4184 .field("url", &self.url)
4185 .field("status", &self.status)
4186 .field("status_text", &self.status_text)
4187 .field("ok", &self.ok)
4188 .finish_non_exhaustive()
4189 }
4190}
4191
4192/// Shared helper: store timeout locally and notify the Playwright server.
4193/// Used by both Page and BrowserContext timeout setters.
4194pub(crate) async fn set_timeout_and_notify(
4195 channel: &crate::server::channel::Channel,
4196 method: &str,
4197 timeout: f64,
4198) {
4199 if let Err(e) = channel
4200 .send_no_result(method, serde_json::json!({ "timeout": timeout }))
4201 .await
4202 {
4203 tracing::warn!("{} send error: {}", method, e);
4204 }
4205}