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