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 /// WebSocketRoute handlers for route_web_socket()
204 ws_route_handlers: Arc<Mutex<Vec<WsRouteHandlerEntry>>>,
205 /// Current viewport size (None when no_viewport is set).
206 /// Updated by set_viewport_size().
207 viewport: Arc<RwLock<Option<Viewport>>>,
208 /// Whether this page has been closed.
209 /// Set to true when close() is called or a "close" event is received.
210 is_closed: Arc<AtomicBool>,
211 /// Default timeout for actions (milliseconds), stored as f64 bits.
212 default_timeout_ms: Arc<AtomicU64>,
213 /// Default timeout for navigation operations (milliseconds), stored as f64 bits.
214 default_navigation_timeout_ms: Arc<AtomicU64>,
215 /// Page-level binding callbacks registered via expose_function / expose_binding
216 binding_callbacks: Arc<Mutex<HashMap<String, PageBindingCallback>>>,
217 /// Console event handlers
218 console_handlers: Arc<Mutex<Vec<ConsoleHandler>>>,
219 /// FileChooser event handlers
220 filechooser_handlers: Arc<Mutex<Vec<FileChooserHandler>>>,
221 /// One-shot senders waiting for the next "fileChooser" event (expect_file_chooser)
222 filechooser_waiters:
223 Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::FileChooser>>>>,
224 /// One-shot senders waiting for the next "popup" event (expect_popup)
225 popup_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Page>>>>,
226 /// One-shot senders waiting for the next "download" event (expect_download)
227 download_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Download>>>>,
228 /// One-shot senders waiting for the next "response" event (expect_response)
229 response_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<ResponseObject>>>>,
230 /// One-shot senders waiting for the next "request" event (expect_request)
231 request_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<Request>>>>,
232 /// One-shot senders waiting for the next "console" event (expect_console_message)
233 console_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::ConsoleMessage>>>>,
234 /// close event handlers (fires when page is closed)
235 close_handlers: Arc<Mutex<Vec<CloseHandler>>>,
236 /// load event handlers (fires when page fully loads)
237 load_handlers: Arc<Mutex<Vec<LoadHandler>>>,
238 /// crash event handlers (fires when page crashes)
239 crash_handlers: Arc<Mutex<Vec<CrashHandler>>>,
240 /// pageError event handlers (fires on uncaught JS exceptions)
241 pageerror_handlers: Arc<Mutex<Vec<PageErrorHandler>>>,
242 /// popup event handlers (fires when a popup window opens)
243 popup_handlers: Arc<Mutex<Vec<PopupHandler>>>,
244 /// frameAttached event handlers
245 frameattached_handlers: Arc<Mutex<Vec<FrameAttachedHandler>>>,
246 /// frameDetached event handlers
247 framedetached_handlers: Arc<Mutex<Vec<FrameDetachedHandler>>>,
248 /// frameNavigated event handlers
249 framenavigated_handlers: Arc<Mutex<Vec<FrameNavigatedHandler>>>,
250 /// worker event handlers (fires when a web worker is created in the page)
251 worker_handlers: Arc<Mutex<Vec<WorkerHandler>>>,
252 /// One-shot senders waiting for the next "close" event (expect_event("close"))
253 close_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<()>>>>,
254 /// One-shot senders waiting for the next "load" event (expect_event("load"))
255 load_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<()>>>>,
256 /// One-shot senders waiting for the next "crash" event (expect_event("crash"))
257 crash_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<()>>>>,
258 /// One-shot senders waiting for the next "pageerror" event (expect_event("pageerror"))
259 pageerror_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<String>>>>,
260 /// One-shot senders waiting for the next frame event (frameattached/detached/navigated)
261 frameattached_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::Frame>>>>,
262 framedetached_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::Frame>>>>,
263 framenavigated_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::Frame>>>>,
264 /// One-shot senders waiting for the next "worker" event (expect_event("worker"))
265 worker_waiters: Arc<Mutex<Vec<tokio::sync::oneshot::Sender<crate::protocol::Worker>>>>,
266 /// Accumulated console messages received so far (appended by trigger_console_event)
267 console_messages_log: Arc<Mutex<Vec<crate::protocol::ConsoleMessage>>>,
268 /// Accumulated uncaught JS error messages received so far (appended by trigger_pageerror_event)
269 page_errors_log: Arc<Mutex<Vec<String>>>,
270 /// Active web workers tracked via "worker" events (appended on creation)
271 workers_list: Arc<Mutex<Vec<Worker>>>,
272 /// Video object — Some when this page was created in a record_video context.
273 /// The inner Video is created eagerly on Page construction; the underlying
274 /// Artifact GUID is read from the Page initializer and resolved asynchronously.
275 video: Option<crate::protocol::Video>,
276 /// Registered locator handlers: maps uid -> (selector, handler fn, times_remaining)
277 /// times_remaining is None when the handler should run indefinitely.
278 locator_handlers: Arc<Mutex<Vec<LocatorHandlerEntry>>>,
279}
280
281/// Type alias for boxed route handler future
282type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
283
284/// Type alias for boxed download handler future
285type DownloadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
286
287/// Type alias for boxed dialog handler future
288type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
289
290/// Type alias for boxed request handler future
291type RequestHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
292
293/// Type alias for boxed response handler future
294type ResponseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
295
296/// Type alias for boxed websocket handler future
297type WebSocketHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
298
299/// Type alias for boxed WebSocketRoute handler future
300type WebSocketRouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
301
302/// Storage for a single WebSocket route handler entry
303#[derive(Clone)]
304struct WsRouteHandlerEntry {
305 pattern: String,
306 handler:
307 Arc<dyn Fn(crate::protocol::WebSocketRoute) -> WebSocketRouteHandlerFuture + Send + Sync>,
308}
309
310/// Storage for a single route handler
311#[derive(Clone)]
312struct RouteHandlerEntry {
313 pattern: String,
314 handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
315}
316
317/// Download event handler
318type DownloadHandler = Arc<dyn Fn(Download) -> DownloadHandlerFuture + Send + Sync>;
319
320/// Dialog event handler
321type DialogHandler = Arc<dyn Fn(Dialog) -> DialogHandlerFuture + Send + Sync>;
322
323/// Request event handler
324type RequestHandler = Arc<dyn Fn(Request) -> RequestHandlerFuture + Send + Sync>;
325
326/// Response event handler
327type ResponseHandler = Arc<dyn Fn(ResponseObject) -> ResponseHandlerFuture + Send + Sync>;
328
329/// WebSocket event handler
330type WebSocketHandler = Arc<dyn Fn(WebSocket) -> WebSocketHandlerFuture + Send + Sync>;
331
332/// Type alias for boxed console handler future
333type ConsoleHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
334
335/// Console event handler
336type ConsoleHandler =
337 Arc<dyn Fn(crate::protocol::ConsoleMessage) -> ConsoleHandlerFuture + Send + Sync>;
338
339/// Type alias for boxed filechooser handler future
340type FileChooserHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
341
342/// FileChooser event handler
343type FileChooserHandler =
344 Arc<dyn Fn(crate::protocol::FileChooser) -> FileChooserHandlerFuture + Send + Sync>;
345
346/// Type alias for boxed close handler future
347type CloseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
348
349/// close event handler (no arguments)
350type CloseHandler = Arc<dyn Fn() -> CloseHandlerFuture + Send + Sync>;
351
352/// Type alias for boxed load handler future
353type LoadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
354
355/// load event handler (no arguments)
356type LoadHandler = Arc<dyn Fn() -> LoadHandlerFuture + Send + Sync>;
357
358/// Type alias for boxed crash handler future
359type CrashHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
360
361/// crash event handler (no arguments)
362type CrashHandler = Arc<dyn Fn() -> CrashHandlerFuture + Send + Sync>;
363
364/// Type alias for boxed pageError handler future
365type PageErrorHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
366
367/// pageError event handler — receives the error message as a String
368type PageErrorHandler = Arc<dyn Fn(String) -> PageErrorHandlerFuture + Send + Sync>;
369
370/// Type alias for boxed popup handler future
371type PopupHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
372
373/// popup event handler — receives the new popup Page
374type PopupHandler = Arc<dyn Fn(Page) -> PopupHandlerFuture + Send + Sync>;
375
376/// Type alias for boxed frameAttached/Detached/Navigated handler future
377type FrameEventHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
378
379/// frameAttached event handler
380type FrameAttachedHandler =
381 Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
382
383/// frameDetached event handler
384type FrameDetachedHandler =
385 Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
386
387/// frameNavigated event handler
388type FrameNavigatedHandler =
389 Arc<dyn Fn(crate::protocol::Frame) -> FrameEventHandlerFuture + Send + Sync>;
390
391/// Type alias for boxed worker handler future
392type WorkerHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
393
394/// worker event handler — receives the new Worker
395type WorkerHandler = Arc<dyn Fn(crate::protocol::Worker) -> WorkerHandlerFuture + Send + Sync>;
396
397/// Type alias for boxed page-level binding callback future
398type PageBindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
399
400/// Page-level binding callback: receives deserialized JS args, returns a JSON value
401type PageBindingCallback =
402 Arc<dyn Fn(Vec<serde_json::Value>) -> PageBindingCallbackFuture + Send + Sync>;
403
404/// Type alias for boxed locator handler future
405type LocatorHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
406
407/// Locator handler callback: receives the matching Locator
408type LocatorHandlerFn = Arc<dyn Fn(crate::protocol::Locator) -> LocatorHandlerFuture + Send + Sync>;
409
410/// Entry in the locator handler registry
411struct LocatorHandlerEntry {
412 uid: u32,
413 selector: String,
414 handler: LocatorHandlerFn,
415 /// Remaining invocations; `None` means unlimited.
416 times_remaining: Option<u32>,
417}
418
419impl Page {
420 /// Creates a new Page from protocol initialization
421 ///
422 /// This is called by the object factory when the server sends a `__create__` message
423 /// for a Page object.
424 ///
425 /// # Arguments
426 ///
427 /// * `parent` - The parent BrowserContext object
428 /// * `type_name` - The protocol type name ("Page")
429 /// * `guid` - The unique identifier for this page
430 /// * `initializer` - The initialization data from the server
431 ///
432 /// # Errors
433 ///
434 /// Returns error if initializer is malformed
435 pub fn new(
436 parent: Arc<dyn ChannelOwner>,
437 type_name: String,
438 guid: Arc<str>,
439 initializer: Value,
440 ) -> Result<Self> {
441 // Extract mainFrame GUID from initializer
442 let main_frame_guid: Arc<str> =
443 Arc::from(initializer["mainFrame"]["guid"].as_str().ok_or_else(|| {
444 crate::error::Error::ProtocolError(
445 "Page initializer missing 'mainFrame.guid' field".to_string(),
446 )
447 })?);
448
449 // Check the parent BrowserContext's initializer for record_video before
450 // moving `parent` into ChannelOwnerImpl. The Playwright server delivers
451 // the video artifact GUID directly in the Page initializer's "video" field.
452 let has_video = parent
453 .initializer()
454 .get("options")
455 .and_then(|opts| opts.get("recordVideo"))
456 .is_some();
457
458 let video_artifact_guid: Option<String> = initializer
459 .get("video")
460 .and_then(|v| v.get("guid"))
461 .and_then(|v| v.as_str())
462 .map(|s| s.to_string());
463
464 let base = ChannelOwnerImpl::new(
465 ParentOrConnection::Parent(parent),
466 type_name,
467 guid,
468 initializer,
469 );
470
471 // Initialize URL to about:blank
472 let url = Arc::new(RwLock::new("about:blank".to_string()));
473
474 // Initialize empty route handlers
475 let route_handlers = Arc::new(Mutex::new(Vec::new()));
476
477 // Initialize empty event handlers
478 let download_handlers = Arc::new(Mutex::new(Vec::new()));
479 let dialog_handlers = Arc::new(Mutex::new(Vec::new()));
480 let websocket_handlers = Arc::new(Mutex::new(Vec::new()));
481 let ws_route_handlers = Arc::new(Mutex::new(Vec::new()));
482
483 // Initialize cached main frame as empty (will be populated on first access)
484 let cached_main_frame = Arc::new(Mutex::new(None));
485
486 // Extract viewport from initializer (may be null for no_viewport contexts)
487 let initial_viewport: Option<Viewport> =
488 base.initializer().get("viewportSize").and_then(|v| {
489 if v.is_null() {
490 None
491 } else {
492 serde_json::from_value(v.clone()).ok()
493 }
494 });
495 let viewport = Arc::new(RwLock::new(initial_viewport));
496
497 let video = if has_video {
498 let v = crate::protocol::Video::new();
499 // Resolve the artifact from the initializer-provided GUID.
500 if let Some(artifact_guid) = video_artifact_guid {
501 let connection = base.connection();
502 let v_clone = v.clone();
503 tokio::spawn(async move {
504 match connection.get_object(&artifact_guid).await {
505 Ok(artifact_arc) => v_clone.set_artifact(artifact_arc),
506 Err(e) => tracing::warn!(
507 "Failed to resolve video artifact {} from initializer: {}",
508 artifact_guid,
509 e
510 ),
511 }
512 });
513 }
514 Some(v)
515 } else {
516 None
517 };
518
519 Ok(Self {
520 base,
521 url,
522 main_frame_guid,
523 cached_main_frame,
524 route_handlers,
525 download_handlers,
526 dialog_handlers,
527 request_handlers: Default::default(),
528 request_finished_handlers: Default::default(),
529 request_failed_handlers: Default::default(),
530 response_handlers: Default::default(),
531 websocket_handlers,
532 ws_route_handlers,
533 viewport,
534 is_closed: Arc::new(AtomicBool::new(false)),
535 default_timeout_ms: Arc::new(AtomicU64::new(crate::DEFAULT_TIMEOUT_MS.to_bits())),
536 default_navigation_timeout_ms: Arc::new(AtomicU64::new(
537 crate::DEFAULT_TIMEOUT_MS.to_bits(),
538 )),
539 binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
540 console_handlers: Arc::new(Mutex::new(Vec::new())),
541 filechooser_handlers: Arc::new(Mutex::new(Vec::new())),
542 filechooser_waiters: Arc::new(Mutex::new(Vec::new())),
543 popup_waiters: Arc::new(Mutex::new(Vec::new())),
544 download_waiters: Arc::new(Mutex::new(Vec::new())),
545 response_waiters: Arc::new(Mutex::new(Vec::new())),
546 request_waiters: Arc::new(Mutex::new(Vec::new())),
547 console_waiters: Arc::new(Mutex::new(Vec::new())),
548 close_handlers: Arc::new(Mutex::new(Vec::new())),
549 load_handlers: Arc::new(Mutex::new(Vec::new())),
550 crash_handlers: Arc::new(Mutex::new(Vec::new())),
551 pageerror_handlers: Arc::new(Mutex::new(Vec::new())),
552 popup_handlers: Arc::new(Mutex::new(Vec::new())),
553 frameattached_handlers: Arc::new(Mutex::new(Vec::new())),
554 framedetached_handlers: Arc::new(Mutex::new(Vec::new())),
555 framenavigated_handlers: Arc::new(Mutex::new(Vec::new())),
556 worker_handlers: Arc::new(Mutex::new(Vec::new())),
557 close_waiters: Arc::new(Mutex::new(Vec::new())),
558 load_waiters: Arc::new(Mutex::new(Vec::new())),
559 crash_waiters: Arc::new(Mutex::new(Vec::new())),
560 pageerror_waiters: Arc::new(Mutex::new(Vec::new())),
561 frameattached_waiters: Arc::new(Mutex::new(Vec::new())),
562 framedetached_waiters: Arc::new(Mutex::new(Vec::new())),
563 framenavigated_waiters: Arc::new(Mutex::new(Vec::new())),
564 worker_waiters: Arc::new(Mutex::new(Vec::new())),
565 console_messages_log: Arc::new(Mutex::new(Vec::new())),
566 page_errors_log: Arc::new(Mutex::new(Vec::new())),
567 workers_list: Arc::new(Mutex::new(Vec::new())),
568 video,
569 locator_handlers: Arc::new(Mutex::new(Vec::new())),
570 })
571 }
572
573 /// Returns the channel for sending protocol messages
574 ///
575 /// Used internally for sending RPC calls to the page.
576 fn channel(&self) -> &Channel {
577 self.base.channel()
578 }
579
580 /// Returns the main frame of the page.
581 ///
582 /// The main frame is where navigation and DOM operations actually happen.
583 ///
584 /// This method also wires up the back-reference from the frame to the page so that
585 /// `frame.page()`, `frame.locator()`, and `frame.get_by_*()` work correctly.
586 pub async fn main_frame(&self) -> Result<crate::protocol::Frame> {
587 // Get and downcast the Frame object from the connection's object registry
588 let frame: crate::protocol::Frame = self
589 .connection()
590 .get_typed::<crate::protocol::Frame>(&self.main_frame_guid)
591 .await?;
592
593 // Wire up the back-reference so frame.page() / frame.locator() work.
594 // This is safe to call multiple times (subsequent calls are no-ops once set).
595 frame.set_page(self.clone());
596
597 // Cache the frame for synchronous access in url()
598 if let Ok(mut cached) = self.cached_main_frame.lock() {
599 *cached = Some(frame.clone());
600 }
601
602 Ok(frame)
603 }
604
605 /// Returns the current URL of the page.
606 ///
607 /// This returns the last committed URL, including hash fragments from anchor navigation.
608 /// Initially, pages are at "about:blank".
609 ///
610 /// See: <https://playwright.dev/docs/api/class-page#page-url>
611 pub fn url(&self) -> String {
612 // Try to get URL from the cached main frame (source of truth for navigation including hashes)
613 if let Ok(cached) = self.cached_main_frame.lock()
614 && let Some(frame) = cached.as_ref()
615 {
616 return frame.url();
617 }
618
619 // Fallback to cached URL if frame not yet loaded
620 self.url.read().unwrap().clone()
621 }
622
623 /// Closes the page.
624 ///
625 /// This is a graceful operation that sends a close command to the page
626 /// and waits for it to shut down properly.
627 ///
628 /// # Errors
629 ///
630 /// Returns error if:
631 /// - Page has already been closed
632 /// - Communication with browser process fails
633 ///
634 /// See: <https://playwright.dev/docs/api/class-page#page-close>
635 pub async fn close(&self) -> Result<()> {
636 // Send close RPC to server
637 let result = self
638 .channel()
639 .send_no_result("close", serde_json::json!({}))
640 .await;
641 // Mark as closed regardless of error (best-effort)
642 self.is_closed.store(true, Ordering::Relaxed);
643 result
644 }
645
646 /// Returns whether the page has been closed.
647 ///
648 /// Returns `true` after `close()` has been called on this page, or after the
649 /// page receives a close event from the server (e.g. when the browser context
650 /// is closed).
651 ///
652 /// See: <https://playwright.dev/docs/api/class-page#page-is-closed>
653 pub fn is_closed(&self) -> bool {
654 self.is_closed.load(Ordering::Relaxed)
655 }
656
657 /// Returns all console messages received so far on this page.
658 ///
659 /// Messages are accumulated in order as they arrive via the `console` event.
660 /// Each call returns a snapshot; new messages arriving concurrently may or may not
661 /// be included depending on timing.
662 ///
663 /// See: <https://playwright.dev/docs/api/class-page#page-console-messages>
664 pub fn console_messages(&self) -> Vec<crate::protocol::ConsoleMessage> {
665 self.console_messages_log.lock().unwrap().clone()
666 }
667
668 /// Returns all uncaught JavaScript error messages received so far on this page.
669 ///
670 /// Errors are accumulated in order as they arrive via the `pageError` event.
671 /// Each string is the `.message` field of the thrown `Error`.
672 pub fn page_errors(&self) -> Vec<String> {
673 self.page_errors_log.lock().unwrap().clone()
674 }
675
676 /// Returns the page that opened this popup, or `None` if this page was not opened
677 /// by another page.
678 ///
679 /// The opener is available from the page's initializer — it is the page that called
680 /// `window.open()` or triggered a link with `target="_blank"`. Returns `None` for
681 /// top-level pages that were not opened as popups.
682 ///
683 /// # Errors
684 ///
685 /// Returns error if the opener page GUID is present in the initializer but the
686 /// object is not found in the connection registry.
687 ///
688 /// See: <https://playwright.dev/docs/api/class-page#page-opener>
689 pub async fn opener(&self) -> Result<Option<Page>> {
690 // The opener guid is stored in the page initializer as {"opener": {"guid": "..."}}.
691 // It is set when the page is created as a popup; absent for non-popup pages.
692 let opener_guid = self
693 .base
694 .initializer()
695 .get("opener")
696 .and_then(|v| v.get("guid"))
697 .and_then(|v| v.as_str())
698 .map(|s| s.to_string());
699
700 match opener_guid {
701 None => Ok(None),
702 Some(guid) => {
703 let page = self.connection().get_typed::<Page>(&guid).await?;
704 Ok(Some(page))
705 }
706 }
707 }
708
709 /// Returns all active web workers belonging to this page.
710 ///
711 /// Workers are tracked as they are created (`worker` event) and this method
712 /// returns a snapshot of the current list.
713 ///
714 /// See: <https://playwright.dev/docs/api/class-page#page-workers>
715 pub fn workers(&self) -> Vec<Worker> {
716 self.workers_list.lock().unwrap().clone()
717 }
718
719 /// Sets the default timeout for all operations on this page.
720 ///
721 /// The timeout applies to actions such as `click`, `fill`, `locator.wait_for`, etc.
722 /// Pass `0` to disable timeouts.
723 ///
724 /// This stores the value locally so that subsequent action calls use it when
725 /// no explicit timeout is provided, and also notifies the Playwright server
726 /// so it can apply the same default on its side.
727 ///
728 /// # Arguments
729 ///
730 /// * `timeout` - Timeout in milliseconds
731 ///
732 /// See: <https://playwright.dev/docs/api/class-page#page-set-default-timeout>
733 pub async fn set_default_timeout(&self, timeout: f64) {
734 self.default_timeout_ms
735 .store(timeout.to_bits(), Ordering::Relaxed);
736 set_timeout_and_notify(self.channel(), "setDefaultTimeoutNoReply", timeout).await;
737 }
738
739 /// Sets the default timeout for navigation operations on this page.
740 ///
741 /// The timeout applies to navigation actions such as `goto`, `reload`,
742 /// `go_back`, and `go_forward`. Pass `0` to disable timeouts.
743 ///
744 /// # Arguments
745 ///
746 /// * `timeout` - Timeout in milliseconds
747 ///
748 /// See: <https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout>
749 pub async fn set_default_navigation_timeout(&self, timeout: f64) {
750 self.default_navigation_timeout_ms
751 .store(timeout.to_bits(), Ordering::Relaxed);
752 set_timeout_and_notify(
753 self.channel(),
754 "setDefaultNavigationTimeoutNoReply",
755 timeout,
756 )
757 .await;
758 }
759
760 /// Returns the current default action timeout in milliseconds.
761 pub fn default_timeout_ms(&self) -> f64 {
762 f64::from_bits(self.default_timeout_ms.load(Ordering::Relaxed))
763 }
764
765 /// Returns the current default navigation timeout in milliseconds.
766 pub fn default_navigation_timeout_ms(&self) -> f64 {
767 f64::from_bits(self.default_navigation_timeout_ms.load(Ordering::Relaxed))
768 }
769
770 /// Returns GotoOptions with the navigation timeout filled in if not already set.
771 ///
772 /// Used internally to ensure the page's configured default navigation timeout
773 /// is used when the caller does not provide an explicit timeout.
774 fn with_navigation_timeout(&self, options: Option<GotoOptions>) -> GotoOptions {
775 let nav_timeout = self.default_navigation_timeout_ms();
776 match options {
777 Some(opts) if opts.timeout.is_some() => opts,
778 Some(mut opts) => {
779 opts.timeout = Some(std::time::Duration::from_millis(nav_timeout as u64));
780 opts
781 }
782 None => GotoOptions {
783 timeout: Some(std::time::Duration::from_millis(nav_timeout as u64)),
784 wait_until: None,
785 },
786 }
787 }
788
789 /// Returns all frames in the page, including the main frame.
790 ///
791 /// Currently returns only the main (top-level) frame. Iframe enumeration
792 /// is not yet implemented and will be added in a future release.
793 ///
794 /// # Errors
795 ///
796 /// Returns error if:
797 /// - Page has been closed
798 /// - Communication with browser process fails
799 ///
800 /// See: <https://playwright.dev/docs/api/class-page#page-frames>
801 pub async fn frames(&self) -> Result<Vec<crate::protocol::Frame>> {
802 // Start with the main frame
803 let main = self.main_frame().await?;
804 Ok(vec![main])
805 }
806
807 /// Navigates to the specified URL.
808 ///
809 /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
810 /// about:blank). This matches Playwright's behavior across all language bindings.
811 ///
812 /// # Arguments
813 ///
814 /// * `url` - The URL to navigate to
815 /// * `options` - Optional navigation options (timeout, wait_until)
816 ///
817 /// # Errors
818 ///
819 /// Returns error if:
820 /// - URL is invalid
821 /// - Navigation timeout (default 30s)
822 /// - Network error
823 ///
824 /// See: <https://playwright.dev/docs/api/class-page#page-goto>
825 pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
826 // Inject the page-level navigation timeout when no explicit timeout is given
827 let options = self.with_navigation_timeout(options);
828
829 // Delegate to main frame
830 let frame = self.main_frame().await.map_err(|e| match e {
831 Error::TargetClosed { context, .. } => Error::TargetClosed {
832 target_type: "Page".to_string(),
833 context,
834 },
835 other => other,
836 })?;
837
838 let response = frame.goto(url, Some(options)).await.map_err(|e| match e {
839 Error::TargetClosed { context, .. } => Error::TargetClosed {
840 target_type: "Page".to_string(),
841 context,
842 },
843 other => other,
844 })?;
845
846 // Update the page's URL if we got a response
847 if let Some(ref resp) = response
848 && let Ok(mut page_url) = self.url.write()
849 {
850 *page_url = resp.url().to_string();
851 }
852
853 Ok(response)
854 }
855
856 /// Returns the browser context that the page belongs to.
857 pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
858 downcast_parent::<crate::protocol::BrowserContext>(self)
859 .ok_or_else(|| Error::ProtocolError("Page parent is not a BrowserContext".to_string()))
860 }
861
862 /// Returns the Clock object for this page's browser context.
863 ///
864 /// This is a convenience accessor that delegates to the parent context's clock.
865 /// All clock RPCs are sent on the BrowserContext channel regardless of whether
866 /// the Clock is obtained via `page.clock()` or `context.clock()`.
867 ///
868 /// # Errors
869 ///
870 /// Returns error if the page's parent is not a BrowserContext.
871 ///
872 /// See: <https://playwright.dev/docs/api/class-clock>
873 pub fn clock(&self) -> Result<crate::protocol::clock::Clock> {
874 Ok(self.context()?.clock())
875 }
876
877 /// Returns the `Video` object associated with this page, if video recording is enabled.
878 ///
879 /// Returns `Some(Video)` when the browser context was created with the `record_video`
880 /// option; returns `None` otherwise.
881 ///
882 /// The `Video` shell is created eagerly. The underlying recording artifact is wired
883 /// up when the Playwright server fires the internal `"video"` event (which typically
884 /// happens when the page is first navigated). Calling [`crate::protocol::Video::save_as`] or
885 /// [`crate::protocol::Video::path`] before the artifact arrives returns an error; close the page
886 /// first to guarantee the artifact is ready.
887 ///
888 /// See: <https://playwright.dev/docs/api/class-page#page-video>
889 pub fn video(&self) -> Option<crate::protocol::Video> {
890 self.video.clone()
891 }
892
893 /// Pauses script execution.
894 ///
895 /// Playwright will stop executing the script and wait for the user to either press
896 /// "Resume" in the page overlay or in the debugger.
897 ///
898 /// See: <https://playwright.dev/docs/api/class-page#page-pause>
899 pub async fn pause(&self) -> Result<()> {
900 self.context()?.pause().await
901 }
902
903 /// Returns the page's title.
904 ///
905 /// See: <https://playwright.dev/docs/api/class-page#page-title>
906 pub async fn title(&self) -> Result<String> {
907 // Delegate to main frame
908 let frame = self.main_frame().await?;
909 frame.title().await
910 }
911
912 /// Returns the full HTML content of the page, including the DOCTYPE.
913 ///
914 /// This method retrieves the complete HTML markup of the page,
915 /// including the doctype declaration and all DOM elements.
916 ///
917 /// See: <https://playwright.dev/docs/api/class-page#page-content>
918 pub async fn content(&self) -> Result<String> {
919 // Delegate to main frame
920 let frame = self.main_frame().await?;
921 frame.content().await
922 }
923
924 /// Sets the content of the page.
925 ///
926 /// See: <https://playwright.dev/docs/api/class-page#page-set-content>
927 pub async fn set_content(&self, html: &str, options: Option<GotoOptions>) -> Result<()> {
928 let frame = self.main_frame().await?;
929 frame.set_content(html, options).await
930 }
931
932 /// Waits for the required load state to be reached.
933 ///
934 /// This resolves when the page reaches a required load state, `load` by default.
935 /// The navigation must have been committed when this method is called. If the current
936 /// document has already reached the required state, resolves immediately.
937 ///
938 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-load-state>
939 pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
940 let frame = self.main_frame().await?;
941 frame.wait_for_load_state(state).await
942 }
943
944 /// Waits for the main frame to navigate to a URL matching the given string or glob pattern.
945 ///
946 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-url>
947 pub async fn wait_for_url(&self, url: &str, options: Option<GotoOptions>) -> Result<()> {
948 let frame = self.main_frame().await?;
949 frame.wait_for_url(url, options).await
950 }
951
952 /// Creates a locator for finding elements on the page.
953 ///
954 /// Locators are the central piece of Playwright's auto-waiting and retry-ability.
955 /// They don't execute queries until an action is performed.
956 ///
957 /// # Arguments
958 ///
959 /// * `selector` - CSS selector or other locating strategy
960 ///
961 /// See: <https://playwright.dev/docs/api/class-page#page-locator>
962 pub async fn locator(&self, selector: &str) -> crate::protocol::Locator {
963 // Get the main frame
964 let frame = self.main_frame().await.expect("Main frame should exist");
965
966 crate::protocol::Locator::new(Arc::new(frame), selector.to_string(), self.clone())
967 }
968
969 /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) for an iframe on this page.
970 ///
971 /// The `selector` identifies the iframe element (e.g., `"iframe[name='content']"`).
972 ///
973 /// See: <https://playwright.dev/docs/api/class-page#page-frame-locator>
974 pub async fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
975 let frame = self.main_frame().await.expect("Main frame should exist");
976 crate::protocol::FrameLocator::new(Arc::new(frame), selector.to_string(), self.clone())
977 }
978
979 /// Returns a locator that matches elements containing the given text.
980 ///
981 /// By default, matching is case-insensitive and searches for a substring.
982 /// Set `exact` to `true` for case-sensitive exact matching.
983 ///
984 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-text>
985 pub async fn get_by_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
986 self.locator(&crate::protocol::locator::get_by_text_selector(text, exact))
987 .await
988 }
989
990 /// Returns a locator that matches elements by their associated label text.
991 ///
992 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-label>
993 pub async fn get_by_label(&self, text: &str, exact: bool) -> crate::protocol::Locator {
994 self.locator(&crate::protocol::locator::get_by_label_selector(
995 text, exact,
996 ))
997 .await
998 }
999
1000 /// Returns a locator that matches elements by their placeholder text.
1001 ///
1002 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-placeholder>
1003 pub async fn get_by_placeholder(&self, text: &str, exact: bool) -> crate::protocol::Locator {
1004 self.locator(&crate::protocol::locator::get_by_placeholder_selector(
1005 text, exact,
1006 ))
1007 .await
1008 }
1009
1010 /// Returns a locator that matches elements by their alt text.
1011 ///
1012 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-alt-text>
1013 pub async fn get_by_alt_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
1014 self.locator(&crate::protocol::locator::get_by_alt_text_selector(
1015 text, exact,
1016 ))
1017 .await
1018 }
1019
1020 /// Returns a locator that matches elements by their title attribute.
1021 ///
1022 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-title>
1023 pub async fn get_by_title(&self, text: &str, exact: bool) -> crate::protocol::Locator {
1024 self.locator(&crate::protocol::locator::get_by_title_selector(
1025 text, exact,
1026 ))
1027 .await
1028 }
1029
1030 /// Returns a locator that matches elements by their test ID attribute.
1031 ///
1032 /// By default, uses the `data-testid` attribute. Call
1033 /// [`playwright.selectors().set_test_id_attribute()`](crate::protocol::Selectors::set_test_id_attribute)
1034 /// to change the attribute name.
1035 ///
1036 /// Always uses exact matching (case-sensitive).
1037 ///
1038 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-test-id>
1039 pub async fn get_by_test_id(&self, test_id: &str) -> crate::protocol::Locator {
1040 let attr = self.connection().selectors().test_id_attribute();
1041 self.locator(&crate::protocol::locator::get_by_test_id_selector_with_attr(test_id, &attr))
1042 .await
1043 }
1044
1045 /// Returns a locator that matches elements by their ARIA role.
1046 ///
1047 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
1048 pub async fn get_by_role(
1049 &self,
1050 role: crate::protocol::locator::AriaRole,
1051 options: Option<crate::protocol::locator::GetByRoleOptions>,
1052 ) -> crate::protocol::Locator {
1053 self.locator(&crate::protocol::locator::get_by_role_selector(
1054 role, options,
1055 ))
1056 .await
1057 }
1058
1059 /// Returns the keyboard instance for low-level keyboard control.
1060 ///
1061 /// See: <https://playwright.dev/docs/api/class-page#page-keyboard>
1062 pub fn keyboard(&self) -> crate::protocol::Keyboard {
1063 crate::protocol::Keyboard::new(self.clone())
1064 }
1065
1066 /// Returns the mouse instance for low-level mouse control.
1067 ///
1068 /// See: <https://playwright.dev/docs/api/class-page#page-mouse>
1069 pub fn mouse(&self) -> crate::protocol::Mouse {
1070 crate::protocol::Mouse::new(self.clone())
1071 }
1072
1073 // Internal keyboard methods (called by Keyboard struct)
1074
1075 pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
1076 self.channel()
1077 .send_no_result(
1078 "keyboardDown",
1079 serde_json::json!({
1080 "key": key
1081 }),
1082 )
1083 .await
1084 }
1085
1086 pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
1087 self.channel()
1088 .send_no_result(
1089 "keyboardUp",
1090 serde_json::json!({
1091 "key": key
1092 }),
1093 )
1094 .await
1095 }
1096
1097 pub(crate) async fn keyboard_press(
1098 &self,
1099 key: &str,
1100 options: Option<crate::protocol::KeyboardOptions>,
1101 ) -> Result<()> {
1102 let mut params = serde_json::json!({
1103 "key": key
1104 });
1105
1106 if let Some(opts) = options {
1107 let opts_json = opts.to_json();
1108 if let Some(obj) = params.as_object_mut()
1109 && let Some(opts_obj) = opts_json.as_object()
1110 {
1111 obj.extend(opts_obj.clone());
1112 }
1113 }
1114
1115 self.channel().send_no_result("keyboardPress", params).await
1116 }
1117
1118 pub(crate) async fn keyboard_type(
1119 &self,
1120 text: &str,
1121 options: Option<crate::protocol::KeyboardOptions>,
1122 ) -> Result<()> {
1123 let mut params = serde_json::json!({
1124 "text": text
1125 });
1126
1127 if let Some(opts) = options {
1128 let opts_json = opts.to_json();
1129 if let Some(obj) = params.as_object_mut()
1130 && let Some(opts_obj) = opts_json.as_object()
1131 {
1132 obj.extend(opts_obj.clone());
1133 }
1134 }
1135
1136 self.channel().send_no_result("keyboardType", params).await
1137 }
1138
1139 pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
1140 self.channel()
1141 .send_no_result(
1142 "keyboardInsertText",
1143 serde_json::json!({
1144 "text": text
1145 }),
1146 )
1147 .await
1148 }
1149
1150 // Internal mouse methods (called by Mouse struct)
1151
1152 pub(crate) async fn mouse_move(
1153 &self,
1154 x: i32,
1155 y: i32,
1156 options: Option<crate::protocol::MouseOptions>,
1157 ) -> Result<()> {
1158 let mut params = serde_json::json!({
1159 "x": x,
1160 "y": y
1161 });
1162
1163 if let Some(opts) = options {
1164 let opts_json = opts.to_json();
1165 if let Some(obj) = params.as_object_mut()
1166 && let Some(opts_obj) = opts_json.as_object()
1167 {
1168 obj.extend(opts_obj.clone());
1169 }
1170 }
1171
1172 self.channel().send_no_result("mouseMove", params).await
1173 }
1174
1175 pub(crate) async fn mouse_click(
1176 &self,
1177 x: i32,
1178 y: i32,
1179 options: Option<crate::protocol::MouseOptions>,
1180 ) -> Result<()> {
1181 let mut params = serde_json::json!({
1182 "x": x,
1183 "y": y
1184 });
1185
1186 if let Some(opts) = options {
1187 let opts_json = opts.to_json();
1188 if let Some(obj) = params.as_object_mut()
1189 && let Some(opts_obj) = opts_json.as_object()
1190 {
1191 obj.extend(opts_obj.clone());
1192 }
1193 }
1194
1195 self.channel().send_no_result("mouseClick", params).await
1196 }
1197
1198 pub(crate) async fn mouse_dblclick(
1199 &self,
1200 x: i32,
1201 y: i32,
1202 options: Option<crate::protocol::MouseOptions>,
1203 ) -> Result<()> {
1204 let mut params = serde_json::json!({
1205 "x": x,
1206 "y": y,
1207 "clickCount": 2
1208 });
1209
1210 if let Some(opts) = options {
1211 let opts_json = opts.to_json();
1212 if let Some(obj) = params.as_object_mut()
1213 && let Some(opts_obj) = opts_json.as_object()
1214 {
1215 obj.extend(opts_obj.clone());
1216 }
1217 }
1218
1219 self.channel().send_no_result("mouseClick", params).await
1220 }
1221
1222 pub(crate) async fn mouse_down(
1223 &self,
1224 options: Option<crate::protocol::MouseOptions>,
1225 ) -> Result<()> {
1226 let mut params = serde_json::json!({});
1227
1228 if let Some(opts) = options {
1229 let opts_json = opts.to_json();
1230 if let Some(obj) = params.as_object_mut()
1231 && let Some(opts_obj) = opts_json.as_object()
1232 {
1233 obj.extend(opts_obj.clone());
1234 }
1235 }
1236
1237 self.channel().send_no_result("mouseDown", params).await
1238 }
1239
1240 pub(crate) async fn mouse_up(
1241 &self,
1242 options: Option<crate::protocol::MouseOptions>,
1243 ) -> Result<()> {
1244 let mut params = serde_json::json!({});
1245
1246 if let Some(opts) = options {
1247 let opts_json = opts.to_json();
1248 if let Some(obj) = params.as_object_mut()
1249 && let Some(opts_obj) = opts_json.as_object()
1250 {
1251 obj.extend(opts_obj.clone());
1252 }
1253 }
1254
1255 self.channel().send_no_result("mouseUp", params).await
1256 }
1257
1258 pub(crate) async fn mouse_wheel(&self, delta_x: i32, delta_y: i32) -> Result<()> {
1259 self.channel()
1260 .send_no_result(
1261 "mouseWheel",
1262 serde_json::json!({
1263 "deltaX": delta_x,
1264 "deltaY": delta_y
1265 }),
1266 )
1267 .await
1268 }
1269
1270 // Internal touchscreen method (called by Touchscreen struct)
1271
1272 pub(crate) async fn touchscreen_tap(&self, x: f64, y: f64) -> Result<()> {
1273 self.channel()
1274 .send_no_result(
1275 "touchscreenTap",
1276 serde_json::json!({
1277 "x": x,
1278 "y": y
1279 }),
1280 )
1281 .await
1282 }
1283
1284 /// Returns the touchscreen instance for low-level touch input simulation.
1285 ///
1286 /// Requires a touch-enabled browser context (`has_touch: true` in
1287 /// [`BrowserContextOptions`](crate::protocol::browser_context::BrowserContext)).
1288 ///
1289 /// See: <https://playwright.dev/docs/api/class-page#page-touchscreen>
1290 pub fn touchscreen(&self) -> crate::protocol::Touchscreen {
1291 crate::protocol::Touchscreen::new(self.clone())
1292 }
1293
1294 /// Performs a drag from source selector to target selector.
1295 ///
1296 /// This is the page-level equivalent of `Locator::drag_to()`. It resolves
1297 /// both selectors in the main frame and performs the drag.
1298 ///
1299 /// # Arguments
1300 ///
1301 /// * `source` - A CSS selector for the element to drag from
1302 /// * `target` - A CSS selector for the element to drop onto
1303 /// * `options` - Optional drag options (positions, force, timeout, trial)
1304 ///
1305 /// # Errors
1306 ///
1307 /// Returns error if either selector does not resolve to an element, the
1308 /// drag action times out, or the page has been closed.
1309 ///
1310 /// See: <https://playwright.dev/docs/api/class-page#page-drag-and-drop>
1311 pub async fn drag_and_drop(
1312 &self,
1313 source: &str,
1314 target: &str,
1315 options: Option<crate::protocol::DragToOptions>,
1316 ) -> Result<()> {
1317 let frame = self.main_frame().await?;
1318 frame.locator_drag_to(source, target, options).await
1319 }
1320
1321 /// Reloads the current page.
1322 ///
1323 /// # Arguments
1324 ///
1325 /// * `options` - Optional reload options (timeout, wait_until)
1326 ///
1327 /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
1328 /// about:blank). This matches Playwright's behavior across all language bindings.
1329 ///
1330 /// See: <https://playwright.dev/docs/api/class-page#page-reload>
1331 pub async fn reload(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1332 self.navigate_history("reload", options).await
1333 }
1334
1335 /// Navigates to the previous page in history.
1336 ///
1337 /// Returns the main resource response. In case of multiple server redirects, the navigation
1338 /// will resolve with the response of the last redirect. If can not go back, returns `None`.
1339 ///
1340 /// See: <https://playwright.dev/docs/api/class-page#page-go-back>
1341 pub async fn go_back(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1342 self.navigate_history("goBack", options).await
1343 }
1344
1345 /// Navigates to the next page in history.
1346 ///
1347 /// Returns the main resource response. In case of multiple server redirects, the navigation
1348 /// will resolve with the response of the last redirect. If can not go forward, returns `None`.
1349 ///
1350 /// See: <https://playwright.dev/docs/api/class-page#page-go-forward>
1351 pub async fn go_forward(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
1352 self.navigate_history("goForward", options).await
1353 }
1354
1355 /// Shared implementation for reload, go_back and go_forward.
1356 async fn navigate_history(
1357 &self,
1358 method: &str,
1359 options: Option<GotoOptions>,
1360 ) -> Result<Option<Response>> {
1361 // Inject the page-level navigation timeout when no explicit timeout is given
1362 let opts = self.with_navigation_timeout(options);
1363 let mut params = serde_json::json!({});
1364
1365 // opts.timeout is always Some(...) because with_navigation_timeout guarantees it
1366 if let Some(timeout) = opts.timeout {
1367 params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
1368 } else {
1369 params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
1370 }
1371 if let Some(wait_until) = opts.wait_until {
1372 params["waitUntil"] = serde_json::json!(wait_until.as_str());
1373 }
1374
1375 #[derive(Deserialize)]
1376 struct NavigationResponse {
1377 response: Option<ResponseReference>,
1378 }
1379
1380 #[derive(Deserialize)]
1381 struct ResponseReference {
1382 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
1383 guid: Arc<str>,
1384 }
1385
1386 let result: NavigationResponse = self.channel().send(method, params).await?;
1387
1388 if let Some(response_ref) = result.response {
1389 let response_arc = {
1390 let mut attempts = 0;
1391 let max_attempts = 20;
1392 loop {
1393 match self.connection().get_object(&response_ref.guid).await {
1394 Ok(obj) => break obj,
1395 Err(_) if attempts < max_attempts => {
1396 attempts += 1;
1397 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1398 }
1399 Err(e) => return Err(e),
1400 }
1401 }
1402 };
1403
1404 let initializer = response_arc.initializer();
1405
1406 let status = initializer["status"].as_u64().ok_or_else(|| {
1407 crate::error::Error::ProtocolError("Response missing status".to_string())
1408 })? as u16;
1409
1410 let headers = initializer["headers"]
1411 .as_array()
1412 .ok_or_else(|| {
1413 crate::error::Error::ProtocolError("Response missing headers".to_string())
1414 })?
1415 .iter()
1416 .filter_map(|h| {
1417 let name = h["name"].as_str()?;
1418 let value = h["value"].as_str()?;
1419 Some((name.to_string(), value.to_string()))
1420 })
1421 .collect();
1422
1423 let response = Response::new(
1424 initializer["url"]
1425 .as_str()
1426 .ok_or_else(|| {
1427 crate::error::Error::ProtocolError("Response missing url".to_string())
1428 })?
1429 .to_string(),
1430 status,
1431 initializer["statusText"].as_str().unwrap_or("").to_string(),
1432 headers,
1433 Some(response_arc),
1434 );
1435
1436 if let Ok(mut page_url) = self.url.write() {
1437 *page_url = response.url().to_string();
1438 }
1439
1440 Ok(Some(response))
1441 } else {
1442 Ok(None)
1443 }
1444 }
1445
1446 /// Returns the first element matching the selector, or None if not found.
1447 ///
1448 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
1449 pub async fn query_selector(
1450 &self,
1451 selector: &str,
1452 ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
1453 let frame = self.main_frame().await?;
1454 frame.query_selector(selector).await
1455 }
1456
1457 /// Returns all elements matching the selector.
1458 ///
1459 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
1460 pub async fn query_selector_all(
1461 &self,
1462 selector: &str,
1463 ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
1464 let frame = self.main_frame().await?;
1465 frame.query_selector_all(selector).await
1466 }
1467
1468 /// Takes a screenshot of the page and returns the image bytes.
1469 ///
1470 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1471 pub async fn screenshot(
1472 &self,
1473 options: Option<crate::protocol::ScreenshotOptions>,
1474 ) -> Result<Vec<u8>> {
1475 let params = if let Some(opts) = options {
1476 opts.to_json()
1477 } else {
1478 // Default to PNG with required timeout
1479 serde_json::json!({
1480 "type": "png",
1481 "timeout": crate::DEFAULT_TIMEOUT_MS
1482 })
1483 };
1484
1485 #[derive(Deserialize)]
1486 struct ScreenshotResponse {
1487 binary: String,
1488 }
1489
1490 let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
1491
1492 // Decode base64 to bytes
1493 let bytes = base64::prelude::BASE64_STANDARD
1494 .decode(&response.binary)
1495 .map_err(|e| {
1496 crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
1497 })?;
1498
1499 Ok(bytes)
1500 }
1501
1502 /// Takes a screenshot and saves it to a file, also returning the bytes.
1503 ///
1504 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1505 pub async fn screenshot_to_file(
1506 &self,
1507 path: &std::path::Path,
1508 options: Option<crate::protocol::ScreenshotOptions>,
1509 ) -> Result<Vec<u8>> {
1510 // Get the screenshot bytes
1511 let bytes = self.screenshot(options).await?;
1512
1513 // Write to file
1514 tokio::fs::write(path, &bytes).await.map_err(|e| {
1515 crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
1516 })?;
1517
1518 Ok(bytes)
1519 }
1520
1521 /// Evaluates JavaScript in the page context (without return value).
1522 ///
1523 /// Executes the provided JavaScript expression or function within the page's
1524 /// context without returning a value.
1525 ///
1526 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1527 pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
1528 // Delegate to the main frame
1529 let frame = self.main_frame().await?;
1530 frame.frame_evaluate_expression(expression).await
1531 }
1532
1533 /// Evaluates JavaScript in the page context with optional arguments.
1534 ///
1535 /// Executes the provided JavaScript expression or function within the page's
1536 /// context and returns the result. The return value must be JSON-serializable.
1537 ///
1538 /// # Arguments
1539 ///
1540 /// * `expression` - JavaScript code to evaluate
1541 /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1542 ///
1543 /// # Returns
1544 ///
1545 /// The result as a `serde_json::Value`
1546 ///
1547 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1548 pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
1549 &self,
1550 expression: &str,
1551 arg: Option<&T>,
1552 ) -> Result<U> {
1553 // Delegate to the main frame
1554 let frame = self.main_frame().await?;
1555 let result = frame.evaluate(expression, arg).await?;
1556 serde_json::from_value(result).map_err(Error::from)
1557 }
1558
1559 /// Evaluates a JavaScript expression and returns the result as a String.
1560 ///
1561 /// # Arguments
1562 ///
1563 /// * `expression` - JavaScript code to evaluate
1564 ///
1565 /// # Returns
1566 ///
1567 /// The result converted to a String
1568 ///
1569 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1570 pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
1571 let frame = self.main_frame().await?;
1572 frame.frame_evaluate_expression_value(expression).await
1573 }
1574
1575 /// Registers a route handler for network interception.
1576 ///
1577 /// When a request matches the specified pattern, the handler will be called
1578 /// with a Route object that can abort, continue, or fulfill the request.
1579 ///
1580 /// # Arguments
1581 ///
1582 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
1583 /// * `handler` - Async closure that handles the route
1584 ///
1585 /// See: <https://playwright.dev/docs/api/class-page#page-route>
1586 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1587 where
1588 F: Fn(Route) -> Fut + Send + Sync + 'static,
1589 Fut: Future<Output = Result<()>> + Send + 'static,
1590 {
1591 // 1. Wrap handler in Arc with type erasure
1592 let handler =
1593 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1594
1595 // 2. Store in handlers list
1596 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1597 pattern: pattern.to_string(),
1598 handler,
1599 });
1600
1601 // 3. Enable network interception via protocol
1602 self.enable_network_interception().await?;
1603
1604 Ok(())
1605 }
1606
1607 /// Updates network interception patterns for this page
1608 async fn enable_network_interception(&self) -> Result<()> {
1609 // Collect all patterns from registered handlers
1610 // Each pattern must be an object with "glob" field
1611 let patterns: Vec<serde_json::Value> = self
1612 .route_handlers
1613 .lock()
1614 .unwrap()
1615 .iter()
1616 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1617 .collect();
1618
1619 // Send protocol command to update network interception patterns
1620 // Follows playwright-python's approach
1621 self.channel()
1622 .send_no_result(
1623 "setNetworkInterceptionPatterns",
1624 serde_json::json!({
1625 "patterns": patterns
1626 }),
1627 )
1628 .await
1629 }
1630
1631 /// Removes route handler(s) matching the given URL pattern.
1632 ///
1633 /// # Arguments
1634 ///
1635 /// * `pattern` - URL pattern to remove handlers for
1636 ///
1637 /// See: <https://playwright.dev/docs/api/class-page#page-unroute>
1638 pub async fn unroute(&self, pattern: &str) -> Result<()> {
1639 self.route_handlers
1640 .lock()
1641 .unwrap()
1642 .retain(|entry| entry.pattern != pattern);
1643 self.enable_network_interception().await
1644 }
1645
1646 /// Removes all registered route handlers.
1647 ///
1648 /// # Arguments
1649 ///
1650 /// * `behavior` - Optional behavior for in-flight handlers
1651 ///
1652 /// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
1653 pub async fn unroute_all(
1654 &self,
1655 _behavior: Option<crate::protocol::route::UnrouteBehavior>,
1656 ) -> Result<()> {
1657 self.route_handlers.lock().unwrap().clear();
1658 self.enable_network_interception().await
1659 }
1660
1661 /// Replays network requests from a HAR file recorded previously.
1662 ///
1663 /// Requests matching `options.url` (or all requests if omitted) will be
1664 /// served from the archive instead of hitting the network. Unmatched
1665 /// requests are either aborted or passed through depending on
1666 /// `options.not_found` (`"abort"` is the default).
1667 ///
1668 /// # Arguments
1669 ///
1670 /// * `har_path` - Path to the `.har` file on disk
1671 /// * `options` - Optional settings (url filter, not_found policy, update mode)
1672 ///
1673 /// # Errors
1674 ///
1675 /// Returns error if:
1676 /// - `har_path` does not exist or cannot be read by the Playwright server
1677 /// - The Playwright server fails to open the archive
1678 ///
1679 /// See: <https://playwright.dev/docs/api/class-page#page-route-from-har>
1680 pub async fn route_from_har(
1681 &self,
1682 har_path: &str,
1683 options: Option<RouteFromHarOptions>,
1684 ) -> Result<()> {
1685 let opts = options.unwrap_or_default();
1686 let not_found = opts.not_found.unwrap_or_else(|| "abort".to_string());
1687 let url_filter = opts.url.clone();
1688
1689 // Resolve to an absolute path so the Playwright server can open it
1690 // regardless of its working directory.
1691 let abs_path = std::path::Path::new(har_path).canonicalize().map_err(|e| {
1692 Error::InvalidPath(format!(
1693 "route_from_har: cannot resolve '{}': {}",
1694 har_path, e
1695 ))
1696 })?;
1697 let abs_str = abs_path.to_string_lossy().into_owned();
1698
1699 // Locate LocalUtils in the connection object registry by type name.
1700 // The Playwright server registers it with a guid like "localUtils@1"
1701 // so we scan all objects for the one with type_name "LocalUtils".
1702 let connection = self.connection();
1703 let local_utils = {
1704 let all = connection.all_objects_sync();
1705 all.into_iter()
1706 .find(|o| o.type_name() == "LocalUtils")
1707 .and_then(|o| {
1708 o.as_any()
1709 .downcast_ref::<crate::protocol::LocalUtils>()
1710 .cloned()
1711 })
1712 .ok_or_else(|| {
1713 Error::ProtocolError(
1714 "route_from_har: LocalUtils not found in connection registry".to_string(),
1715 )
1716 })?
1717 };
1718
1719 // Open the HAR archive on the server side.
1720 let har_id = local_utils.har_open(&abs_str).await?;
1721
1722 // Determine the URL pattern to intercept.
1723 let pattern = url_filter.clone().unwrap_or_else(|| "**/*".to_string());
1724
1725 // Register a route handler that performs HAR lookup for each request.
1726 let har_id_clone = har_id.clone();
1727 let local_utils_clone = local_utils.clone();
1728 let not_found_clone = not_found.clone();
1729
1730 self.route(&pattern, move |route| {
1731 let har_id = har_id_clone.clone();
1732 let local_utils = local_utils_clone.clone();
1733 let not_found = not_found_clone.clone();
1734 async move {
1735 let request = route.request();
1736 let req_url = request.url().to_string();
1737 let req_method = request.method().to_string();
1738
1739 // Build headers array as [{name, value}]
1740 let headers: Vec<serde_json::Value> = request
1741 .headers()
1742 .iter()
1743 .map(|(k, v)| serde_json::json!({"name": k, "value": v}))
1744 .collect();
1745
1746 let lookup = local_utils
1747 .har_lookup(
1748 &har_id,
1749 &req_url,
1750 &req_method,
1751 headers,
1752 None,
1753 request.is_navigation_request(),
1754 )
1755 .await;
1756
1757 match lookup {
1758 Err(e) => {
1759 tracing::warn!("har_lookup error for {}: {}", req_url, e);
1760 route.continue_(None).await
1761 }
1762 Ok(result) => match result.action.as_str() {
1763 "redirect" => {
1764 let redirect_url = result.redirect_url.unwrap_or_default();
1765 let opts = crate::protocol::ContinueOptions::builder()
1766 .url(redirect_url)
1767 .build();
1768 route.continue_(Some(opts)).await
1769 }
1770 "fulfill" => {
1771 let status = result.status.unwrap_or(200);
1772
1773 // Decode base64 body if present
1774 let body_bytes = result.body.as_deref().map(|b64| {
1775 base64::engine::general_purpose::STANDARD
1776 .decode(b64)
1777 .unwrap_or_default()
1778 });
1779
1780 // Build headers map
1781 let mut headers_map = std::collections::HashMap::new();
1782 if let Some(raw_headers) = result.headers {
1783 for h in raw_headers {
1784 if let (Some(name), Some(value)) = (
1785 h.get("name").and_then(|v| v.as_str()),
1786 h.get("value").and_then(|v| v.as_str()),
1787 ) {
1788 headers_map.insert(name.to_string(), value.to_string());
1789 }
1790 }
1791 }
1792
1793 let mut builder =
1794 crate::protocol::FulfillOptions::builder().status(status);
1795
1796 if !headers_map.is_empty() {
1797 builder = builder.headers(headers_map);
1798 }
1799
1800 if let Some(body) = body_bytes {
1801 builder = builder.body(body);
1802 }
1803
1804 route.fulfill(Some(builder.build())).await
1805 }
1806 _ => {
1807 // "fallback" or "error" or unknown
1808 if not_found == "fallback" {
1809 route.fallback(None).await
1810 } else {
1811 route.abort(None).await
1812 }
1813 }
1814 },
1815 }
1816 }
1817 })
1818 .await
1819 }
1820
1821 /// Intercepts WebSocket connections matching the given URL pattern.
1822 ///
1823 /// When a WebSocket connection from the page matches `url`, the `handler`
1824 /// is called with a [`WebSocketRoute`](crate::protocol::WebSocketRoute) object.
1825 /// The handler must call [`connect_to_server`](crate::protocol::WebSocketRoute::connect_to_server)
1826 /// to forward the connection to the real server, or
1827 /// [`close`](crate::protocol::WebSocketRoute::close) to terminate it.
1828 ///
1829 /// # Arguments
1830 ///
1831 /// * `url` — URL glob pattern (e.g. `"ws://**"` or `"wss://example.com/ws"`).
1832 /// * `handler` — Async closure receiving a `WebSocketRoute`.
1833 ///
1834 /// # Errors
1835 ///
1836 /// Returns an error if the RPC call to enable interception fails.
1837 ///
1838 /// See: <https://playwright.dev/docs/api/class-page#page-route-web-socket>
1839 pub async fn route_web_socket<F, Fut>(&self, url: &str, handler: F) -> Result<()>
1840 where
1841 F: Fn(crate::protocol::WebSocketRoute) -> Fut + Send + Sync + 'static,
1842 Fut: Future<Output = Result<()>> + Send + 'static,
1843 {
1844 let handler = Arc::new(
1845 move |route: crate::protocol::WebSocketRoute| -> WebSocketRouteHandlerFuture {
1846 Box::pin(handler(route))
1847 },
1848 );
1849
1850 self.ws_route_handlers
1851 .lock()
1852 .unwrap()
1853 .push(WsRouteHandlerEntry {
1854 pattern: url.to_string(),
1855 handler,
1856 });
1857
1858 self.enable_ws_interception().await
1859 }
1860
1861 /// Updates WebSocket interception patterns for this page.
1862 async fn enable_ws_interception(&self) -> Result<()> {
1863 let patterns: Vec<serde_json::Value> = self
1864 .ws_route_handlers
1865 .lock()
1866 .unwrap()
1867 .iter()
1868 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1869 .collect();
1870
1871 self.channel()
1872 .send_no_result(
1873 "setWebSocketInterceptionPatterns",
1874 serde_json::json!({ "patterns": patterns }),
1875 )
1876 .await
1877 }
1878
1879 /// Handles a route event from the protocol
1880 ///
1881 /// Called by on_event when a "route" event is received.
1882 /// Supports handler chaining via `route.fallback()` — if a handler calls
1883 /// `fallback()` instead of `continue_()`, `abort()`, or `fulfill()`, the
1884 /// next matching handler in the chain is tried.
1885 async fn on_route_event(&self, route: Route) {
1886 let handlers = self.route_handlers.lock().unwrap().clone();
1887 let url = route.request().url().to_string();
1888
1889 // Find matching handler (last registered wins, with fallback chaining)
1890 for entry in handlers.iter().rev() {
1891 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
1892 let handler = entry.handler.clone();
1893 if let Err(e) = handler(route.clone()).await {
1894 tracing::warn!("Route handler error: {}", e);
1895 break;
1896 }
1897 // If handler called fallback(), try the next matching handler
1898 if !route.was_handled() {
1899 continue;
1900 }
1901 break;
1902 }
1903 }
1904 }
1905
1906 /// Registers a download event handler.
1907 ///
1908 /// The handler will be called when a download is triggered by the page.
1909 /// Downloads occur when the page initiates a file download (e.g., clicking a link
1910 /// with the download attribute, or a server response with Content-Disposition: attachment).
1911 ///
1912 /// # Arguments
1913 ///
1914 /// * `handler` - Async closure that receives the Download object
1915 ///
1916 /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
1917 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1918 where
1919 F: Fn(Download) -> Fut + Send + Sync + 'static,
1920 Fut: Future<Output = Result<()>> + Send + 'static,
1921 {
1922 // Wrap handler with type erasure
1923 let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
1924 Box::pin(handler(download))
1925 });
1926
1927 // Store handler
1928 self.download_handlers.lock().unwrap().push(handler);
1929
1930 Ok(())
1931 }
1932
1933 /// Registers a dialog event handler.
1934 ///
1935 /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
1936 /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
1937 ///
1938 /// # Arguments
1939 ///
1940 /// * `handler` - Async closure that receives the Dialog object
1941 ///
1942 /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
1943 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1944 where
1945 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1946 Fut: Future<Output = Result<()>> + Send + 'static,
1947 {
1948 // Wrap handler with type erasure
1949 let handler =
1950 Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
1951
1952 // Store handler
1953 self.dialog_handlers.lock().unwrap().push(handler);
1954
1955 // Dialog events are auto-emitted (no subscription needed)
1956
1957 Ok(())
1958 }
1959
1960 /// Registers a console event handler.
1961 ///
1962 /// The handler is called whenever the page emits a JavaScript console message
1963 /// (e.g. `console.log`, `console.error`, `console.warn`, etc.).
1964 ///
1965 /// The server only sends console events after the first handler is registered
1966 /// (subscription is managed automatically).
1967 ///
1968 /// # Arguments
1969 ///
1970 /// * `handler` - Async closure that receives the [`ConsoleMessage`](crate::protocol::ConsoleMessage)
1971 ///
1972 /// See: <https://playwright.dev/docs/api/class-page#page-event-console>
1973 pub async fn on_console<F, Fut>(&self, handler: F) -> Result<()>
1974 where
1975 F: Fn(crate::protocol::ConsoleMessage) -> Fut + Send + Sync + 'static,
1976 Fut: Future<Output = Result<()>> + Send + 'static,
1977 {
1978 let handler = Arc::new(
1979 move |msg: crate::protocol::ConsoleMessage| -> ConsoleHandlerFuture {
1980 Box::pin(handler(msg))
1981 },
1982 );
1983
1984 let needs_subscription = {
1985 let handlers = self.console_handlers.lock().unwrap();
1986 let waiters = self.console_waiters.lock().unwrap();
1987 handlers.is_empty() && waiters.is_empty()
1988 };
1989 if needs_subscription {
1990 _ = self.channel().update_subscription("console", true).await;
1991 }
1992 self.console_handlers.lock().unwrap().push(handler);
1993
1994 Ok(())
1995 }
1996
1997 /// Registers a handler for file chooser events.
1998 ///
1999 /// The handler is called whenever the page opens a file chooser dialog
2000 /// (e.g. when the user clicks an `<input type="file">` element).
2001 ///
2002 /// Use [`FileChooser::set_files`](crate::protocol::FileChooser::set_files) inside
2003 /// the handler to satisfy the file chooser without OS-level interaction.
2004 ///
2005 /// The server only sends `"fileChooser"` events after the first handler is
2006 /// registered (subscription is managed automatically via `updateSubscription`).
2007 ///
2008 /// # Arguments
2009 ///
2010 /// * `handler` - Async closure that receives a [`FileChooser`](crate::protocol::FileChooser)
2011 ///
2012 /// # Example
2013 ///
2014 /// ```ignore
2015 /// page.on_filechooser(|chooser| async move {
2016 /// chooser.set_files(&[std::path::PathBuf::from("/tmp/file.txt")]).await
2017 /// }).await?;
2018 /// ```
2019 ///
2020 /// See: <https://playwright.dev/docs/api/class-page#page-event-file-chooser>
2021 pub async fn on_filechooser<F, Fut>(&self, handler: F) -> Result<()>
2022 where
2023 F: Fn(crate::protocol::FileChooser) -> Fut + Send + Sync + 'static,
2024 Fut: Future<Output = Result<()>> + Send + 'static,
2025 {
2026 let handler = Arc::new(
2027 move |chooser: crate::protocol::FileChooser| -> FileChooserHandlerFuture {
2028 Box::pin(handler(chooser))
2029 },
2030 );
2031
2032 let needs_subscription = {
2033 let handlers = self.filechooser_handlers.lock().unwrap();
2034 let waiters = self.filechooser_waiters.lock().unwrap();
2035 handlers.is_empty() && waiters.is_empty()
2036 };
2037 if needs_subscription {
2038 _ = self
2039 .channel()
2040 .update_subscription("fileChooser", true)
2041 .await;
2042 }
2043 self.filechooser_handlers.lock().unwrap().push(handler);
2044
2045 Ok(())
2046 }
2047
2048 /// Creates a one-shot waiter that resolves when the next file chooser opens.
2049 ///
2050 /// The waiter **must** be created before the action that triggers the file
2051 /// chooser to avoid a race condition.
2052 ///
2053 /// # Arguments
2054 ///
2055 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2056 ///
2057 /// # Errors
2058 ///
2059 /// Returns [`crate::error::Error::Timeout`] if the file chooser
2060 /// does not open within the timeout.
2061 ///
2062 /// # Example
2063 ///
2064 /// ```ignore
2065 /// // Set up waiter BEFORE triggering the file chooser
2066 /// let waiter = page.expect_file_chooser(None).await?;
2067 /// page.locator("input[type=file]").await.click(None).await?;
2068 /// let chooser = waiter.wait().await?;
2069 /// chooser.set_files(&[PathBuf::from("/tmp/file.txt")]).await?;
2070 /// ```
2071 ///
2072 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2073 pub async fn expect_file_chooser(
2074 &self,
2075 timeout: Option<f64>,
2076 ) -> Result<crate::protocol::EventWaiter<crate::protocol::FileChooser>> {
2077 let (tx, rx) = tokio::sync::oneshot::channel();
2078
2079 let needs_subscription = {
2080 let handlers = self.filechooser_handlers.lock().unwrap();
2081 let waiters = self.filechooser_waiters.lock().unwrap();
2082 handlers.is_empty() && waiters.is_empty()
2083 };
2084 if needs_subscription {
2085 _ = self
2086 .channel()
2087 .update_subscription("fileChooser", true)
2088 .await;
2089 }
2090 self.filechooser_waiters.lock().unwrap().push(tx);
2091
2092 Ok(crate::protocol::EventWaiter::new(
2093 rx,
2094 timeout.or(Some(30_000.0)),
2095 ))
2096 }
2097
2098 /// Creates a one-shot waiter that resolves when the next popup window opens.
2099 ///
2100 /// The waiter **must** be created before the action that opens the popup to
2101 /// avoid a race condition.
2102 ///
2103 /// # Arguments
2104 ///
2105 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2106 ///
2107 /// # Errors
2108 ///
2109 /// Returns [`crate::error::Error::Timeout`] if no popup
2110 /// opens within the timeout.
2111 ///
2112 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2113 pub async fn expect_popup(
2114 &self,
2115 timeout: Option<f64>,
2116 ) -> Result<crate::protocol::EventWaiter<Page>> {
2117 let (tx, rx) = tokio::sync::oneshot::channel();
2118 self.popup_waiters.lock().unwrap().push(tx);
2119 Ok(crate::protocol::EventWaiter::new(
2120 rx,
2121 timeout.or(Some(30_000.0)),
2122 ))
2123 }
2124
2125 /// Creates a one-shot waiter that resolves when the next download starts.
2126 ///
2127 /// The waiter **must** be created before the action that triggers the download
2128 /// to avoid a race condition.
2129 ///
2130 /// # Arguments
2131 ///
2132 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2133 ///
2134 /// # Errors
2135 ///
2136 /// Returns [`crate::error::Error::Timeout`] if no download
2137 /// starts within the timeout.
2138 ///
2139 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2140 pub async fn expect_download(
2141 &self,
2142 timeout: Option<f64>,
2143 ) -> Result<crate::protocol::EventWaiter<Download>> {
2144 let (tx, rx) = tokio::sync::oneshot::channel();
2145 self.download_waiters.lock().unwrap().push(tx);
2146 Ok(crate::protocol::EventWaiter::new(
2147 rx,
2148 timeout.or(Some(30_000.0)),
2149 ))
2150 }
2151
2152 /// Creates a one-shot waiter that resolves when the next network response is received.
2153 ///
2154 /// The waiter **must** be created before the action that triggers the response
2155 /// to avoid a race condition.
2156 ///
2157 /// # Arguments
2158 ///
2159 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2160 ///
2161 /// # Errors
2162 ///
2163 /// Returns [`crate::error::Error::Timeout`] if no response
2164 /// arrives within the timeout.
2165 ///
2166 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2167 pub async fn expect_response(
2168 &self,
2169 timeout: Option<f64>,
2170 ) -> Result<crate::protocol::EventWaiter<ResponseObject>> {
2171 let (tx, rx) = tokio::sync::oneshot::channel();
2172
2173 let needs_subscription = {
2174 let handlers = self.response_handlers.lock().unwrap();
2175 let waiters = self.response_waiters.lock().unwrap();
2176 handlers.is_empty() && waiters.is_empty()
2177 };
2178 if needs_subscription {
2179 _ = self.channel().update_subscription("response", true).await;
2180 }
2181 self.response_waiters.lock().unwrap().push(tx);
2182
2183 Ok(crate::protocol::EventWaiter::new(
2184 rx,
2185 timeout.or(Some(30_000.0)),
2186 ))
2187 }
2188
2189 /// Creates a one-shot waiter that resolves when the next network request is issued.
2190 ///
2191 /// The waiter **must** be created before the action that issues the request
2192 /// to avoid a race condition.
2193 ///
2194 /// # Arguments
2195 ///
2196 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2197 ///
2198 /// # Errors
2199 ///
2200 /// Returns [`crate::error::Error::Timeout`] if no request
2201 /// is issued within the timeout.
2202 ///
2203 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2204 pub async fn expect_request(
2205 &self,
2206 timeout: Option<f64>,
2207 ) -> Result<crate::protocol::EventWaiter<Request>> {
2208 let (tx, rx) = tokio::sync::oneshot::channel();
2209
2210 let needs_subscription = {
2211 let handlers = self.request_handlers.lock().unwrap();
2212 let waiters = self.request_waiters.lock().unwrap();
2213 handlers.is_empty() && waiters.is_empty()
2214 };
2215 if needs_subscription {
2216 _ = self.channel().update_subscription("request", true).await;
2217 }
2218 self.request_waiters.lock().unwrap().push(tx);
2219
2220 Ok(crate::protocol::EventWaiter::new(
2221 rx,
2222 timeout.or(Some(30_000.0)),
2223 ))
2224 }
2225
2226 /// Creates a one-shot waiter that resolves when the next console message is produced.
2227 ///
2228 /// The waiter **must** be created before the action that produces the console
2229 /// message to avoid a race condition.
2230 ///
2231 /// # Arguments
2232 ///
2233 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2234 ///
2235 /// # Errors
2236 ///
2237 /// Returns [`crate::error::Error::Timeout`] if no console
2238 /// message is produced within the timeout.
2239 ///
2240 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2241 pub async fn expect_console_message(
2242 &self,
2243 timeout: Option<f64>,
2244 ) -> Result<crate::protocol::EventWaiter<crate::protocol::ConsoleMessage>> {
2245 let (tx, rx) = tokio::sync::oneshot::channel();
2246
2247 let needs_subscription = {
2248 let handlers = self.console_handlers.lock().unwrap();
2249 let waiters = self.console_waiters.lock().unwrap();
2250 handlers.is_empty() && waiters.is_empty()
2251 };
2252 if needs_subscription {
2253 _ = self.channel().update_subscription("console", true).await;
2254 }
2255 self.console_waiters.lock().unwrap().push(tx);
2256
2257 Ok(crate::protocol::EventWaiter::new(
2258 rx,
2259 timeout.or(Some(30_000.0)),
2260 ))
2261 }
2262
2263 /// Waits for the given event to fire and returns a typed `EventValue`.
2264 ///
2265 /// This is the generic version of the specific `expect_*` methods. It matches
2266 /// the playwright-python / playwright-js `page.expect_event(event_name)` API.
2267 ///
2268 /// The waiter **must** be created before the action that triggers the event.
2269 ///
2270 /// # Supported event names
2271 ///
2272 /// `"request"`, `"response"`, `"popup"`, `"download"`, `"console"`,
2273 /// `"filechooser"`, `"close"`, `"load"`, `"crash"`, `"pageerror"`,
2274 /// `"frameattached"`, `"framedetached"`, `"framenavigated"`, `"worker"`
2275 ///
2276 /// # Arguments
2277 ///
2278 /// * `event` - Event name (case-sensitive, matches Playwright protocol names).
2279 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
2280 ///
2281 /// # Errors
2282 ///
2283 /// Returns [`crate::error::Error::InvalidArgument`] for unknown event names.
2284 /// Returns [`crate::error::Error::Timeout`] if the event does not fire within the timeout.
2285 ///
2286 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-event>
2287 pub async fn expect_event(
2288 &self,
2289 event: &str,
2290 timeout: Option<f64>,
2291 ) -> Result<crate::protocol::EventWaiter<crate::protocol::EventValue>> {
2292 use crate::protocol::EventValue;
2293 use tokio::sync::oneshot;
2294
2295 let timeout_ms = timeout.or(Some(30_000.0));
2296
2297 match event {
2298 "request" => {
2299 let (tx, rx) = oneshot::channel::<EventValue>();
2300 let (inner_tx, inner_rx) = oneshot::channel::<Request>();
2301
2302 let needs_subscription = {
2303 let handlers = self.request_handlers.lock().unwrap();
2304 let waiters = self.request_waiters.lock().unwrap();
2305 handlers.is_empty() && waiters.is_empty()
2306 };
2307 if needs_subscription {
2308 _ = self.channel().update_subscription("request", true).await;
2309 }
2310 self.request_waiters.lock().unwrap().push(inner_tx);
2311
2312 tokio::spawn(async move {
2313 if let Ok(v) = inner_rx.await {
2314 let _ = tx.send(EventValue::Request(v));
2315 }
2316 });
2317
2318 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2319 }
2320
2321 "response" => {
2322 let (tx, rx) = oneshot::channel::<EventValue>();
2323 let (inner_tx, inner_rx) = oneshot::channel::<ResponseObject>();
2324
2325 let needs_subscription = {
2326 let handlers = self.response_handlers.lock().unwrap();
2327 let waiters = self.response_waiters.lock().unwrap();
2328 handlers.is_empty() && waiters.is_empty()
2329 };
2330 if needs_subscription {
2331 _ = self.channel().update_subscription("response", true).await;
2332 }
2333 self.response_waiters.lock().unwrap().push(inner_tx);
2334
2335 tokio::spawn(async move {
2336 if let Ok(v) = inner_rx.await {
2337 let _ = tx.send(EventValue::Response(v));
2338 }
2339 });
2340
2341 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2342 }
2343
2344 "popup" => {
2345 let (tx, rx) = oneshot::channel::<EventValue>();
2346 let (inner_tx, inner_rx) = oneshot::channel::<Page>();
2347 self.popup_waiters.lock().unwrap().push(inner_tx);
2348
2349 tokio::spawn(async move {
2350 if let Ok(v) = inner_rx.await {
2351 let _ = tx.send(EventValue::Page(v));
2352 }
2353 });
2354
2355 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2356 }
2357
2358 "download" => {
2359 let (tx, rx) = oneshot::channel::<EventValue>();
2360 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Download>();
2361 self.download_waiters.lock().unwrap().push(inner_tx);
2362
2363 tokio::spawn(async move {
2364 if let Ok(v) = inner_rx.await {
2365 let _ = tx.send(EventValue::Download(v));
2366 }
2367 });
2368
2369 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2370 }
2371
2372 "console" => {
2373 let (tx, rx) = oneshot::channel::<EventValue>();
2374 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::ConsoleMessage>();
2375
2376 let needs_subscription = {
2377 let handlers = self.console_handlers.lock().unwrap();
2378 let waiters = self.console_waiters.lock().unwrap();
2379 handlers.is_empty() && waiters.is_empty()
2380 };
2381 if needs_subscription {
2382 _ = self.channel().update_subscription("console", true).await;
2383 }
2384 self.console_waiters.lock().unwrap().push(inner_tx);
2385
2386 tokio::spawn(async move {
2387 if let Ok(v) = inner_rx.await {
2388 let _ = tx.send(EventValue::ConsoleMessage(v));
2389 }
2390 });
2391
2392 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2393 }
2394
2395 "filechooser" => {
2396 let (tx, rx) = oneshot::channel::<EventValue>();
2397 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::FileChooser>();
2398
2399 let needs_subscription = {
2400 let handlers = self.filechooser_handlers.lock().unwrap();
2401 let waiters = self.filechooser_waiters.lock().unwrap();
2402 handlers.is_empty() && waiters.is_empty()
2403 };
2404 if needs_subscription {
2405 _ = self
2406 .channel()
2407 .update_subscription("fileChooser", true)
2408 .await;
2409 }
2410 self.filechooser_waiters.lock().unwrap().push(inner_tx);
2411
2412 tokio::spawn(async move {
2413 if let Ok(v) = inner_rx.await {
2414 let _ = tx.send(EventValue::FileChooser(v));
2415 }
2416 });
2417
2418 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2419 }
2420
2421 "close" => {
2422 let (tx, rx) = oneshot::channel::<EventValue>();
2423 let (inner_tx, inner_rx) = oneshot::channel::<()>();
2424 self.close_waiters.lock().unwrap().push(inner_tx);
2425
2426 tokio::spawn(async move {
2427 if inner_rx.await.is_ok() {
2428 let _ = tx.send(EventValue::Close);
2429 }
2430 });
2431
2432 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2433 }
2434
2435 "load" => {
2436 let (tx, rx) = oneshot::channel::<EventValue>();
2437 let (inner_tx, inner_rx) = oneshot::channel::<()>();
2438 self.load_waiters.lock().unwrap().push(inner_tx);
2439
2440 tokio::spawn(async move {
2441 if inner_rx.await.is_ok() {
2442 let _ = tx.send(EventValue::Load);
2443 }
2444 });
2445
2446 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2447 }
2448
2449 "crash" => {
2450 let (tx, rx) = oneshot::channel::<EventValue>();
2451 let (inner_tx, inner_rx) = oneshot::channel::<()>();
2452 self.crash_waiters.lock().unwrap().push(inner_tx);
2453
2454 tokio::spawn(async move {
2455 if inner_rx.await.is_ok() {
2456 let _ = tx.send(EventValue::Crash);
2457 }
2458 });
2459
2460 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2461 }
2462
2463 "pageerror" => {
2464 let (tx, rx) = oneshot::channel::<EventValue>();
2465 let (inner_tx, inner_rx) = oneshot::channel::<String>();
2466 self.pageerror_waiters.lock().unwrap().push(inner_tx);
2467
2468 tokio::spawn(async move {
2469 if let Ok(msg) = inner_rx.await {
2470 let _ = tx.send(EventValue::PageError(msg));
2471 }
2472 });
2473
2474 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2475 }
2476
2477 "frameattached" => {
2478 let (tx, rx) = oneshot::channel::<EventValue>();
2479 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Frame>();
2480 self.frameattached_waiters.lock().unwrap().push(inner_tx);
2481
2482 tokio::spawn(async move {
2483 if let Ok(v) = inner_rx.await {
2484 let _ = tx.send(EventValue::Frame(v));
2485 }
2486 });
2487
2488 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2489 }
2490
2491 "framedetached" => {
2492 let (tx, rx) = oneshot::channel::<EventValue>();
2493 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Frame>();
2494 self.framedetached_waiters.lock().unwrap().push(inner_tx);
2495
2496 tokio::spawn(async move {
2497 if let Ok(v) = inner_rx.await {
2498 let _ = tx.send(EventValue::Frame(v));
2499 }
2500 });
2501
2502 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2503 }
2504
2505 "framenavigated" => {
2506 let (tx, rx) = oneshot::channel::<EventValue>();
2507 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Frame>();
2508 self.framenavigated_waiters.lock().unwrap().push(inner_tx);
2509
2510 tokio::spawn(async move {
2511 if let Ok(v) = inner_rx.await {
2512 let _ = tx.send(EventValue::Frame(v));
2513 }
2514 });
2515
2516 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2517 }
2518
2519 "worker" => {
2520 let (tx, rx) = oneshot::channel::<EventValue>();
2521 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Worker>();
2522 self.worker_waiters.lock().unwrap().push(inner_tx);
2523
2524 tokio::spawn(async move {
2525 if let Ok(v) = inner_rx.await {
2526 let _ = tx.send(EventValue::Worker(v));
2527 }
2528 });
2529
2530 Ok(crate::protocol::EventWaiter::new(rx, timeout_ms))
2531 }
2532
2533 other => Err(Error::InvalidArgument(format!(
2534 "Unknown event name '{}'. Supported: request, response, popup, download, \
2535 console, filechooser, close, load, crash, pageerror, \
2536 frameattached, framedetached, framenavigated, worker",
2537 other
2538 ))),
2539 }
2540 }
2541
2542 /// See: <https://playwright.dev/docs/api/class-page#page-event-request>
2543 pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
2544 where
2545 F: Fn(Request) -> Fut + Send + Sync + 'static,
2546 Fut: Future<Output = Result<()>> + Send + 'static,
2547 {
2548 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
2549 Box::pin(handler(request))
2550 });
2551
2552 let needs_subscription = {
2553 let handlers = self.request_handlers.lock().unwrap();
2554 let waiters = self.request_waiters.lock().unwrap();
2555 handlers.is_empty() && waiters.is_empty()
2556 };
2557 if needs_subscription {
2558 _ = self.channel().update_subscription("request", true).await;
2559 }
2560 self.request_handlers.lock().unwrap().push(handler);
2561
2562 Ok(())
2563 }
2564
2565 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-finished>
2566 pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
2567 where
2568 F: Fn(Request) -> Fut + Send + Sync + 'static,
2569 Fut: Future<Output = Result<()>> + Send + 'static,
2570 {
2571 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
2572 Box::pin(handler(request))
2573 });
2574
2575 let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
2576 if needs_subscription {
2577 _ = self
2578 .channel()
2579 .update_subscription("requestFinished", true)
2580 .await;
2581 }
2582 self.request_finished_handlers.lock().unwrap().push(handler);
2583
2584 Ok(())
2585 }
2586
2587 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-failed>
2588 pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
2589 where
2590 F: Fn(Request) -> Fut + Send + Sync + 'static,
2591 Fut: Future<Output = Result<()>> + Send + 'static,
2592 {
2593 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
2594 Box::pin(handler(request))
2595 });
2596
2597 let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
2598 if needs_subscription {
2599 _ = self
2600 .channel()
2601 .update_subscription("requestFailed", true)
2602 .await;
2603 }
2604 self.request_failed_handlers.lock().unwrap().push(handler);
2605
2606 Ok(())
2607 }
2608
2609 /// See: <https://playwright.dev/docs/api/class-page#page-event-response>
2610 pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
2611 where
2612 F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
2613 Fut: Future<Output = Result<()>> + Send + 'static,
2614 {
2615 let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
2616 Box::pin(handler(response))
2617 });
2618
2619 let needs_subscription = {
2620 let handlers = self.response_handlers.lock().unwrap();
2621 let waiters = self.response_waiters.lock().unwrap();
2622 handlers.is_empty() && waiters.is_empty()
2623 };
2624 if needs_subscription {
2625 _ = self.channel().update_subscription("response", true).await;
2626 }
2627 self.response_handlers.lock().unwrap().push(handler);
2628
2629 Ok(())
2630 }
2631
2632 /// Adds a listener for the `websocket` event.
2633 ///
2634 /// The handler will be called when a WebSocket request is dispatched.
2635 ///
2636 /// # Arguments
2637 ///
2638 /// * `handler` - The function to call when the event occurs
2639 ///
2640 /// See: <https://playwright.dev/docs/api/class-page#page-on-websocket>
2641 pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
2642 where
2643 F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
2644 Fut: Future<Output = Result<()>> + Send + 'static,
2645 {
2646 let handler =
2647 Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
2648 self.websocket_handlers.lock().unwrap().push(handler);
2649 Ok(())
2650 }
2651
2652 /// Registers a handler for the `worker` event.
2653 ///
2654 /// The handler is called when a new Web Worker is created in the page.
2655 ///
2656 /// # Arguments
2657 ///
2658 /// * `handler` - Async closure called with the new [`Worker`] object
2659 ///
2660 /// See: <https://playwright.dev/docs/api/class-page#page-event-worker>
2661 pub async fn on_worker<F, Fut>(&self, handler: F) -> Result<()>
2662 where
2663 F: Fn(Worker) -> Fut + Send + Sync + 'static,
2664 Fut: Future<Output = Result<()>> + Send + 'static,
2665 {
2666 let handler = Arc::new(move |w: Worker| -> WorkerHandlerFuture { Box::pin(handler(w)) });
2667 self.worker_handlers.lock().unwrap().push(handler);
2668 Ok(())
2669 }
2670
2671 /// Registers a handler for the `close` event.
2672 ///
2673 /// The handler is called when the page is closed, either by calling `page.close()`,
2674 /// by the browser context being closed, or when the browser process exits.
2675 ///
2676 /// # Arguments
2677 ///
2678 /// * `handler` - Async closure called with no arguments when the page closes
2679 ///
2680 /// See: <https://playwright.dev/docs/api/class-page#page-event-close>
2681 pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
2682 where
2683 F: Fn() -> Fut + Send + Sync + 'static,
2684 Fut: Future<Output = Result<()>> + Send + 'static,
2685 {
2686 let handler = Arc::new(move || -> CloseHandlerFuture { Box::pin(handler()) });
2687 self.close_handlers.lock().unwrap().push(handler);
2688 Ok(())
2689 }
2690
2691 /// Registers a handler for the `load` event.
2692 ///
2693 /// The handler is called when the page's `load` event fires, i.e. after
2694 /// all resources including stylesheets and images have finished loading.
2695 ///
2696 /// The server only sends `"load"` events after the first handler is registered
2697 /// (subscription is managed automatically).
2698 ///
2699 /// # Arguments
2700 ///
2701 /// * `handler` - Async closure called with no arguments when the page loads
2702 ///
2703 /// See: <https://playwright.dev/docs/api/class-page#page-event-load>
2704 pub async fn on_load<F, Fut>(&self, handler: F) -> Result<()>
2705 where
2706 F: Fn() -> Fut + Send + Sync + 'static,
2707 Fut: Future<Output = Result<()>> + Send + 'static,
2708 {
2709 let handler = Arc::new(move || -> LoadHandlerFuture { Box::pin(handler()) });
2710 // "load" events come via Frame's "loadstate" event, no subscription needed.
2711 self.load_handlers.lock().unwrap().push(handler);
2712 Ok(())
2713 }
2714
2715 /// Registers a handler for the `crash` event.
2716 ///
2717 /// The handler is called when the page crashes (e.g. runs out of memory).
2718 ///
2719 /// # Arguments
2720 ///
2721 /// * `handler` - Async closure called with no arguments when the page crashes
2722 ///
2723 /// See: <https://playwright.dev/docs/api/class-page#page-event-crash>
2724 pub async fn on_crash<F, Fut>(&self, handler: F) -> Result<()>
2725 where
2726 F: Fn() -> Fut + Send + Sync + 'static,
2727 Fut: Future<Output = Result<()>> + Send + 'static,
2728 {
2729 let handler = Arc::new(move || -> CrashHandlerFuture { Box::pin(handler()) });
2730 self.crash_handlers.lock().unwrap().push(handler);
2731 Ok(())
2732 }
2733
2734 /// Registers a handler for the `pageError` event.
2735 ///
2736 /// The handler is called when an uncaught JavaScript exception is thrown in the page.
2737 /// The handler receives the error message as a `String`.
2738 ///
2739 /// The server only sends `"pageError"` events after the first handler is registered
2740 /// (subscription is managed automatically).
2741 ///
2742 /// # Arguments
2743 ///
2744 /// * `handler` - Async closure that receives the error message string
2745 ///
2746 /// See: <https://playwright.dev/docs/api/class-page#page-event-page-error>
2747 pub async fn on_pageerror<F, Fut>(&self, handler: F) -> Result<()>
2748 where
2749 F: Fn(String) -> Fut + Send + Sync + 'static,
2750 Fut: Future<Output = Result<()>> + Send + 'static,
2751 {
2752 let handler =
2753 Arc::new(move |msg: String| -> PageErrorHandlerFuture { Box::pin(handler(msg)) });
2754 // "pageError" events come via BrowserContext, no subscription needed.
2755 self.pageerror_handlers.lock().unwrap().push(handler);
2756 Ok(())
2757 }
2758
2759 /// Registers a handler for the `popup` event.
2760 ///
2761 /// The handler is called when the page opens a popup window (e.g. via `window.open()`).
2762 /// The handler receives the new popup [`Page`] object.
2763 ///
2764 /// The server only sends `"popup"` events after the first handler is registered
2765 /// (subscription is managed automatically).
2766 ///
2767 /// # Arguments
2768 ///
2769 /// * `handler` - Async closure that receives the popup Page
2770 ///
2771 /// See: <https://playwright.dev/docs/api/class-page#page-event-popup>
2772 pub async fn on_popup<F, Fut>(&self, handler: F) -> Result<()>
2773 where
2774 F: Fn(Page) -> Fut + Send + Sync + 'static,
2775 Fut: Future<Output = Result<()>> + Send + 'static,
2776 {
2777 let handler = Arc::new(move |page: Page| -> PopupHandlerFuture { Box::pin(handler(page)) });
2778 // "popup" events arrive via BrowserContext's "page" event when a page has an opener.
2779 self.popup_handlers.lock().unwrap().push(handler);
2780 Ok(())
2781 }
2782
2783 /// Registers a handler for the `frameAttached` event.
2784 ///
2785 /// The handler is called when a new frame (iframe) is attached to the page.
2786 /// The handler receives the attached [`Frame`](crate::protocol::Frame) object.
2787 ///
2788 /// # Arguments
2789 ///
2790 /// * `handler` - Async closure that receives the attached Frame
2791 ///
2792 /// See: <https://playwright.dev/docs/api/class-page#page-event-frameattached>
2793 pub async fn on_frameattached<F, Fut>(&self, handler: F) -> Result<()>
2794 where
2795 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2796 Fut: Future<Output = Result<()>> + Send + 'static,
2797 {
2798 let handler = Arc::new(
2799 move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2800 Box::pin(handler(frame))
2801 },
2802 );
2803 self.frameattached_handlers.lock().unwrap().push(handler);
2804 Ok(())
2805 }
2806
2807 /// Registers a handler for the `frameDetached` event.
2808 ///
2809 /// The handler is called when a frame (iframe) is detached from the page.
2810 /// The handler receives the detached [`Frame`](crate::protocol::Frame) object.
2811 ///
2812 /// # Arguments
2813 ///
2814 /// * `handler` - Async closure that receives the detached Frame
2815 ///
2816 /// See: <https://playwright.dev/docs/api/class-page#page-event-framedetached>
2817 pub async fn on_framedetached<F, Fut>(&self, handler: F) -> Result<()>
2818 where
2819 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2820 Fut: Future<Output = Result<()>> + Send + 'static,
2821 {
2822 let handler = Arc::new(
2823 move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2824 Box::pin(handler(frame))
2825 },
2826 );
2827 self.framedetached_handlers.lock().unwrap().push(handler);
2828 Ok(())
2829 }
2830
2831 /// Registers a handler for the `frameNavigated` event.
2832 ///
2833 /// The handler is called when a frame navigates to a new URL.
2834 /// The handler receives the navigated [`Frame`](crate::protocol::Frame) object.
2835 ///
2836 /// # Arguments
2837 ///
2838 /// * `handler` - Async closure that receives the navigated Frame
2839 ///
2840 /// See: <https://playwright.dev/docs/api/class-page#page-event-framenavigated>
2841 pub async fn on_framenavigated<F, Fut>(&self, handler: F) -> Result<()>
2842 where
2843 F: Fn(crate::protocol::Frame) -> Fut + Send + Sync + 'static,
2844 Fut: Future<Output = Result<()>> + Send + 'static,
2845 {
2846 let handler = Arc::new(
2847 move |frame: crate::protocol::Frame| -> FrameEventHandlerFuture {
2848 Box::pin(handler(frame))
2849 },
2850 );
2851 self.framenavigated_handlers.lock().unwrap().push(handler);
2852 Ok(())
2853 }
2854
2855 /// Exposes a Rust function to this page as `window[name]` in JavaScript.
2856 ///
2857 /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
2858 /// server fires a `bindingCall` event on the **page** channel that invokes
2859 /// `callback` with the deserialized arguments. The return value is sent back
2860 /// to JS so the `await window[name](…)` expression resolves with it.
2861 ///
2862 /// The binding is page-scoped and not visible to other pages in the same context.
2863 ///
2864 /// # Arguments
2865 ///
2866 /// * `name` – JavaScript identifier that will be available as `window[name]`.
2867 /// * `callback` – Async closure called with `Vec<serde_json::Value>` (JS arguments)
2868 /// returning `serde_json::Value` (the result).
2869 ///
2870 /// # Errors
2871 ///
2872 /// Returns error if:
2873 /// - The page has been closed.
2874 /// - Communication with the browser process fails.
2875 ///
2876 /// See: <https://playwright.dev/docs/api/class-page#page-expose-function>
2877 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
2878 where
2879 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2880 Fut: Future<Output = serde_json::Value> + Send + 'static,
2881 {
2882 self.expose_binding_internal(name, false, callback).await
2883 }
2884
2885 /// Exposes a Rust function to this page as `window[name]` in JavaScript,
2886 /// with `needsHandle: true`.
2887 ///
2888 /// Identical to [`expose_function`](Self::expose_function) but the Playwright
2889 /// server passes the first argument as a `JSHandle` object rather than a plain
2890 /// value.
2891 ///
2892 /// # Arguments
2893 ///
2894 /// * `name` – JavaScript identifier.
2895 /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
2896 ///
2897 /// # Errors
2898 ///
2899 /// Returns error if:
2900 /// - The page has been closed.
2901 /// - Communication with the browser process fails.
2902 ///
2903 /// See: <https://playwright.dev/docs/api/class-page#page-expose-binding>
2904 pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
2905 where
2906 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2907 Fut: Future<Output = serde_json::Value> + Send + 'static,
2908 {
2909 self.expose_binding_internal(name, true, callback).await
2910 }
2911
2912 /// Internal implementation shared by page-level expose_function and expose_binding.
2913 ///
2914 /// Both `expose_function` and `expose_binding` use `needsHandle: false` because
2915 /// the current implementation does not support JSHandle objects. Using
2916 /// `needsHandle: true` would cause the Playwright server to wrap the first
2917 /// argument as a `JSHandle`, which requires a JSHandle protocol object that
2918 /// is not yet implemented.
2919 async fn expose_binding_internal<F, Fut>(
2920 &self,
2921 name: &str,
2922 _needs_handle: bool,
2923 callback: F,
2924 ) -> Result<()>
2925 where
2926 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
2927 Fut: Future<Output = serde_json::Value> + Send + 'static,
2928 {
2929 let callback: PageBindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
2930 Box::pin(callback(args)) as PageBindingCallbackFuture
2931 });
2932
2933 // Store callback before sending RPC (avoids race with early bindingCall events)
2934 self.binding_callbacks
2935 .lock()
2936 .unwrap()
2937 .insert(name.to_string(), callback);
2938
2939 // Tell the Playwright server to inject window[name] into this page.
2940 // Always use needsHandle: false — see note above.
2941 self.channel()
2942 .send_no_result(
2943 "exposeBinding",
2944 serde_json::json!({ "name": name, "needsHandle": false }),
2945 )
2946 .await
2947 }
2948
2949 /// Handles a download event from the protocol
2950 async fn on_download_event(&self, download: Download) {
2951 let handlers = self.download_handlers.lock().unwrap().clone();
2952
2953 for handler in handlers {
2954 if let Err(e) = handler(download.clone()).await {
2955 tracing::warn!("Download handler error: {}", e);
2956 }
2957 }
2958 // Notify the first expect_download() waiter (FIFO order)
2959 if let Some(tx) = self.download_waiters.lock().unwrap().pop() {
2960 let _ = tx.send(download);
2961 }
2962 }
2963
2964 /// Handles a dialog event from the protocol
2965 async fn on_dialog_event(&self, dialog: Dialog) {
2966 let handlers = self.dialog_handlers.lock().unwrap().clone();
2967
2968 for handler in handlers {
2969 if let Err(e) = handler(dialog.clone()).await {
2970 tracing::warn!("Dialog handler error: {}", e);
2971 }
2972 }
2973 }
2974
2975 async fn on_request_event(&self, request: Request) {
2976 let handlers = self.request_handlers.lock().unwrap().clone();
2977
2978 for handler in handlers {
2979 if let Err(e) = handler(request.clone()).await {
2980 tracing::warn!("Request handler error: {}", e);
2981 }
2982 }
2983 // Notify the first expect_request() waiter (FIFO order)
2984 if let Some(tx) = self.request_waiters.lock().unwrap().pop() {
2985 let _ = tx.send(request);
2986 }
2987 }
2988
2989 async fn on_request_failed_event(&self, request: Request) {
2990 let handlers = self.request_failed_handlers.lock().unwrap().clone();
2991
2992 for handler in handlers {
2993 if let Err(e) = handler(request.clone()).await {
2994 tracing::warn!("RequestFailed handler error: {}", e);
2995 }
2996 }
2997 }
2998
2999 async fn on_request_finished_event(&self, request: Request) {
3000 let handlers = self.request_finished_handlers.lock().unwrap().clone();
3001
3002 for handler in handlers {
3003 if let Err(e) = handler(request.clone()).await {
3004 tracing::warn!("RequestFinished handler error: {}", e);
3005 }
3006 }
3007 }
3008
3009 async fn on_response_event(&self, response: ResponseObject) {
3010 let handlers = self.response_handlers.lock().unwrap().clone();
3011
3012 for handler in handlers {
3013 if let Err(e) = handler(response.clone()).await {
3014 tracing::warn!("Response handler error: {}", e);
3015 }
3016 }
3017 // Notify the first expect_response() waiter (FIFO order)
3018 if let Some(tx) = self.response_waiters.lock().unwrap().pop() {
3019 let _ = tx.send(response);
3020 }
3021 }
3022
3023 /// Registers a handler function that runs whenever a locator matches an element on the page.
3024 ///
3025 /// This is useful for handling overlays (cookie banners, modals, permission dialogs)
3026 /// that appear unexpectedly and need to be dismissed before test actions can proceed.
3027 ///
3028 /// When a matching element appears, Playwright sends a `locatorHandlerTriggered` event.
3029 /// The handler is called with the matching `Locator`. After the handler completes,
3030 /// Playwright is notified via `resolveLocatorHandler` so it can resume pending actions.
3031 ///
3032 /// # Arguments
3033 ///
3034 /// * `locator` - A locator identifying the overlay element to watch for
3035 /// * `handler` - Async function called with the matching Locator when the element appears
3036 /// * `options` - Optional settings (no_wait_after, times)
3037 ///
3038 /// # Errors
3039 ///
3040 /// Returns error if communication with the browser process fails.
3041 ///
3042 /// See: <https://playwright.dev/docs/api/class-page#page-add-locator-handler>
3043 pub async fn add_locator_handler<F, Fut>(
3044 &self,
3045 locator: &crate::protocol::Locator,
3046 handler: F,
3047 options: Option<AddLocatorHandlerOptions>,
3048 ) -> Result<()>
3049 where
3050 F: Fn(crate::protocol::Locator) -> Fut + Send + Sync + 'static,
3051 Fut: Future<Output = Result<()>> + Send + 'static,
3052 {
3053 let selector = locator.selector().to_string();
3054 let no_wait_after = options
3055 .as_ref()
3056 .and_then(|o| o.no_wait_after)
3057 .unwrap_or(false);
3058 let times = options.as_ref().and_then(|o| o.times);
3059
3060 // Send registerLocatorHandler RPC — returns {"uid": N}
3061 let params = serde_json::json!({
3062 "selector": selector,
3063 "noWaitAfter": no_wait_after,
3064 });
3065 let result: Value = self
3066 .channel()
3067 .send("registerLocatorHandler", params)
3068 .await?;
3069
3070 let uid = result
3071 .get("uid")
3072 .and_then(|v| v.as_u64())
3073 .map(|v| v as u32)
3074 .ok_or_else(|| {
3075 Error::ProtocolError("registerLocatorHandler response missing 'uid'".to_string())
3076 })?;
3077
3078 let handler_fn: LocatorHandlerFn = Arc::new(
3079 move |loc: crate::protocol::Locator| -> LocatorHandlerFuture { Box::pin(handler(loc)) },
3080 );
3081
3082 self.locator_handlers
3083 .lock()
3084 .unwrap()
3085 .push(LocatorHandlerEntry {
3086 uid,
3087 selector,
3088 handler: handler_fn,
3089 times_remaining: times,
3090 });
3091
3092 Ok(())
3093 }
3094
3095 /// Removes a previously registered locator handler.
3096 ///
3097 /// Sends `unregisterLocatorHandler` to the Playwright server using the uid
3098 /// that was assigned when the handler was first registered.
3099 ///
3100 /// # Arguments
3101 ///
3102 /// * `locator` - The same locator that was passed to `add_locator_handler`
3103 ///
3104 /// # Errors
3105 ///
3106 /// Returns error if no handler for this locator is registered, or if
3107 /// communication with the browser process fails.
3108 ///
3109 /// See: <https://playwright.dev/docs/api/class-page#page-remove-locator-handler>
3110 pub async fn remove_locator_handler(&self, locator: &crate::protocol::Locator) -> Result<()> {
3111 let selector = locator.selector();
3112
3113 // Find the uid for this selector
3114 let uid = {
3115 let handlers = self.locator_handlers.lock().unwrap();
3116 handlers
3117 .iter()
3118 .find(|e| e.selector == selector)
3119 .map(|e| e.uid)
3120 };
3121
3122 let uid = uid.ok_or_else(|| {
3123 Error::ProtocolError(format!(
3124 "No locator handler registered for selector '{}'",
3125 selector
3126 ))
3127 })?;
3128
3129 // Send unregisterLocatorHandler RPC
3130 self.channel()
3131 .send_no_result(
3132 "unregisterLocatorHandler",
3133 serde_json::json!({ "uid": uid }),
3134 )
3135 .await?;
3136
3137 // Remove from local registry
3138 self.locator_handlers
3139 .lock()
3140 .unwrap()
3141 .retain(|e| e.uid != uid);
3142
3143 Ok(())
3144 }
3145
3146 /// Triggers dialog event (called by BrowserContext when dialog events arrive)
3147 ///
3148 /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
3149 /// This method is public so BrowserContext can forward dialog events.
3150 pub async fn trigger_dialog_event(&self, dialog: Dialog) {
3151 self.on_dialog_event(dialog).await;
3152 }
3153
3154 /// Triggers request event (called by BrowserContext when request events arrive)
3155 pub(crate) async fn trigger_request_event(&self, request: Request) {
3156 self.on_request_event(request).await;
3157 }
3158
3159 pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
3160 self.on_request_finished_event(request).await;
3161 }
3162
3163 pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
3164 self.on_request_failed_event(request).await;
3165 }
3166
3167 /// Triggers response event (called by BrowserContext when response events arrive)
3168 pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
3169 self.on_response_event(response).await;
3170 }
3171
3172 /// Triggers console event (called by BrowserContext when console events arrive).
3173 ///
3174 /// The BrowserContext receives all `"console"` events, constructs the
3175 /// [`ConsoleMessage`](crate::protocol::ConsoleMessage), dispatches to
3176 /// context-level handlers, then calls this method to forward to page-level handlers.
3177 pub(crate) async fn trigger_console_event(&self, msg: crate::protocol::ConsoleMessage) {
3178 self.on_console_event(msg).await;
3179 }
3180
3181 async fn on_console_event(&self, msg: crate::protocol::ConsoleMessage) {
3182 // Accumulate message for console_messages() accessor
3183 self.console_messages_log.lock().unwrap().push(msg.clone());
3184 // Notify the first expect_console_message() waiter (FIFO order)
3185 if let Some(tx) = self.console_waiters.lock().unwrap().pop() {
3186 let _ = tx.send(msg.clone());
3187 }
3188 let handlers = self.console_handlers.lock().unwrap().clone();
3189 for handler in handlers {
3190 if let Err(e) = handler(msg.clone()).await {
3191 tracing::warn!("Console handler error: {}", e);
3192 }
3193 }
3194 }
3195
3196 /// Dispatches a FileChooser event to registered handlers and one-shot waiters.
3197 async fn on_filechooser_event(&self, chooser: crate::protocol::FileChooser) {
3198 // Dispatch to persistent handlers
3199 let handlers = self.filechooser_handlers.lock().unwrap().clone();
3200 for handler in handlers {
3201 if let Err(e) = handler(chooser.clone()).await {
3202 tracing::warn!("FileChooser handler error: {}", e);
3203 }
3204 }
3205
3206 // Notify the first expect_file_chooser() waiter (FIFO order)
3207 if let Some(tx) = self.filechooser_waiters.lock().unwrap().pop() {
3208 let _ = tx.send(chooser);
3209 }
3210 }
3211
3212 /// Triggers load event (called by Frame when loadstate "load" is added)
3213 pub(crate) async fn trigger_load_event(&self) {
3214 self.on_load_event().await;
3215 }
3216
3217 /// Triggers pageError event (called by BrowserContext when pageError arrives)
3218 pub(crate) async fn trigger_pageerror_event(&self, message: String) {
3219 self.on_pageerror_event(message).await;
3220 }
3221
3222 /// Triggers popup event (called by BrowserContext when a page is opened with an opener)
3223 pub(crate) async fn trigger_popup_event(&self, popup: Page) {
3224 self.on_popup_event(popup).await;
3225 }
3226
3227 /// Triggers frameNavigated event (called by Frame when "navigated" is received)
3228 pub(crate) async fn trigger_framenavigated_event(&self, frame: crate::protocol::Frame) {
3229 self.on_framenavigated_event(frame).await;
3230 }
3231
3232 async fn on_close_event(&self) {
3233 let handlers = self.close_handlers.lock().unwrap().clone();
3234 for handler in handlers {
3235 if let Err(e) = handler().await {
3236 tracing::warn!("Close handler error: {}", e);
3237 }
3238 }
3239 // Notify expect_event("close") waiters
3240 let waiters: Vec<_> = self.close_waiters.lock().unwrap().drain(..).collect();
3241 for tx in waiters {
3242 let _ = tx.send(());
3243 }
3244 }
3245
3246 async fn on_load_event(&self) {
3247 let handlers = self.load_handlers.lock().unwrap().clone();
3248 for handler in handlers {
3249 if let Err(e) = handler().await {
3250 tracing::warn!("Load handler error: {}", e);
3251 }
3252 }
3253 // Notify expect_event("load") waiters
3254 let waiters: Vec<_> = self.load_waiters.lock().unwrap().drain(..).collect();
3255 for tx in waiters {
3256 let _ = tx.send(());
3257 }
3258 }
3259
3260 async fn on_crash_event(&self) {
3261 let handlers = self.crash_handlers.lock().unwrap().clone();
3262 for handler in handlers {
3263 if let Err(e) = handler().await {
3264 tracing::warn!("Crash handler error: {}", e);
3265 }
3266 }
3267 // Notify expect_event("crash") waiters
3268 let waiters: Vec<_> = self.crash_waiters.lock().unwrap().drain(..).collect();
3269 for tx in waiters {
3270 let _ = tx.send(());
3271 }
3272 }
3273
3274 async fn on_pageerror_event(&self, message: String) {
3275 // Accumulate error for page_errors() accessor
3276 self.page_errors_log.lock().unwrap().push(message.clone());
3277 let handlers = self.pageerror_handlers.lock().unwrap().clone();
3278 for handler in handlers {
3279 if let Err(e) = handler(message.clone()).await {
3280 tracing::warn!("PageError handler error: {}", e);
3281 }
3282 }
3283 // Notify expect_event("pageerror") waiters
3284 if let Some(tx) = self.pageerror_waiters.lock().unwrap().pop() {
3285 let _ = tx.send(message);
3286 }
3287 }
3288
3289 async fn on_popup_event(&self, popup: Page) {
3290 let handlers = self.popup_handlers.lock().unwrap().clone();
3291 for handler in handlers {
3292 if let Err(e) = handler(popup.clone()).await {
3293 tracing::warn!("Popup handler error: {}", e);
3294 }
3295 }
3296 // Notify the first expect_popup() waiter (FIFO order)
3297 if let Some(tx) = self.popup_waiters.lock().unwrap().pop() {
3298 let _ = tx.send(popup);
3299 }
3300 }
3301
3302 async fn on_frameattached_event(&self, frame: crate::protocol::Frame) {
3303 let handlers = self.frameattached_handlers.lock().unwrap().clone();
3304 for handler in handlers {
3305 if let Err(e) = handler(frame.clone()).await {
3306 tracing::warn!("FrameAttached handler error: {}", e);
3307 }
3308 }
3309 if let Some(tx) = self.frameattached_waiters.lock().unwrap().pop() {
3310 let _ = tx.send(frame);
3311 }
3312 }
3313
3314 async fn on_framedetached_event(&self, frame: crate::protocol::Frame) {
3315 let handlers = self.framedetached_handlers.lock().unwrap().clone();
3316 for handler in handlers {
3317 if let Err(e) = handler(frame.clone()).await {
3318 tracing::warn!("FrameDetached handler error: {}", e);
3319 }
3320 }
3321 if let Some(tx) = self.framedetached_waiters.lock().unwrap().pop() {
3322 let _ = tx.send(frame);
3323 }
3324 }
3325
3326 async fn on_framenavigated_event(&self, frame: crate::protocol::Frame) {
3327 let handlers = self.framenavigated_handlers.lock().unwrap().clone();
3328 for handler in handlers {
3329 if let Err(e) = handler(frame.clone()).await {
3330 tracing::warn!("FrameNavigated handler error: {}", e);
3331 }
3332 }
3333 if let Some(tx) = self.framenavigated_waiters.lock().unwrap().pop() {
3334 let _ = tx.send(frame);
3335 }
3336 }
3337
3338 /// Adds a `<style>` tag into the page with the desired content.
3339 ///
3340 /// # Arguments
3341 ///
3342 /// * `options` - Style tag options (content, url, or path)
3343 ///
3344 /// # Returns
3345 ///
3346 /// Returns an ElementHandle pointing to the injected `<style>` tag
3347 ///
3348 /// # Example
3349 ///
3350 /// ```no_run
3351 /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
3352 /// # #[tokio::main]
3353 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3354 /// # let playwright = Playwright::launch().await?;
3355 /// # let browser = playwright.chromium().launch().await?;
3356 /// # let context = browser.new_context().await?;
3357 /// # let page = context.new_page().await?;
3358 /// use playwright_rs::protocol::AddStyleTagOptions;
3359 ///
3360 /// // With inline CSS
3361 /// page.add_style_tag(
3362 /// AddStyleTagOptions::builder()
3363 /// .content("body { background-color: red; }")
3364 /// .build()
3365 /// ).await?;
3366 ///
3367 /// // With external URL
3368 /// page.add_style_tag(
3369 /// AddStyleTagOptions::builder()
3370 /// .url("https://example.com/style.css")
3371 /// .build()
3372 /// ).await?;
3373 ///
3374 /// // From file
3375 /// page.add_style_tag(
3376 /// AddStyleTagOptions::builder()
3377 /// .path("./styles/custom.css")
3378 /// .build()
3379 /// ).await?;
3380 /// # Ok(())
3381 /// # }
3382 /// ```
3383 ///
3384 /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
3385 pub async fn add_style_tag(
3386 &self,
3387 options: AddStyleTagOptions,
3388 ) -> Result<Arc<crate::protocol::ElementHandle>> {
3389 let frame = self.main_frame().await?;
3390 frame.add_style_tag(options).await
3391 }
3392
3393 /// Adds a script which would be evaluated in one of the following scenarios:
3394 /// - Whenever the page is navigated
3395 /// - Whenever a child frame is attached or navigated
3396 ///
3397 /// The script is evaluated after the document was created but before any of its scripts were run.
3398 ///
3399 /// # Arguments
3400 ///
3401 /// * `script` - JavaScript code to be injected into the page
3402 ///
3403 /// # Example
3404 ///
3405 /// ```no_run
3406 /// # use playwright_rs::protocol::Playwright;
3407 /// # #[tokio::main]
3408 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3409 /// # let playwright = Playwright::launch().await?;
3410 /// # let browser = playwright.chromium().launch().await?;
3411 /// # let context = browser.new_context().await?;
3412 /// # let page = context.new_page().await?;
3413 /// page.add_init_script("window.injected = 123;").await?;
3414 /// # Ok(())
3415 /// # }
3416 /// ```
3417 ///
3418 /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
3419 pub async fn add_init_script(&self, script: &str) -> Result<()> {
3420 self.channel()
3421 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
3422 .await
3423 }
3424
3425 /// Sets the viewport size for the page.
3426 ///
3427 /// This method allows dynamic resizing of the viewport after page creation,
3428 /// useful for testing responsive layouts at different screen sizes.
3429 ///
3430 /// # Arguments
3431 ///
3432 /// * `viewport` - The viewport dimensions (width and height in pixels)
3433 ///
3434 /// # Example
3435 ///
3436 /// ```no_run
3437 /// # use playwright_rs::protocol::{Playwright, Viewport};
3438 /// # #[tokio::main]
3439 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3440 /// # let playwright = Playwright::launch().await?;
3441 /// # let browser = playwright.chromium().launch().await?;
3442 /// # let page = browser.new_page().await?;
3443 /// // Set viewport to mobile size
3444 /// let mobile = Viewport {
3445 /// width: 375,
3446 /// height: 667,
3447 /// };
3448 /// page.set_viewport_size(mobile).await?;
3449 ///
3450 /// // Later, test desktop layout
3451 /// let desktop = Viewport {
3452 /// width: 1920,
3453 /// height: 1080,
3454 /// };
3455 /// page.set_viewport_size(desktop).await?;
3456 /// # Ok(())
3457 /// # }
3458 /// ```
3459 ///
3460 /// # Errors
3461 ///
3462 /// Returns error if:
3463 /// - Page has been closed
3464 /// - Communication with browser process fails
3465 ///
3466 /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
3467 pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
3468 // Store the new viewport locally so viewport_size() can reflect the change
3469 if let Ok(mut guard) = self.viewport.write() {
3470 *guard = Some(viewport.clone());
3471 }
3472 self.channel()
3473 .send_no_result(
3474 "setViewportSize",
3475 serde_json::json!({ "viewportSize": viewport }),
3476 )
3477 .await
3478 }
3479
3480 /// Brings this page to the front (activates the tab).
3481 ///
3482 /// Activates the page in the browser, making it the focused tab. This is
3483 /// useful in multi-page tests to ensure actions target the correct page.
3484 ///
3485 /// # Errors
3486 ///
3487 /// Returns error if:
3488 /// - Page has been closed
3489 /// - Communication with browser process fails
3490 ///
3491 /// See: <https://playwright.dev/docs/api/class-page#page-bring-to-front>
3492 pub async fn bring_to_front(&self) -> Result<()> {
3493 self.channel()
3494 .send_no_result("bringToFront", serde_json::json!({}))
3495 .await
3496 }
3497
3498 /// Forces garbage collection in the browser (Chromium only).
3499 ///
3500 /// See: <https://playwright.dev/docs/api/class-page#page-request-gc>
3501 pub async fn request_gc(&self) -> Result<()> {
3502 self.channel()
3503 .send_no_result("requestGC", serde_json::json!({}))
3504 .await
3505 }
3506
3507 /// Sets extra HTTP headers that will be sent with every request from this page.
3508 ///
3509 /// These headers are sent in addition to headers set on the browser context via
3510 /// `BrowserContext::set_extra_http_headers()`. Page-level headers take precedence
3511 /// over context-level headers when names conflict.
3512 ///
3513 /// # Arguments
3514 ///
3515 /// * `headers` - Map of header names to values.
3516 ///
3517 /// # Errors
3518 ///
3519 /// Returns error if:
3520 /// - Page has been closed
3521 /// - Communication with browser process fails
3522 ///
3523 /// See: <https://playwright.dev/docs/api/class-page#page-set-extra-http-headers>
3524 pub async fn set_extra_http_headers(
3525 &self,
3526 headers: std::collections::HashMap<String, String>,
3527 ) -> Result<()> {
3528 // Playwright protocol expects an array of {name, value} objects
3529 // This RPC is sent on the Page channel (not the Frame channel)
3530 let headers_array: Vec<serde_json::Value> = headers
3531 .into_iter()
3532 .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
3533 .collect();
3534 self.channel()
3535 .send_no_result(
3536 "setExtraHTTPHeaders",
3537 serde_json::json!({ "headers": headers_array }),
3538 )
3539 .await
3540 }
3541
3542 /// Emulates media features for the page.
3543 ///
3544 /// This method allows emulating CSS media features such as `media`, `color-scheme`,
3545 /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
3546 ///
3547 /// To reset a specific feature to the browser default, use the `NoOverride` variant.
3548 ///
3549 /// # Arguments
3550 ///
3551 /// * `options` - Optional emulation options. If `None`, this is a no-op.
3552 ///
3553 /// # Example
3554 ///
3555 /// ```no_run
3556 /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
3557 /// # #[tokio::main]
3558 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3559 /// # let playwright = Playwright::launch().await?;
3560 /// # let browser = playwright.chromium().launch().await?;
3561 /// # let page = browser.new_page().await?;
3562 /// // Emulate print media
3563 /// page.emulate_media(Some(
3564 /// EmulateMediaOptions::builder()
3565 /// .media(Media::Print)
3566 /// .build()
3567 /// )).await?;
3568 ///
3569 /// // Emulate dark color scheme
3570 /// page.emulate_media(Some(
3571 /// EmulateMediaOptions::builder()
3572 /// .color_scheme(ColorScheme::Dark)
3573 /// .build()
3574 /// )).await?;
3575 /// # Ok(())
3576 /// # }
3577 /// ```
3578 ///
3579 /// # Errors
3580 ///
3581 /// Returns error if:
3582 /// - Page has been closed
3583 /// - Communication with browser process fails
3584 ///
3585 /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
3586 pub async fn emulate_media(&self, options: Option<EmulateMediaOptions>) -> Result<()> {
3587 let mut params = serde_json::json!({});
3588
3589 if let Some(opts) = options {
3590 if let Some(media) = opts.media {
3591 params["media"] = serde_json::to_value(media).map_err(|e| {
3592 crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
3593 })?;
3594 }
3595 if let Some(color_scheme) = opts.color_scheme {
3596 params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
3597 crate::error::Error::ProtocolError(format!(
3598 "Failed to serialize colorScheme: {}",
3599 e
3600 ))
3601 })?;
3602 }
3603 if let Some(reduced_motion) = opts.reduced_motion {
3604 params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
3605 crate::error::Error::ProtocolError(format!(
3606 "Failed to serialize reducedMotion: {}",
3607 e
3608 ))
3609 })?;
3610 }
3611 if let Some(forced_colors) = opts.forced_colors {
3612 params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
3613 crate::error::Error::ProtocolError(format!(
3614 "Failed to serialize forcedColors: {}",
3615 e
3616 ))
3617 })?;
3618 }
3619 }
3620
3621 self.channel().send_no_result("emulateMedia", params).await
3622 }
3623
3624 /// Generates a PDF of the page and returns it as bytes.
3625 ///
3626 /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
3627 /// not supported in Firefox or WebKit.
3628 ///
3629 /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
3630 /// saved to that file.
3631 ///
3632 /// # Arguments
3633 ///
3634 /// * `options` - Optional PDF generation options
3635 ///
3636 /// # Example
3637 ///
3638 /// ```no_run
3639 /// # use playwright_rs::protocol::Playwright;
3640 /// # #[tokio::main]
3641 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3642 /// # let playwright = Playwright::launch().await?;
3643 /// # let browser = playwright.chromium().launch().await?;
3644 /// # let page = browser.new_page().await?;
3645 /// let pdf_bytes = page.pdf(None).await?;
3646 /// assert!(!pdf_bytes.is_empty());
3647 /// # Ok(())
3648 /// # }
3649 /// ```
3650 ///
3651 /// # Errors
3652 ///
3653 /// Returns error if:
3654 /// - The browser is not Chromium (PDF only supported in Chromium)
3655 /// - Page has been closed
3656 /// - Communication with browser process fails
3657 ///
3658 /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
3659 pub async fn pdf(&self, options: Option<PdfOptions>) -> Result<Vec<u8>> {
3660 let mut params = serde_json::json!({});
3661 let mut save_path: Option<std::path::PathBuf> = None;
3662
3663 if let Some(opts) = options {
3664 // Capture the file path before consuming opts
3665 save_path = opts.path;
3666
3667 if let Some(scale) = opts.scale {
3668 params["scale"] = serde_json::json!(scale);
3669 }
3670 if let Some(v) = opts.display_header_footer {
3671 params["displayHeaderFooter"] = serde_json::json!(v);
3672 }
3673 if let Some(v) = opts.header_template {
3674 params["headerTemplate"] = serde_json::json!(v);
3675 }
3676 if let Some(v) = opts.footer_template {
3677 params["footerTemplate"] = serde_json::json!(v);
3678 }
3679 if let Some(v) = opts.print_background {
3680 params["printBackground"] = serde_json::json!(v);
3681 }
3682 if let Some(v) = opts.landscape {
3683 params["landscape"] = serde_json::json!(v);
3684 }
3685 if let Some(v) = opts.page_ranges {
3686 params["pageRanges"] = serde_json::json!(v);
3687 }
3688 if let Some(v) = opts.format {
3689 params["format"] = serde_json::json!(v);
3690 }
3691 if let Some(v) = opts.width {
3692 params["width"] = serde_json::json!(v);
3693 }
3694 if let Some(v) = opts.height {
3695 params["height"] = serde_json::json!(v);
3696 }
3697 if let Some(v) = opts.prefer_css_page_size {
3698 params["preferCSSPageSize"] = serde_json::json!(v);
3699 }
3700 if let Some(margin) = opts.margin {
3701 params["margin"] = serde_json::to_value(margin).map_err(|e| {
3702 crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
3703 })?;
3704 }
3705 }
3706
3707 #[derive(Deserialize)]
3708 struct PdfResponse {
3709 pdf: String,
3710 }
3711
3712 let response: PdfResponse = self.channel().send("pdf", params).await?;
3713
3714 // Decode base64 to bytes
3715 let pdf_bytes = base64::engine::general_purpose::STANDARD
3716 .decode(&response.pdf)
3717 .map_err(|e| {
3718 crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
3719 })?;
3720
3721 // If a path was specified, save the PDF to disk as well
3722 if let Some(path) = save_path {
3723 tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
3724 crate::error::Error::InvalidArgument(format!(
3725 "Failed to write PDF to '{}': {}",
3726 path.display(),
3727 e
3728 ))
3729 })?;
3730 }
3731
3732 Ok(pdf_bytes)
3733 }
3734
3735 /// Adds a `<script>` tag into the page with the desired URL or content.
3736 ///
3737 /// # Arguments
3738 ///
3739 /// * `options` - Optional script tag options (content, url, or path).
3740 /// If `None`, returns an error because no source is specified.
3741 ///
3742 /// At least one of `content`, `url`, or `path` must be provided.
3743 ///
3744 /// # Example
3745 ///
3746 /// ```no_run
3747 /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
3748 /// # #[tokio::main]
3749 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3750 /// # let playwright = Playwright::launch().await?;
3751 /// # let browser = playwright.chromium().launch().await?;
3752 /// # let context = browser.new_context().await?;
3753 /// # let page = context.new_page().await?;
3754 /// // With inline JavaScript
3755 /// page.add_script_tag(Some(
3756 /// AddScriptTagOptions::builder()
3757 /// .content("window.myVar = 42;")
3758 /// .build()
3759 /// )).await?;
3760 ///
3761 /// // With external URL
3762 /// page.add_script_tag(Some(
3763 /// AddScriptTagOptions::builder()
3764 /// .url("https://example.com/script.js")
3765 /// .build()
3766 /// )).await?;
3767 /// # Ok(())
3768 /// # }
3769 /// ```
3770 ///
3771 /// # Errors
3772 ///
3773 /// Returns error if:
3774 /// - `options` is `None` or no content/url/path is specified
3775 /// - Page has been closed
3776 /// - Script loading fails (e.g., invalid URL)
3777 ///
3778 /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
3779 pub async fn add_script_tag(
3780 &self,
3781 options: Option<AddScriptTagOptions>,
3782 ) -> Result<Arc<crate::protocol::ElementHandle>> {
3783 let opts = options.ok_or_else(|| {
3784 Error::InvalidArgument(
3785 "At least one of content, url, or path must be specified".to_string(),
3786 )
3787 })?;
3788 let frame = self.main_frame().await?;
3789 frame.add_script_tag(opts).await
3790 }
3791
3792 /// Returns the current viewport size of the page, or `None` if no viewport is set.
3793 ///
3794 /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
3795 /// returns the dimensions configured at context creation time or updated via
3796 /// `set_viewport_size()`.
3797 ///
3798 /// # Example
3799 ///
3800 /// ```ignore
3801 /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
3802 /// # #[tokio::main]
3803 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3804 /// # let playwright = Playwright::launch().await?;
3805 /// # let browser = playwright.chromium().launch().await?;
3806 /// let context = browser.new_context_with_options(
3807 /// BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
3808 /// ).await?;
3809 /// let page = context.new_page().await?;
3810 /// let size = page.viewport_size().expect("Viewport should be set");
3811 /// assert_eq!(size.width, 1280);
3812 /// assert_eq!(size.height, 720);
3813 /// # Ok(())
3814 /// # }
3815 /// ```
3816 ///
3817 /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
3818 pub fn viewport_size(&self) -> Option<Viewport> {
3819 self.viewport.read().ok()?.clone()
3820 }
3821
3822 /// Returns the `Accessibility` object for this page.
3823 ///
3824 /// Use `accessibility().snapshot()` to capture the current state of the
3825 /// page's accessibility tree.
3826 ///
3827 /// See: <https://playwright.dev/docs/api/class-page#page-accessibility>
3828 pub fn accessibility(&self) -> crate::protocol::Accessibility {
3829 crate::protocol::Accessibility::new(self.clone())
3830 }
3831
3832 /// Returns the `Coverage` object for this page (Chromium only).
3833 ///
3834 /// Use `coverage().start_js_coverage()` / `stop_js_coverage()` and
3835 /// `start_css_coverage()` / `stop_css_coverage()` to collect code coverage data.
3836 ///
3837 /// Coverage is only available in Chromium. Calling coverage methods on
3838 /// Firefox or WebKit will return an error from the Playwright server.
3839 ///
3840 /// See: <https://playwright.dev/docs/api/class-page#page-coverage>
3841 pub fn coverage(&self) -> crate::protocol::Coverage {
3842 crate::protocol::Coverage::new(self.clone())
3843 }
3844
3845 // Internal accessibility method (called by Accessibility struct)
3846 //
3847 // The legacy `accessibilitySnapshot` RPC was removed in modern Playwright.
3848 // We implement snapshot() using `FrameAriaSnapshot` on the main frame, which
3849 // returns the ARIA accessibility tree as a YAML string (the current equivalent).
3850 // The YAML string is returned as a JSON string Value for API compatibility.
3851
3852 pub(crate) async fn accessibility_snapshot(
3853 &self,
3854 _options: Option<crate::protocol::accessibility::AccessibilitySnapshotOptions>,
3855 ) -> Result<serde_json::Value> {
3856 let frame = self.main_frame().await?;
3857 let timeout = self.default_timeout_ms();
3858 let snapshot = frame.aria_snapshot_raw("body", timeout).await?;
3859 Ok(serde_json::Value::String(snapshot))
3860 }
3861
3862 // Internal coverage methods (called by Coverage struct)
3863
3864 pub(crate) async fn coverage_start_js(
3865 &self,
3866 options: Option<crate::protocol::coverage::StartJSCoverageOptions>,
3867 ) -> Result<()> {
3868 let mut params = serde_json::json!({});
3869
3870 if let Some(opts) = options {
3871 if let Some(v) = opts.reset_on_navigation {
3872 params["resetOnNavigation"] = serde_json::json!(v);
3873 }
3874 if let Some(v) = opts.report_anonymous_scripts {
3875 params["reportAnonymousScripts"] = serde_json::json!(v);
3876 }
3877 }
3878
3879 self.channel()
3880 .send_no_result("startJSCoverage", params)
3881 .await
3882 }
3883
3884 pub(crate) async fn coverage_stop_js(
3885 &self,
3886 ) -> Result<Vec<crate::protocol::coverage::JSCoverageEntry>> {
3887 #[derive(serde::Deserialize)]
3888 struct StopJSCoverageResponse {
3889 entries: Vec<crate::protocol::coverage::JSCoverageEntry>,
3890 }
3891
3892 let response: StopJSCoverageResponse = self
3893 .channel()
3894 .send("stopJSCoverage", serde_json::json!({}))
3895 .await?;
3896
3897 Ok(response.entries)
3898 }
3899
3900 pub(crate) async fn coverage_start_css(
3901 &self,
3902 options: Option<crate::protocol::coverage::StartCSSCoverageOptions>,
3903 ) -> Result<()> {
3904 let mut params = serde_json::json!({});
3905
3906 if let Some(opts) = options
3907 && let Some(v) = opts.reset_on_navigation
3908 {
3909 params["resetOnNavigation"] = serde_json::json!(v);
3910 }
3911
3912 self.channel()
3913 .send_no_result("startCSSCoverage", params)
3914 .await
3915 }
3916
3917 pub(crate) async fn coverage_stop_css(
3918 &self,
3919 ) -> Result<Vec<crate::protocol::coverage::CSSCoverageEntry>> {
3920 #[derive(serde::Deserialize)]
3921 struct StopCSSCoverageResponse {
3922 entries: Vec<crate::protocol::coverage::CSSCoverageEntry>,
3923 }
3924
3925 let response: StopCSSCoverageResponse = self
3926 .channel()
3927 .send("stopCSSCoverage", serde_json::json!({}))
3928 .await?;
3929
3930 Ok(response.entries)
3931 }
3932}
3933
3934impl ChannelOwner for Page {
3935 fn guid(&self) -> &str {
3936 self.base.guid()
3937 }
3938
3939 fn type_name(&self) -> &str {
3940 self.base.type_name()
3941 }
3942
3943 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
3944 self.base.parent()
3945 }
3946
3947 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
3948 self.base.connection()
3949 }
3950
3951 fn initializer(&self) -> &Value {
3952 self.base.initializer()
3953 }
3954
3955 fn channel(&self) -> &Channel {
3956 self.base.channel()
3957 }
3958
3959 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
3960 self.base.dispose(reason)
3961 }
3962
3963 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
3964 self.base.adopt(child)
3965 }
3966
3967 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
3968 self.base.add_child(guid, child)
3969 }
3970
3971 fn remove_child(&self, guid: &str) {
3972 self.base.remove_child(guid)
3973 }
3974
3975 fn on_event(&self, method: &str, params: Value) {
3976 match method {
3977 "navigated" => {
3978 // Update URL when page navigates
3979 if let Some(url_value) = params.get("url")
3980 && let Some(url_str) = url_value.as_str()
3981 && let Ok(mut url) = self.url.write()
3982 {
3983 *url = url_str.to_string();
3984 }
3985 }
3986 "route" => {
3987 // Handle network routing event
3988 if let Some(route_guid) = params
3989 .get("route")
3990 .and_then(|v| v.get("guid"))
3991 .and_then(|v| v.as_str())
3992 {
3993 // Get the Route object from connection's registry
3994 let connection = self.connection();
3995 let route_guid_owned = route_guid.to_string();
3996 let self_clone = self.clone();
3997
3998 tokio::spawn(async move {
3999 // Get and downcast Route object
4000 let route: Route =
4001 match connection.get_typed::<Route>(&route_guid_owned).await {
4002 Ok(r) => r,
4003 Err(e) => {
4004 tracing::warn!("Failed to get route object: {}", e);
4005 return;
4006 }
4007 };
4008
4009 // Set APIRequestContext on the route for fetch() support.
4010 // Page's parent is BrowserContext, which has the request context.
4011 if let Some(ctx) =
4012 downcast_parent::<crate::protocol::BrowserContext>(&self_clone)
4013 && let Ok(api_ctx) = ctx.request().await
4014 {
4015 route.set_api_request_context(api_ctx);
4016 }
4017
4018 // Call the route handler and wait for completion
4019 self_clone.on_route_event(route).await;
4020 });
4021 }
4022 }
4023 "download" => {
4024 // Handle download event
4025 // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
4026 let url = params
4027 .get("url")
4028 .and_then(|v| v.as_str())
4029 .unwrap_or("")
4030 .to_string();
4031
4032 let suggested_filename = params
4033 .get("suggestedFilename")
4034 .and_then(|v| v.as_str())
4035 .unwrap_or("")
4036 .to_string();
4037
4038 if let Some(artifact_guid) = params
4039 .get("artifact")
4040 .and_then(|v| v.get("guid"))
4041 .and_then(|v| v.as_str())
4042 {
4043 let connection = self.connection();
4044 let artifact_guid_owned = artifact_guid.to_string();
4045 let self_clone = self.clone();
4046
4047 tokio::spawn(async move {
4048 // Wait for Artifact object to be created
4049 let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
4050 Ok(obj) => obj,
4051 Err(e) => {
4052 tracing::warn!("Failed to get artifact object: {}", e);
4053 return;
4054 }
4055 };
4056
4057 // Create Download wrapper from Artifact + event params
4058 let download = Download::from_artifact(
4059 artifact_arc,
4060 url,
4061 suggested_filename,
4062 self_clone.clone(),
4063 );
4064
4065 // Call the download handlers
4066 self_clone.on_download_event(download).await;
4067 });
4068 }
4069 }
4070 "dialog" => {
4071 // Dialog events are handled by BrowserContext and forwarded to Page
4072 // This case should not be reached, but keeping for completeness
4073 }
4074 "webSocket" => {
4075 if let Some(ws_guid) = params
4076 .get("webSocket")
4077 .and_then(|v| v.get("guid"))
4078 .and_then(|v| v.as_str())
4079 {
4080 let connection = self.connection();
4081 let ws_guid_owned = ws_guid.to_string();
4082 let self_clone = self.clone();
4083
4084 tokio::spawn(async move {
4085 // Get and downcast WebSocket object
4086 let ws: WebSocket =
4087 match connection.get_typed::<WebSocket>(&ws_guid_owned).await {
4088 Ok(ws) => ws,
4089 Err(e) => {
4090 tracing::warn!("Failed to get WebSocket object: {}", e);
4091 return;
4092 }
4093 };
4094
4095 // Call handlers
4096 let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
4097 for handler in handlers {
4098 let ws_clone = ws.clone();
4099 tokio::spawn(async move {
4100 if let Err(e) = handler(ws_clone).await {
4101 tracing::error!("Error in websocket handler: {}", e);
4102 }
4103 });
4104 }
4105 });
4106 }
4107 }
4108 "webSocketRoute" => {
4109 // A WebSocket matched a route_web_socket pattern.
4110 // Event format: {webSocketRoute: {guid: "WebSocketRoute@..."}}
4111 if let Some(wsr_guid) = params
4112 .get("webSocketRoute")
4113 .and_then(|v| v.get("guid"))
4114 .and_then(|v| v.as_str())
4115 {
4116 let connection = self.connection();
4117 let wsr_guid_owned = wsr_guid.to_string();
4118 let self_clone = self.clone();
4119
4120 tokio::spawn(async move {
4121 let route: crate::protocol::WebSocketRoute = match connection
4122 .get_typed::<crate::protocol::WebSocketRoute>(&wsr_guid_owned)
4123 .await
4124 {
4125 Ok(r) => r,
4126 Err(e) => {
4127 tracing::warn!("Failed to get WebSocketRoute object: {}", e);
4128 return;
4129 }
4130 };
4131
4132 let url = route.url().to_string();
4133 let handlers = self_clone.ws_route_handlers.lock().unwrap().clone();
4134 for entry in handlers.iter().rev() {
4135 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
4136 let handler = entry.handler.clone();
4137 let route_clone = route.clone();
4138 tokio::spawn(async move {
4139 if let Err(e) = handler(route_clone).await {
4140 tracing::error!("Error in webSocketRoute handler: {}", e);
4141 }
4142 });
4143 break;
4144 }
4145 }
4146 });
4147 }
4148 }
4149 "worker" => {
4150 // A new Web Worker was created in the page.
4151 // Event format: {worker: {guid: "Worker@..."}}
4152 if let Some(worker_guid) = params
4153 .get("worker")
4154 .and_then(|v| v.get("guid"))
4155 .and_then(|v| v.as_str())
4156 {
4157 let connection = self.connection();
4158 let worker_guid_owned = worker_guid.to_string();
4159 let self_clone = self.clone();
4160
4161 tokio::spawn(async move {
4162 let worker: Worker =
4163 match connection.get_typed::<Worker>(&worker_guid_owned).await {
4164 Ok(w) => w,
4165 Err(e) => {
4166 tracing::warn!("Failed to get Worker object: {}", e);
4167 return;
4168 }
4169 };
4170
4171 // Track the worker for workers() accessor
4172 self_clone.workers_list.lock().unwrap().push(worker.clone());
4173
4174 let handlers = self_clone.worker_handlers.lock().unwrap().clone();
4175 for handler in handlers {
4176 let worker_clone = worker.clone();
4177 tokio::spawn(async move {
4178 if let Err(e) = handler(worker_clone).await {
4179 tracing::error!("Error in worker handler: {}", e);
4180 }
4181 });
4182 }
4183 // Notify expect_event("worker") waiters
4184 if let Some(tx) = self_clone.worker_waiters.lock().unwrap().pop() {
4185 let _ = tx.send(worker);
4186 }
4187 });
4188 }
4189 }
4190 "bindingCall" => {
4191 // A JS caller on this page invoked a page-level exposed function.
4192 // Event format: {binding: {guid: "..."}}
4193 if let Some(binding_guid) = params
4194 .get("binding")
4195 .and_then(|v| v.get("guid"))
4196 .and_then(|v| v.as_str())
4197 {
4198 let connection = self.connection();
4199 let binding_guid_owned = binding_guid.to_string();
4200 let binding_callbacks = self.binding_callbacks.clone();
4201
4202 tokio::spawn(async move {
4203 let binding_call: crate::protocol::BindingCall = match connection
4204 .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
4205 .await
4206 {
4207 Ok(bc) => bc,
4208 Err(e) => {
4209 tracing::warn!("Failed to get BindingCall object: {}", e);
4210 return;
4211 }
4212 };
4213
4214 let name = binding_call.name().to_string();
4215
4216 // Look up page-level callback
4217 let callback = {
4218 let callbacks = binding_callbacks.lock().unwrap();
4219 callbacks.get(&name).cloned()
4220 };
4221
4222 let Some(callback) = callback else {
4223 // No page-level handler — the context-level handler on
4224 // BrowserContext::on_event("bindingCall") will handle it.
4225 return;
4226 };
4227
4228 // Deserialize args from Playwright protocol format
4229 let raw_args = binding_call.args();
4230 let args = crate::protocol::browser_context::BrowserContext::deserialize_binding_args_pub(raw_args);
4231
4232 // Call callback and serialize result
4233 let result_value = callback(args).await;
4234 let serialized =
4235 crate::protocol::evaluate_conversion::serialize_argument(&result_value);
4236
4237 if let Err(e) = binding_call.resolve(serialized).await {
4238 tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
4239 }
4240 });
4241 }
4242 }
4243 "fileChooser" => {
4244 // FileChooser event: sent when an <input type="file"> is interacted with.
4245 // Event params: {element: {guid: "..."}, isMultiple: bool}
4246 let is_multiple = params
4247 .get("isMultiple")
4248 .and_then(|v| v.as_bool())
4249 .unwrap_or(false);
4250
4251 if let Some(element_guid) = params
4252 .get("element")
4253 .and_then(|v| v.get("guid"))
4254 .and_then(|v| v.as_str())
4255 {
4256 let connection = self.connection();
4257 let element_guid_owned = element_guid.to_string();
4258 let self_clone = self.clone();
4259
4260 tokio::spawn(async move {
4261 let element: crate::protocol::ElementHandle = match connection
4262 .get_typed::<crate::protocol::ElementHandle>(&element_guid_owned)
4263 .await
4264 {
4265 Ok(e) => e,
4266 Err(err) => {
4267 tracing::warn!(
4268 "Failed to get ElementHandle for fileChooser: {}",
4269 err
4270 );
4271 return;
4272 }
4273 };
4274
4275 let chooser = crate::protocol::FileChooser::new(
4276 self_clone.clone(),
4277 std::sync::Arc::new(element),
4278 is_multiple,
4279 );
4280
4281 self_clone.on_filechooser_event(chooser).await;
4282 });
4283 }
4284 }
4285 "close" => {
4286 // Server-initiated close (e.g. context was closed)
4287 self.is_closed.store(true, Ordering::Relaxed);
4288 // Dispatch close handlers
4289 let self_clone = self.clone();
4290 tokio::spawn(async move {
4291 self_clone.on_close_event().await;
4292 });
4293 }
4294 "load" => {
4295 let self_clone = self.clone();
4296 tokio::spawn(async move {
4297 self_clone.on_load_event().await;
4298 });
4299 }
4300 "crash" => {
4301 let self_clone = self.clone();
4302 tokio::spawn(async move {
4303 self_clone.on_crash_event().await;
4304 });
4305 }
4306 "pageError" => {
4307 // params: {"error": {"message": "...", "stack": "..."}}
4308 let message = params
4309 .get("error")
4310 .and_then(|e| e.get("message"))
4311 .and_then(|m| m.as_str())
4312 .unwrap_or("")
4313 .to_string();
4314 let self_clone = self.clone();
4315 tokio::spawn(async move {
4316 self_clone.on_pageerror_event(message).await;
4317 });
4318 }
4319 // "popup" is forwarded from BrowserContext::on_event when a "page" event
4320 // is received for a page that has an opener. No direct "popup" event on Page.
4321 "frameAttached" => {
4322 // params: {"frame": {"guid": "..."}}
4323 if let Some(frame_guid) = params
4324 .get("frame")
4325 .and_then(|v| v.get("guid"))
4326 .and_then(|v| v.as_str())
4327 {
4328 let connection = self.connection();
4329 let frame_guid_owned = frame_guid.to_string();
4330 let self_clone = self.clone();
4331
4332 tokio::spawn(async move {
4333 let frame: crate::protocol::Frame = match connection
4334 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
4335 .await
4336 {
4337 Ok(f) => f,
4338 Err(e) => {
4339 tracing::warn!("Failed to get Frame for frameAttached: {}", e);
4340 return;
4341 }
4342 };
4343 self_clone.on_frameattached_event(frame).await;
4344 });
4345 }
4346 }
4347 "frameDetached" => {
4348 // params: {"frame": {"guid": "..."}}
4349 if let Some(frame_guid) = params
4350 .get("frame")
4351 .and_then(|v| v.get("guid"))
4352 .and_then(|v| v.as_str())
4353 {
4354 let connection = self.connection();
4355 let frame_guid_owned = frame_guid.to_string();
4356 let self_clone = self.clone();
4357
4358 tokio::spawn(async move {
4359 let frame: crate::protocol::Frame = match connection
4360 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
4361 .await
4362 {
4363 Ok(f) => f,
4364 Err(e) => {
4365 tracing::warn!("Failed to get Frame for frameDetached: {}", e);
4366 return;
4367 }
4368 };
4369 self_clone.on_framedetached_event(frame).await;
4370 });
4371 }
4372 }
4373 "frameNavigated" => {
4374 // params: {"frame": {"guid": "..."}}
4375 // Note: frameNavigated may also contain url, name, etc. at top level
4376 // The frame guid is in the "frame" field (same as attached/detached)
4377 if let Some(frame_guid) = params
4378 .get("frame")
4379 .and_then(|v| v.get("guid"))
4380 .and_then(|v| v.as_str())
4381 {
4382 let connection = self.connection();
4383 let frame_guid_owned = frame_guid.to_string();
4384 let self_clone = self.clone();
4385
4386 tokio::spawn(async move {
4387 let frame: crate::protocol::Frame = match connection
4388 .get_typed::<crate::protocol::Frame>(&frame_guid_owned)
4389 .await
4390 {
4391 Ok(f) => f,
4392 Err(e) => {
4393 tracing::warn!("Failed to get Frame for frameNavigated: {}", e);
4394 return;
4395 }
4396 };
4397 self_clone.on_framenavigated_event(frame).await;
4398 });
4399 }
4400 }
4401 "locatorHandlerTriggered" => {
4402 // Server fires this when a registered locator matches an element.
4403 // params: {"uid": N}
4404 if let Some(uid) = params.get("uid").and_then(|v| v.as_u64()).map(|v| v as u32) {
4405 let locator_handlers = self.locator_handlers.clone();
4406 let self_clone = self.clone();
4407
4408 tokio::spawn(async move {
4409 // Look up handler and decrement times_remaining
4410 let (handler, selector, should_remove) = {
4411 let mut handlers = locator_handlers.lock().unwrap();
4412 let entry = handlers.iter_mut().find(|e| e.uid == uid);
4413 match entry {
4414 None => return,
4415 Some(e) => {
4416 let handler = e.handler.clone();
4417 let selector = e.selector.clone();
4418 let remove = match e.times_remaining {
4419 Some(1) => true,
4420 Some(ref mut n) => {
4421 *n -= 1;
4422 false
4423 }
4424 None => false,
4425 };
4426 (handler, selector, remove)
4427 }
4428 }
4429 };
4430
4431 // Build a Locator for the handler to receive
4432 let locator = self_clone.locator(&selector).await;
4433
4434 // Run the handler
4435 if let Err(e) = handler(locator).await {
4436 tracing::warn!("locator handler error (uid={}): {}", uid, e);
4437 }
4438
4439 // Send resolveLocatorHandler — remove=true if times exhausted
4440 let _ = self_clone
4441 .channel()
4442 .send_no_result(
4443 "resolveLocatorHandler",
4444 serde_json::json!({ "uid": uid, "remove": should_remove }),
4445 )
4446 .await;
4447
4448 // Remove from local registry if one-shot
4449 if should_remove {
4450 self_clone
4451 .locator_handlers
4452 .lock()
4453 .unwrap()
4454 .retain(|e| e.uid != uid);
4455 }
4456 });
4457 }
4458 }
4459 _ => {
4460 // Other events not yet handled
4461 }
4462 }
4463 }
4464
4465 fn was_collected(&self) -> bool {
4466 self.base.was_collected()
4467 }
4468
4469 fn as_any(&self) -> &dyn Any {
4470 self
4471 }
4472}
4473
4474impl std::fmt::Debug for Page {
4475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4476 f.debug_struct("Page")
4477 .field("guid", &self.guid())
4478 .field("url", &self.url())
4479 .finish()
4480 }
4481}
4482
4483/// Options for page.goto() and page.reload()
4484#[derive(Debug, Clone)]
4485pub struct GotoOptions {
4486 /// Maximum operation time in milliseconds
4487 pub timeout: Option<std::time::Duration>,
4488 /// When to consider operation succeeded
4489 pub wait_until: Option<WaitUntil>,
4490}
4491
4492impl GotoOptions {
4493 /// Creates new GotoOptions with default values
4494 pub fn new() -> Self {
4495 Self {
4496 timeout: None,
4497 wait_until: None,
4498 }
4499 }
4500
4501 /// Sets the timeout
4502 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
4503 self.timeout = Some(timeout);
4504 self
4505 }
4506
4507 /// Sets the wait_until option
4508 pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
4509 self.wait_until = Some(wait_until);
4510 self
4511 }
4512}
4513
4514impl Default for GotoOptions {
4515 fn default() -> Self {
4516 Self::new()
4517 }
4518}
4519
4520/// When to consider navigation succeeded
4521#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4522pub enum WaitUntil {
4523 /// Consider operation to be finished when the `load` event is fired
4524 Load,
4525 /// Consider operation to be finished when the `DOMContentLoaded` event is fired
4526 DomContentLoaded,
4527 /// Consider operation to be finished when there are no network connections for at least 500ms
4528 NetworkIdle,
4529 /// Consider operation to be finished when the commit event is fired
4530 Commit,
4531}
4532
4533impl WaitUntil {
4534 pub(crate) fn as_str(&self) -> &'static str {
4535 match self {
4536 WaitUntil::Load => "load",
4537 WaitUntil::DomContentLoaded => "domcontentloaded",
4538 WaitUntil::NetworkIdle => "networkidle",
4539 WaitUntil::Commit => "commit",
4540 }
4541 }
4542}
4543
4544/// Options for adding a style tag to the page
4545///
4546/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
4547#[derive(Debug, Clone, Default)]
4548pub struct AddStyleTagOptions {
4549 /// Raw CSS content to inject
4550 pub content: Option<String>,
4551 /// URL of the `<link>` tag to add
4552 pub url: Option<String>,
4553 /// Path to a CSS file to inject
4554 pub path: Option<String>,
4555}
4556
4557impl AddStyleTagOptions {
4558 /// Creates a new builder for AddStyleTagOptions
4559 pub fn builder() -> AddStyleTagOptionsBuilder {
4560 AddStyleTagOptionsBuilder::default()
4561 }
4562
4563 /// Validates that at least one option is specified
4564 pub(crate) fn validate(&self) -> Result<()> {
4565 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
4566 return Err(Error::InvalidArgument(
4567 "At least one of content, url, or path must be specified".to_string(),
4568 ));
4569 }
4570 Ok(())
4571 }
4572}
4573
4574/// Builder for AddStyleTagOptions
4575#[derive(Debug, Clone, Default)]
4576pub struct AddStyleTagOptionsBuilder {
4577 content: Option<String>,
4578 url: Option<String>,
4579 path: Option<String>,
4580}
4581
4582impl AddStyleTagOptionsBuilder {
4583 /// Sets the CSS content to inject
4584 pub fn content(mut self, content: impl Into<String>) -> Self {
4585 self.content = Some(content.into());
4586 self
4587 }
4588
4589 /// Sets the URL of the stylesheet
4590 pub fn url(mut self, url: impl Into<String>) -> Self {
4591 self.url = Some(url.into());
4592 self
4593 }
4594
4595 /// Sets the path to a CSS file
4596 pub fn path(mut self, path: impl Into<String>) -> Self {
4597 self.path = Some(path.into());
4598 self
4599 }
4600
4601 /// Builds the AddStyleTagOptions
4602 pub fn build(self) -> AddStyleTagOptions {
4603 AddStyleTagOptions {
4604 content: self.content,
4605 url: self.url,
4606 path: self.path,
4607 }
4608 }
4609}
4610
4611// ============================================================================
4612// AddScriptTagOptions
4613// ============================================================================
4614
4615/// Options for adding a `<script>` tag to the page.
4616///
4617/// At least one of `content`, `url`, or `path` must be specified.
4618///
4619/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
4620#[derive(Debug, Clone, Default)]
4621pub struct AddScriptTagOptions {
4622 /// Raw JavaScript content to inject
4623 pub content: Option<String>,
4624 /// URL of the `<script>` tag to add
4625 pub url: Option<String>,
4626 /// Path to a JavaScript file to inject (file contents will be read and sent as content)
4627 pub path: Option<String>,
4628 /// Script type attribute (e.g., `"module"`)
4629 pub type_: Option<String>,
4630}
4631
4632impl AddScriptTagOptions {
4633 /// Creates a new builder for AddScriptTagOptions
4634 pub fn builder() -> AddScriptTagOptionsBuilder {
4635 AddScriptTagOptionsBuilder::default()
4636 }
4637
4638 /// Validates that at least one option is specified
4639 pub(crate) fn validate(&self) -> Result<()> {
4640 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
4641 return Err(Error::InvalidArgument(
4642 "At least one of content, url, or path must be specified".to_string(),
4643 ));
4644 }
4645 Ok(())
4646 }
4647}
4648
4649/// Builder for AddScriptTagOptions
4650#[derive(Debug, Clone, Default)]
4651pub struct AddScriptTagOptionsBuilder {
4652 content: Option<String>,
4653 url: Option<String>,
4654 path: Option<String>,
4655 type_: Option<String>,
4656}
4657
4658impl AddScriptTagOptionsBuilder {
4659 /// Sets the JavaScript content to inject
4660 pub fn content(mut self, content: impl Into<String>) -> Self {
4661 self.content = Some(content.into());
4662 self
4663 }
4664
4665 /// Sets the URL of the script to load
4666 pub fn url(mut self, url: impl Into<String>) -> Self {
4667 self.url = Some(url.into());
4668 self
4669 }
4670
4671 /// Sets the path to a JavaScript file to inject
4672 pub fn path(mut self, path: impl Into<String>) -> Self {
4673 self.path = Some(path.into());
4674 self
4675 }
4676
4677 /// Sets the script type attribute (e.g., `"module"`)
4678 pub fn type_(mut self, type_: impl Into<String>) -> Self {
4679 self.type_ = Some(type_.into());
4680 self
4681 }
4682
4683 /// Builds the AddScriptTagOptions
4684 pub fn build(self) -> AddScriptTagOptions {
4685 AddScriptTagOptions {
4686 content: self.content,
4687 url: self.url,
4688 path: self.path,
4689 type_: self.type_,
4690 }
4691 }
4692}
4693
4694// ============================================================================
4695// EmulateMediaOptions and related enums
4696// ============================================================================
4697
4698/// Media type for `page.emulate_media()`.
4699///
4700/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
4701#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4702#[serde(rename_all = "lowercase")]
4703pub enum Media {
4704 /// Emulate screen media type
4705 Screen,
4706 /// Emulate print media type
4707 Print,
4708 /// Reset media emulation to browser default (sends `"no-override"` to protocol)
4709 #[serde(rename = "no-override")]
4710 NoOverride,
4711}
4712
4713/// Preferred color scheme for `page.emulate_media()`.
4714///
4715/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
4716#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4717pub enum ColorScheme {
4718 /// Emulate light color scheme
4719 #[serde(rename = "light")]
4720 Light,
4721 /// Emulate dark color scheme
4722 #[serde(rename = "dark")]
4723 Dark,
4724 /// Emulate no preference for color scheme
4725 #[serde(rename = "no-preference")]
4726 NoPreference,
4727 /// Reset color scheme to browser default
4728 #[serde(rename = "no-override")]
4729 NoOverride,
4730}
4731
4732/// Reduced motion preference for `page.emulate_media()`.
4733///
4734/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
4735#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4736pub enum ReducedMotion {
4737 /// Emulate reduced motion preference
4738 #[serde(rename = "reduce")]
4739 Reduce,
4740 /// Emulate no preference for reduced motion
4741 #[serde(rename = "no-preference")]
4742 NoPreference,
4743 /// Reset reduced motion to browser default
4744 #[serde(rename = "no-override")]
4745 NoOverride,
4746}
4747
4748/// Forced colors preference for `page.emulate_media()`.
4749///
4750/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
4751#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4752pub enum ForcedColors {
4753 /// Emulate active forced colors
4754 #[serde(rename = "active")]
4755 Active,
4756 /// Emulate no forced colors
4757 #[serde(rename = "none")]
4758 None_,
4759 /// Reset forced colors to browser default
4760 #[serde(rename = "no-override")]
4761 NoOverride,
4762}
4763
4764/// Options for `page.emulate_media()`.
4765///
4766/// All fields are optional. Fields that are `None` are omitted from the protocol
4767/// message (meaning they are not changed). To reset a field to browser default,
4768/// use the `NoOverride` variant.
4769///
4770/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
4771#[derive(Debug, Clone, Default)]
4772pub struct EmulateMediaOptions {
4773 /// Media type to emulate (screen, print, or no-override)
4774 pub media: Option<Media>,
4775 /// Color scheme preference to emulate
4776 pub color_scheme: Option<ColorScheme>,
4777 /// Reduced motion preference to emulate
4778 pub reduced_motion: Option<ReducedMotion>,
4779 /// Forced colors preference to emulate
4780 pub forced_colors: Option<ForcedColors>,
4781}
4782
4783impl EmulateMediaOptions {
4784 /// Creates a new builder for EmulateMediaOptions
4785 pub fn builder() -> EmulateMediaOptionsBuilder {
4786 EmulateMediaOptionsBuilder::default()
4787 }
4788}
4789
4790/// Builder for EmulateMediaOptions
4791#[derive(Debug, Clone, Default)]
4792pub struct EmulateMediaOptionsBuilder {
4793 media: Option<Media>,
4794 color_scheme: Option<ColorScheme>,
4795 reduced_motion: Option<ReducedMotion>,
4796 forced_colors: Option<ForcedColors>,
4797}
4798
4799impl EmulateMediaOptionsBuilder {
4800 /// Sets the media type to emulate
4801 pub fn media(mut self, media: Media) -> Self {
4802 self.media = Some(media);
4803 self
4804 }
4805
4806 /// Sets the color scheme preference
4807 pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
4808 self.color_scheme = Some(color_scheme);
4809 self
4810 }
4811
4812 /// Sets the reduced motion preference
4813 pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
4814 self.reduced_motion = Some(reduced_motion);
4815 self
4816 }
4817
4818 /// Sets the forced colors preference
4819 pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
4820 self.forced_colors = Some(forced_colors);
4821 self
4822 }
4823
4824 /// Builds the EmulateMediaOptions
4825 pub fn build(self) -> EmulateMediaOptions {
4826 EmulateMediaOptions {
4827 media: self.media,
4828 color_scheme: self.color_scheme,
4829 reduced_motion: self.reduced_motion,
4830 forced_colors: self.forced_colors,
4831 }
4832 }
4833}
4834
4835// ============================================================================
4836// PdfOptions
4837// ============================================================================
4838
4839/// Margin options for PDF generation.
4840///
4841/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
4842#[derive(Debug, Clone, Default, Serialize)]
4843pub struct PdfMargin {
4844 /// Top margin (e.g. `"1in"`)
4845 #[serde(skip_serializing_if = "Option::is_none")]
4846 pub top: Option<String>,
4847 /// Right margin
4848 #[serde(skip_serializing_if = "Option::is_none")]
4849 pub right: Option<String>,
4850 /// Bottom margin
4851 #[serde(skip_serializing_if = "Option::is_none")]
4852 pub bottom: Option<String>,
4853 /// Left margin
4854 #[serde(skip_serializing_if = "Option::is_none")]
4855 pub left: Option<String>,
4856}
4857
4858/// Options for generating a PDF from a page.
4859///
4860/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
4861/// Firefox or WebKit will result in an error.
4862///
4863/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
4864#[derive(Debug, Clone, Default)]
4865pub struct PdfOptions {
4866 /// If specified, the PDF will also be saved to this file path.
4867 pub path: Option<std::path::PathBuf>,
4868 /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
4869 pub scale: Option<f64>,
4870 /// Whether to display header and footer (default false).
4871 pub display_header_footer: Option<bool>,
4872 /// HTML template for the print header. Should be valid HTML.
4873 pub header_template: Option<String>,
4874 /// HTML template for the print footer.
4875 pub footer_template: Option<String>,
4876 /// Whether to print background graphics (default false).
4877 pub print_background: Option<bool>,
4878 /// Paper orientation — `true` for landscape (default false).
4879 pub landscape: Option<bool>,
4880 /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
4881 pub page_ranges: Option<String>,
4882 /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
4883 pub format: Option<String>,
4884 /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
4885 pub width: Option<String>,
4886 /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
4887 pub height: Option<String>,
4888 /// Whether or not to prefer page size as defined by CSS.
4889 pub prefer_css_page_size: Option<bool>,
4890 /// Paper margins, defaulting to none.
4891 pub margin: Option<PdfMargin>,
4892}
4893
4894impl PdfOptions {
4895 /// Creates a new builder for PdfOptions
4896 pub fn builder() -> PdfOptionsBuilder {
4897 PdfOptionsBuilder::default()
4898 }
4899}
4900
4901/// Builder for PdfOptions
4902#[derive(Debug, Clone, Default)]
4903pub struct PdfOptionsBuilder {
4904 path: Option<std::path::PathBuf>,
4905 scale: Option<f64>,
4906 display_header_footer: Option<bool>,
4907 header_template: Option<String>,
4908 footer_template: Option<String>,
4909 print_background: Option<bool>,
4910 landscape: Option<bool>,
4911 page_ranges: Option<String>,
4912 format: Option<String>,
4913 width: Option<String>,
4914 height: Option<String>,
4915 prefer_css_page_size: Option<bool>,
4916 margin: Option<PdfMargin>,
4917}
4918
4919impl PdfOptionsBuilder {
4920 /// Sets the file path for saving the PDF
4921 pub fn path(mut self, path: std::path::PathBuf) -> Self {
4922 self.path = Some(path);
4923 self
4924 }
4925
4926 /// Sets the scale of the webpage rendering
4927 pub fn scale(mut self, scale: f64) -> Self {
4928 self.scale = Some(scale);
4929 self
4930 }
4931
4932 /// Sets whether to display header and footer
4933 pub fn display_header_footer(mut self, display: bool) -> Self {
4934 self.display_header_footer = Some(display);
4935 self
4936 }
4937
4938 /// Sets the HTML template for the print header
4939 pub fn header_template(mut self, template: impl Into<String>) -> Self {
4940 self.header_template = Some(template.into());
4941 self
4942 }
4943
4944 /// Sets the HTML template for the print footer
4945 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
4946 self.footer_template = Some(template.into());
4947 self
4948 }
4949
4950 /// Sets whether to print background graphics
4951 pub fn print_background(mut self, print: bool) -> Self {
4952 self.print_background = Some(print);
4953 self
4954 }
4955
4956 /// Sets whether to use landscape orientation
4957 pub fn landscape(mut self, landscape: bool) -> Self {
4958 self.landscape = Some(landscape);
4959 self
4960 }
4961
4962 /// Sets the page ranges to print
4963 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
4964 self.page_ranges = Some(ranges.into());
4965 self
4966 }
4967
4968 /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
4969 pub fn format(mut self, format: impl Into<String>) -> Self {
4970 self.format = Some(format.into());
4971 self
4972 }
4973
4974 /// Sets the paper width
4975 pub fn width(mut self, width: impl Into<String>) -> Self {
4976 self.width = Some(width.into());
4977 self
4978 }
4979
4980 /// Sets the paper height
4981 pub fn height(mut self, height: impl Into<String>) -> Self {
4982 self.height = Some(height.into());
4983 self
4984 }
4985
4986 /// Sets whether to prefer page size as defined by CSS
4987 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
4988 self.prefer_css_page_size = Some(prefer);
4989 self
4990 }
4991
4992 /// Sets the paper margins
4993 pub fn margin(mut self, margin: PdfMargin) -> Self {
4994 self.margin = Some(margin);
4995 self
4996 }
4997
4998 /// Builds the PdfOptions
4999 pub fn build(self) -> PdfOptions {
5000 PdfOptions {
5001 path: self.path,
5002 scale: self.scale,
5003 display_header_footer: self.display_header_footer,
5004 header_template: self.header_template,
5005 footer_template: self.footer_template,
5006 print_background: self.print_background,
5007 landscape: self.landscape,
5008 page_ranges: self.page_ranges,
5009 format: self.format,
5010 width: self.width,
5011 height: self.height,
5012 prefer_css_page_size: self.prefer_css_page_size,
5013 margin: self.margin,
5014 }
5015 }
5016}
5017
5018/// Response from navigation operations.
5019///
5020/// Returned from `page.goto()`, `page.reload()`, `page.go_back()`, and similar
5021/// navigation methods. Provides access to the HTTP response status, headers, and body.
5022///
5023/// See: <https://playwright.dev/docs/api/class-response>
5024#[derive(Clone)]
5025pub struct Response {
5026 url: String,
5027 status: u16,
5028 status_text: String,
5029 ok: bool,
5030 headers: std::collections::HashMap<String, String>,
5031 /// Reference to the backing channel owner for RPC calls (body, rawHeaders, etc.)
5032 /// Stored as the generic trait object so it can be downcast to ResponseObject when needed.
5033 response_channel_owner: Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
5034}
5035
5036impl Response {
5037 /// Creates a new Response from protocol data.
5038 ///
5039 /// This is used internally when constructing a Response from the protocol
5040 /// initializer (e.g., after `goto` or `reload`).
5041 pub(crate) fn new(
5042 url: String,
5043 status: u16,
5044 status_text: String,
5045 headers: std::collections::HashMap<String, String>,
5046 response_channel_owner: Option<
5047 std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>,
5048 >,
5049 ) -> Self {
5050 Self {
5051 url,
5052 status,
5053 status_text,
5054 ok: (200..300).contains(&status),
5055 headers,
5056 response_channel_owner,
5057 }
5058 }
5059}
5060
5061impl Response {
5062 /// Returns the URL of the response.
5063 ///
5064 /// See: <https://playwright.dev/docs/api/class-response#response-url>
5065 pub fn url(&self) -> &str {
5066 &self.url
5067 }
5068
5069 /// Returns the HTTP status code.
5070 ///
5071 /// See: <https://playwright.dev/docs/api/class-response#response-status>
5072 pub fn status(&self) -> u16 {
5073 self.status
5074 }
5075
5076 /// Returns the HTTP status text.
5077 ///
5078 /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
5079 pub fn status_text(&self) -> &str {
5080 &self.status_text
5081 }
5082
5083 /// Returns whether the response was successful (status 200-299).
5084 ///
5085 /// See: <https://playwright.dev/docs/api/class-response#response-ok>
5086 pub fn ok(&self) -> bool {
5087 self.ok
5088 }
5089
5090 /// Returns the response headers as a HashMap.
5091 ///
5092 /// Note: these are the headers from the protocol initializer. For the full
5093 /// raw headers (including duplicates), use `headers_array()` or `all_headers()`.
5094 ///
5095 /// See: <https://playwright.dev/docs/api/class-response#response-headers>
5096 pub fn headers(&self) -> &std::collections::HashMap<String, String> {
5097 &self.headers
5098 }
5099
5100 /// Returns the [`Request`] that triggered this response.
5101 ///
5102 /// Navigates the protocol object hierarchy: ResponseObject → parent (Request).
5103 ///
5104 /// See: <https://playwright.dev/docs/api/class-response#response-request>
5105 pub fn request(&self) -> Option<crate::protocol::Request> {
5106 let owner = self.response_channel_owner.as_ref()?;
5107 downcast_parent::<crate::protocol::Request>(&**owner)
5108 }
5109
5110 /// Returns the [`Frame`](crate::protocol::Frame) that initiated the request for this response.
5111 ///
5112 /// Navigates the protocol object hierarchy: ResponseObject → Request → Frame.
5113 ///
5114 /// See: <https://playwright.dev/docs/api/class-response#response-frame>
5115 pub fn frame(&self) -> Option<crate::protocol::Frame> {
5116 let request = self.request()?;
5117 request.frame()
5118 }
5119
5120 /// Returns the backing `ResponseObject`, or an error if unavailable.
5121 pub(crate) fn response_object(&self) -> crate::error::Result<crate::protocol::ResponseObject> {
5122 let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
5123 crate::error::Error::ProtocolError(
5124 "Response has no backing protocol object".to_string(),
5125 )
5126 })?;
5127 arc.as_any()
5128 .downcast_ref::<crate::protocol::ResponseObject>()
5129 .cloned()
5130 .ok_or_else(|| crate::error::Error::TypeMismatch {
5131 guid: arc.guid().to_string(),
5132 expected: "ResponseObject".to_string(),
5133 actual: arc.type_name().to_string(),
5134 })
5135 }
5136
5137 /// Returns TLS/SSL security details for HTTPS connections, or `None` for HTTP.
5138 ///
5139 /// See: <https://playwright.dev/docs/api/class-response#response-security-details>
5140 pub async fn security_details(
5141 &self,
5142 ) -> crate::error::Result<Option<crate::protocol::response::SecurityDetails>> {
5143 self.response_object()?.security_details().await
5144 }
5145
5146 /// Returns the server's IP address and port, or `None`.
5147 ///
5148 /// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
5149 pub async fn server_addr(
5150 &self,
5151 ) -> crate::error::Result<Option<crate::protocol::response::RemoteAddr>> {
5152 self.response_object()?.server_addr().await
5153 }
5154
5155 /// Waits for this response to finish loading.
5156 ///
5157 /// For responses obtained from navigation methods (`goto`, `reload`), the response
5158 /// is already finished when returned. For responses from `on_response` handlers,
5159 /// the body may still be loading.
5160 ///
5161 /// See: <https://playwright.dev/docs/api/class-response#response-finished>
5162 pub async fn finished(&self) -> crate::error::Result<()> {
5163 // The Playwright protocol dispatches `requestFinished` as a separate event
5164 // rather than exposing a `finished` RPC method on Response.
5165 // For responses from goto/reload, the response is already complete.
5166 // TODO: For on_response handlers, implement proper waiting via requestFinished event.
5167 Ok(())
5168 }
5169
5170 /// Returns the HTTP version used by this response (e.g. `"HTTP/1.1"` or `"HTTP/2.0"`).
5171 ///
5172 /// Makes an RPC call to the Playwright server.
5173 ///
5174 /// # Errors
5175 ///
5176 /// Returns an error if:
5177 /// - No backing protocol object is available (edge case)
5178 /// - The RPC call to the server fails
5179 ///
5180 /// See: <https://playwright.dev/docs/api/class-response#response-http-version>
5181 pub async fn http_version(&self) -> crate::error::Result<String> {
5182 self.response_object()?.http_version().await
5183 }
5184
5185 /// Returns the response body as raw bytes.
5186 ///
5187 /// Makes an RPC call to the Playwright server to fetch the response body.
5188 ///
5189 /// # Errors
5190 ///
5191 /// Returns an error if:
5192 /// - No backing protocol object is available (edge case)
5193 /// - The RPC call to the server fails
5194 /// - The base64 response cannot be decoded
5195 ///
5196 /// See: <https://playwright.dev/docs/api/class-response#response-body>
5197 pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
5198 self.response_object()?.body().await
5199 }
5200
5201 /// Returns the response body as a UTF-8 string.
5202 ///
5203 /// Calls `body()` then converts bytes to a UTF-8 string.
5204 ///
5205 /// # Errors
5206 ///
5207 /// Returns an error if:
5208 /// - `body()` fails
5209 /// - The body is not valid UTF-8
5210 ///
5211 /// See: <https://playwright.dev/docs/api/class-response#response-text>
5212 pub async fn text(&self) -> crate::error::Result<String> {
5213 let bytes = self.body().await?;
5214 String::from_utf8(bytes).map_err(|e| {
5215 crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
5216 })
5217 }
5218
5219 /// Parses the response body as JSON and deserializes it into type `T`.
5220 ///
5221 /// Calls `text()` then uses `serde_json` to deserialize the body.
5222 ///
5223 /// # Errors
5224 ///
5225 /// Returns an error if:
5226 /// - `text()` fails
5227 /// - The body is not valid JSON or doesn't match the expected type
5228 ///
5229 /// See: <https://playwright.dev/docs/api/class-response#response-json>
5230 pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
5231 let text = self.text().await?;
5232 serde_json::from_str(&text).map_err(|e| {
5233 crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
5234 })
5235 }
5236
5237 /// Returns all response headers as name-value pairs, preserving duplicates.
5238 ///
5239 /// Makes an RPC call for `"rawHeaders"` which returns the complete header list.
5240 ///
5241 /// # Errors
5242 ///
5243 /// Returns an error if:
5244 /// - No backing protocol object is available (edge case)
5245 /// - The RPC call to the server fails
5246 ///
5247 /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
5248 pub async fn headers_array(
5249 &self,
5250 ) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
5251 self.response_object()?.raw_headers().await
5252 }
5253
5254 /// Returns all response headers merged into a HashMap with lowercase keys.
5255 ///
5256 /// When multiple headers have the same name, their values are joined with `, `.
5257 /// This matches the behavior of `response.allHeaders()` in other Playwright bindings.
5258 ///
5259 /// # Errors
5260 ///
5261 /// Returns an error if:
5262 /// - No backing protocol object is available (edge case)
5263 /// - The RPC call to the server fails
5264 ///
5265 /// See: <https://playwright.dev/docs/api/class-response#response-all-headers>
5266 pub async fn all_headers(
5267 &self,
5268 ) -> crate::error::Result<std::collections::HashMap<String, String>> {
5269 let entries = self.headers_array().await?;
5270 let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
5271 for entry in entries {
5272 let key = entry.name.to_lowercase();
5273 map.entry(key)
5274 .and_modify(|v| {
5275 v.push_str(", ");
5276 v.push_str(&entry.value);
5277 })
5278 .or_insert(entry.value);
5279 }
5280 Ok(map)
5281 }
5282
5283 /// Returns the value for a single response header, or `None` if not present.
5284 ///
5285 /// The lookup is case-insensitive.
5286 ///
5287 /// # Errors
5288 ///
5289 /// Returns an error if:
5290 /// - No backing protocol object is available (edge case)
5291 /// - The RPC call to the server fails
5292 ///
5293 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
5294 /// Returns the value for a single response header, or `None` if not present.
5295 ///
5296 /// The lookup is case-insensitive. When multiple headers share the same name,
5297 /// their values are joined with `, ` (matching Playwright's behavior).
5298 ///
5299 /// Uses the raw headers from the server for accurate results.
5300 ///
5301 /// # Errors
5302 ///
5303 /// Returns an error if the underlying `headers_array()` RPC call fails.
5304 ///
5305 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
5306 pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
5307 let entries = self.headers_array().await?;
5308 let name_lower = name.to_lowercase();
5309 let mut values: Vec<String> = entries
5310 .into_iter()
5311 .filter(|h| h.name.to_lowercase() == name_lower)
5312 .map(|h| h.value)
5313 .collect();
5314
5315 if values.is_empty() {
5316 Ok(None)
5317 } else if values.len() == 1 {
5318 Ok(Some(values.remove(0)))
5319 } else {
5320 Ok(Some(values.join(", ")))
5321 }
5322 }
5323}
5324
5325impl std::fmt::Debug for Response {
5326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5327 f.debug_struct("Response")
5328 .field("url", &self.url)
5329 .field("status", &self.status)
5330 .field("status_text", &self.status_text)
5331 .field("ok", &self.ok)
5332 .finish_non_exhaustive()
5333 }
5334}
5335
5336/// Options for `page.route_from_har()` and `context.route_from_har()`.
5337///
5338/// See: <https://playwright.dev/docs/api/class-page#page-route-from-har>
5339#[derive(Debug, Clone, Default)]
5340pub struct RouteFromHarOptions {
5341 /// URL glob pattern — only requests matching this pattern are served from
5342 /// the HAR file. All requests are intercepted when omitted.
5343 pub url: Option<String>,
5344
5345 /// Policy for requests not found in the HAR file.
5346 ///
5347 /// - `"abort"` (default) — terminate the request with a network error.
5348 /// - `"fallback"` — pass the request through to the next handler (or network).
5349 pub not_found: Option<String>,
5350
5351 /// When `true`, record new network activity into the HAR file instead of
5352 /// replaying existing entries. Defaults to `false`.
5353 pub update: Option<bool>,
5354
5355 /// Content storage strategy used when `update` is `true`.
5356 ///
5357 /// - `"embed"` (default) — inline base64-encoded content in the HAR.
5358 /// - `"attach"` — store content as separate files alongside the HAR.
5359 pub update_content: Option<String>,
5360
5361 /// Recording detail level used when `update` is `true`.
5362 ///
5363 /// - `"minimal"` (default) — omit timing, cookies, and security info.
5364 /// - `"full"` — record everything.
5365 pub update_mode: Option<String>,
5366}
5367
5368/// Options for `page.add_locator_handler()`.
5369///
5370/// See: <https://playwright.dev/docs/api/class-page#page-add-locator-handler>
5371#[derive(Debug, Clone, Default)]
5372pub struct AddLocatorHandlerOptions {
5373 /// Whether to keep the page frozen after the handler has been called.
5374 ///
5375 /// When `false` (default), Playwright resumes normal page operation after
5376 /// the handler completes. When `true`, the page stays paused.
5377 pub no_wait_after: Option<bool>,
5378
5379 /// Maximum number of times to invoke this handler.
5380 ///
5381 /// Once exhausted, the handler is automatically unregistered.
5382 /// `None` (default) means the handler runs indefinitely.
5383 pub times: Option<u32>,
5384}
5385
5386/// Shared helper: store timeout locally and notify the Playwright server.
5387/// Used by both Page and BrowserContext timeout setters.
5388pub(crate) async fn set_timeout_and_notify(
5389 channel: &crate::server::channel::Channel,
5390 method: &str,
5391 timeout: f64,
5392) {
5393 if let Err(e) = channel
5394 .send_no_result(method, serde_json::json!({ "timeout": timeout }))
5395 .await
5396 {
5397 tracing::warn!("{} send error: {}", method, e);
5398 }
5399}