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