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