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