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};
9use crate::server::channel::Channel;
10use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
11use crate::server::connection::{ConnectionExt, downcast_parent};
12use base64::Engine;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::any::Any;
16use std::collections::HashMap;
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::sync::{Arc, Mutex, RwLock};
21
22/// Page represents a web page within a browser context.
23///
24/// A Page is created when you call `BrowserContext::new_page()` or `Browser::new_page()`.
25/// Each page is an isolated tab/window within its parent context.
26///
27/// Initially, pages are navigated to "about:blank". Use navigation methods
28/// Use navigation methods to navigate to URLs.
29///
30/// # Example
31///
32/// ```ignore
33/// use playwright_rs::protocol::{
34/// Playwright, ScreenshotOptions, ScreenshotType, AddStyleTagOptions, AddScriptTagOptions,
35/// EmulateMediaOptions, Media, ColorScheme, Viewport,
36/// };
37/// use std::path::PathBuf;
38///
39/// #[tokio::main]
40/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
41/// let playwright = Playwright::launch().await?;
42/// let browser = playwright.chromium().launch().await?;
43/// let page = browser.new_page().await?;
44///
45/// // Demonstrate url() - initially at about:blank
46/// assert_eq!(page.url(), "about:blank");
47///
48/// // Demonstrate goto() - navigate to a page
49/// let html = r#"<!DOCTYPE html>
50/// <html>
51/// <head><title>Test Page</title></head>
52/// <body>
53/// <h1 id="heading">Hello World</h1>
54/// <p>First paragraph</p>
55/// <p>Second paragraph</p>
56/// <button onclick="alert('Alert!')">Alert</button>
57/// <a href="data:text/plain,file" download="test.txt">Download</a>
58/// </body>
59/// </html>
60/// "#;
61/// // Data URLs may not return a response (this is normal)
62/// let _response = page.goto(&format!("data:text/html,{}", html), None).await?;
63///
64/// // Demonstrate title()
65/// let title = page.title().await?;
66/// assert_eq!(title, "Test Page");
67///
68/// // Demonstrate content() - returns full HTML including DOCTYPE
69/// let content = page.content().await?;
70/// assert!(content.contains("<!DOCTYPE html>") || content.to_lowercase().contains("<!doctype html>"));
71/// assert!(content.contains("<title>Test Page</title>"));
72/// assert!(content.contains("Hello World"));
73///
74/// // Demonstrate locator()
75/// let heading = page.locator("#heading").await;
76/// let text = heading.text_content().await?;
77/// assert_eq!(text, Some("Hello World".to_string()));
78///
79/// // Demonstrate query_selector()
80/// let element = page.query_selector("h1").await?;
81/// assert!(element.is_some(), "Should find the h1 element");
82///
83/// // Demonstrate query_selector_all()
84/// let paragraphs = page.query_selector_all("p").await?;
85/// assert_eq!(paragraphs.len(), 2);
86///
87/// // Demonstrate evaluate()
88/// page.evaluate::<(), ()>("console.log('Hello from Playwright!')", None).await?;
89///
90/// // Demonstrate evaluate_value()
91/// let result = page.evaluate_value("1 + 1").await?;
92/// assert_eq!(result, "2");
93///
94/// // Demonstrate screenshot()
95/// let bytes = page.screenshot(None).await?;
96/// assert!(!bytes.is_empty());
97///
98/// // Demonstrate screenshot_to_file()
99/// let temp_dir = std::env::temp_dir();
100/// let path = temp_dir.join("playwright_doctest_screenshot.png");
101/// let bytes = page.screenshot_to_file(&path, Some(
102/// ScreenshotOptions::builder()
103/// .screenshot_type(ScreenshotType::Png)
104/// .build()
105/// )).await?;
106/// assert!(!bytes.is_empty());
107///
108/// // Demonstrate reload()
109/// // Data URLs may not return a response on reload (this is normal)
110/// let _response = page.reload(None).await?;
111///
112/// // Demonstrate route() - network interception
113/// page.route("**/*.png", |route| async move {
114/// route.abort(None).await
115/// }).await?;
116///
117/// // Demonstrate on_download() - download handler
118/// page.on_download(|download| async move {
119/// println!("Download started: {}", download.url());
120/// Ok(())
121/// }).await?;
122///
123/// // Demonstrate on_dialog() - dialog handler
124/// page.on_dialog(|dialog| async move {
125/// println!("Dialog: {} - {}", dialog.type_(), dialog.message());
126/// dialog.accept(None).await
127/// }).await?;
128///
129/// // Demonstrate add_style_tag() - inject CSS
130/// page.add_style_tag(
131/// AddStyleTagOptions::builder()
132/// .content("body { background-color: blue; }")
133/// .build()
134/// ).await?;
135///
136/// // Demonstrate set_extra_http_headers() - set page-level headers
137/// let mut headers = std::collections::HashMap::new();
138/// headers.insert("x-custom-header".to_string(), "value".to_string());
139/// page.set_extra_http_headers(headers).await?;
140///
141/// // Demonstrate emulate_media() - emulate print media type
142/// page.emulate_media(Some(
143/// EmulateMediaOptions::builder()
144/// .media(Media::Print)
145/// .color_scheme(ColorScheme::Dark)
146/// .build()
147/// )).await?;
148///
149/// // Demonstrate add_script_tag() - inject a script
150/// page.add_script_tag(Some(
151/// AddScriptTagOptions::builder()
152/// .content("window.injectedByScriptTag = true;")
153/// .build()
154/// )).await?;
155///
156/// // Demonstrate pdf() - generate PDF (Chromium only)
157/// let pdf_bytes = page.pdf(None).await?;
158/// assert!(!pdf_bytes.is_empty());
159///
160/// // Demonstrate set_viewport_size() - responsive testing
161/// let mobile_viewport = Viewport {
162/// width: 375,
163/// height: 667,
164/// };
165/// page.set_viewport_size(mobile_viewport).await?;
166///
167/// // Demonstrate close()
168/// page.close().await?;
169///
170/// browser.close().await?;
171/// Ok(())
172/// }
173/// ```
174///
175/// See: <https://playwright.dev/docs/api/class-page>
176#[derive(Clone)]
177pub struct Page {
178 base: ChannelOwnerImpl,
179 /// Current URL of the page
180 /// Wrapped in RwLock to allow updates from events
181 url: Arc<RwLock<String>>,
182 /// GUID of the main frame
183 main_frame_guid: Arc<str>,
184 /// Cached reference to the main frame for synchronous URL access
185 /// This is populated after the first call to main_frame()
186 cached_main_frame: Arc<Mutex<Option<crate::protocol::Frame>>>,
187 /// Route handlers for network interception
188 route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
189 /// Download event handlers
190 download_handlers: Arc<Mutex<Vec<DownloadHandler>>>,
191 /// Dialog event handlers
192 dialog_handlers: Arc<Mutex<Vec<DialogHandler>>>,
193 /// Request event handlers
194 request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
195 /// Request finished event handlers
196 request_finished_handlers: Arc<Mutex<Vec<RequestHandler>>>,
197 /// Request failed event handlers
198 request_failed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
199 /// Response event handlers
200 response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
201 /// WebSocket event handlers
202 websocket_handlers: Arc<Mutex<Vec<WebSocketHandler>>>,
203 /// Current viewport size (None when no_viewport is set).
204 /// Updated by set_viewport_size().
205 viewport: Arc<RwLock<Option<Viewport>>>,
206 /// Whether this page has been closed.
207 /// Set to true when close() is called or a "close" event is received.
208 is_closed: Arc<AtomicBool>,
209 /// Default timeout for actions (milliseconds), stored as f64 bits.
210 default_timeout_ms: Arc<AtomicU64>,
211 /// Default timeout for navigation operations (milliseconds), stored as f64 bits.
212 default_navigation_timeout_ms: Arc<AtomicU64>,
213 /// Page-level binding callbacks registered via expose_function / expose_binding
214 binding_callbacks: Arc<Mutex<HashMap<String, PageBindingCallback>>>,
215}
216
217/// Type alias for boxed route handler future
218type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
219
220/// Type alias for boxed download handler future
221type DownloadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
222
223/// Type alias for boxed dialog handler future
224type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
225
226/// Type alias for boxed request handler future
227type RequestHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
228
229/// Type alias for boxed response handler future
230type ResponseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
231
232/// Type alias for boxed websocket handler future
233type WebSocketHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
234
235/// Storage for a single route handler
236#[derive(Clone)]
237struct RouteHandlerEntry {
238 pattern: String,
239 handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
240}
241
242/// Download event handler
243type DownloadHandler = Arc<dyn Fn(Download) -> DownloadHandlerFuture + Send + Sync>;
244
245/// Dialog event handler
246type DialogHandler = Arc<dyn Fn(Dialog) -> DialogHandlerFuture + Send + Sync>;
247
248/// Request event handler
249type RequestHandler = Arc<dyn Fn(Request) -> RequestHandlerFuture + Send + Sync>;
250
251/// Response event handler
252type ResponseHandler = Arc<dyn Fn(ResponseObject) -> ResponseHandlerFuture + Send + Sync>;
253
254/// WebSocket event handler
255type WebSocketHandler = Arc<dyn Fn(WebSocket) -> WebSocketHandlerFuture + Send + Sync>;
256
257/// Type alias for boxed page-level binding callback future
258type PageBindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
259
260/// Page-level binding callback: receives deserialized JS args, returns a JSON value
261type PageBindingCallback =
262 Arc<dyn Fn(Vec<serde_json::Value>) -> PageBindingCallbackFuture + Send + Sync>;
263
264impl Page {
265 /// Creates a new Page from protocol initialization
266 ///
267 /// This is called by the object factory when the server sends a `__create__` message
268 /// for a Page object.
269 ///
270 /// # Arguments
271 ///
272 /// * `parent` - The parent BrowserContext object
273 /// * `type_name` - The protocol type name ("Page")
274 /// * `guid` - The unique identifier for this page
275 /// * `initializer` - The initialization data from the server
276 ///
277 /// # Errors
278 ///
279 /// Returns error if initializer is malformed
280 pub fn new(
281 parent: Arc<dyn ChannelOwner>,
282 type_name: String,
283 guid: Arc<str>,
284 initializer: Value,
285 ) -> Result<Self> {
286 // Extract mainFrame GUID from initializer
287 let main_frame_guid: Arc<str> =
288 Arc::from(initializer["mainFrame"]["guid"].as_str().ok_or_else(|| {
289 crate::error::Error::ProtocolError(
290 "Page initializer missing 'mainFrame.guid' field".to_string(),
291 )
292 })?);
293
294 let base = ChannelOwnerImpl::new(
295 ParentOrConnection::Parent(parent),
296 type_name,
297 guid,
298 initializer,
299 );
300
301 // Initialize URL to about:blank
302 let url = Arc::new(RwLock::new("about:blank".to_string()));
303
304 // Initialize empty route handlers
305 let route_handlers = Arc::new(Mutex::new(Vec::new()));
306
307 // Initialize empty event handlers
308 let download_handlers = Arc::new(Mutex::new(Vec::new()));
309 let dialog_handlers = Arc::new(Mutex::new(Vec::new()));
310 let websocket_handlers = Arc::new(Mutex::new(Vec::new()));
311
312 // Initialize cached main frame as empty (will be populated on first access)
313 let cached_main_frame = Arc::new(Mutex::new(None));
314
315 // Extract viewport from initializer (may be null for no_viewport contexts)
316 let initial_viewport: Option<Viewport> =
317 base.initializer().get("viewportSize").and_then(|v| {
318 if v.is_null() {
319 None
320 } else {
321 serde_json::from_value(v.clone()).ok()
322 }
323 });
324 let viewport = Arc::new(RwLock::new(initial_viewport));
325
326 Ok(Self {
327 base,
328 url,
329 main_frame_guid,
330 cached_main_frame,
331 route_handlers,
332 download_handlers,
333 dialog_handlers,
334 request_handlers: Default::default(),
335 request_finished_handlers: Default::default(),
336 request_failed_handlers: Default::default(),
337 response_handlers: Default::default(),
338 websocket_handlers,
339 viewport,
340 is_closed: Arc::new(AtomicBool::new(false)),
341 default_timeout_ms: Arc::new(AtomicU64::new(crate::DEFAULT_TIMEOUT_MS.to_bits())),
342 default_navigation_timeout_ms: Arc::new(AtomicU64::new(
343 crate::DEFAULT_TIMEOUT_MS.to_bits(),
344 )),
345 binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
346 })
347 }
348
349 /// Returns the channel for sending protocol messages
350 ///
351 /// Used internally for sending RPC calls to the page.
352 fn channel(&self) -> &Channel {
353 self.base.channel()
354 }
355
356 /// Returns the main frame of the page.
357 ///
358 /// The main frame is where navigation and DOM operations actually happen.
359 ///
360 /// This method also wires up the back-reference from the frame to the page so that
361 /// `frame.page()`, `frame.locator()`, and `frame.get_by_*()` work correctly.
362 pub async fn main_frame(&self) -> Result<crate::protocol::Frame> {
363 // Get and downcast the Frame object from the connection's object registry
364 let frame: crate::protocol::Frame = self
365 .connection()
366 .get_typed::<crate::protocol::Frame>(&self.main_frame_guid)
367 .await?;
368
369 // Wire up the back-reference so frame.page() / frame.locator() work.
370 // This is safe to call multiple times (subsequent calls are no-ops once set).
371 frame.set_page(self.clone());
372
373 // Cache the frame for synchronous access in url()
374 if let Ok(mut cached) = self.cached_main_frame.lock() {
375 *cached = Some(frame.clone());
376 }
377
378 Ok(frame)
379 }
380
381 /// Returns the current URL of the page.
382 ///
383 /// This returns the last committed URL, including hash fragments from anchor navigation.
384 /// Initially, pages are at "about:blank".
385 ///
386 /// See: <https://playwright.dev/docs/api/class-page#page-url>
387 pub fn url(&self) -> String {
388 // Try to get URL from the cached main frame (source of truth for navigation including hashes)
389 if let Ok(cached) = self.cached_main_frame.lock()
390 && let Some(frame) = cached.as_ref()
391 {
392 return frame.url();
393 }
394
395 // Fallback to cached URL if frame not yet loaded
396 self.url.read().unwrap().clone()
397 }
398
399 /// Closes the page.
400 ///
401 /// This is a graceful operation that sends a close command to the page
402 /// and waits for it to shut down properly.
403 ///
404 /// # Errors
405 ///
406 /// Returns error if:
407 /// - Page has already been closed
408 /// - Communication with browser process fails
409 ///
410 /// See: <https://playwright.dev/docs/api/class-page#page-close>
411 pub async fn close(&self) -> Result<()> {
412 // Send close RPC to server
413 let result = self
414 .channel()
415 .send_no_result("close", serde_json::json!({}))
416 .await;
417 // Mark as closed regardless of error (best-effort)
418 self.is_closed.store(true, Ordering::Relaxed);
419 result
420 }
421
422 /// Returns whether the page has been closed.
423 ///
424 /// Returns `true` after `close()` has been called on this page, or after the
425 /// page receives a close event from the server (e.g. when the browser context
426 /// is closed).
427 ///
428 /// See: <https://playwright.dev/docs/api/class-page#page-is-closed>
429 pub fn is_closed(&self) -> bool {
430 self.is_closed.load(Ordering::Relaxed)
431 }
432
433 /// Sets the default timeout for all operations on this page.
434 ///
435 /// The timeout applies to actions such as `click`, `fill`, `locator.wait_for`, etc.
436 /// Pass `0` to disable timeouts.
437 ///
438 /// This stores the value locally so that subsequent action calls use it when
439 /// no explicit timeout is provided, and also notifies the Playwright server
440 /// so it can apply the same default on its side.
441 ///
442 /// # Arguments
443 ///
444 /// * `timeout` - Timeout in milliseconds
445 ///
446 /// See: <https://playwright.dev/docs/api/class-page#page-set-default-timeout>
447 pub async fn set_default_timeout(&self, timeout: f64) {
448 self.default_timeout_ms
449 .store(timeout.to_bits(), Ordering::Relaxed);
450 set_timeout_and_notify(self.channel(), "setDefaultTimeoutNoReply", timeout).await;
451 }
452
453 /// Sets the default timeout for navigation operations on this page.
454 ///
455 /// The timeout applies to navigation actions such as `goto`, `reload`,
456 /// `go_back`, and `go_forward`. Pass `0` to disable timeouts.
457 ///
458 /// # Arguments
459 ///
460 /// * `timeout` - Timeout in milliseconds
461 ///
462 /// See: <https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout>
463 pub async fn set_default_navigation_timeout(&self, timeout: f64) {
464 self.default_navigation_timeout_ms
465 .store(timeout.to_bits(), Ordering::Relaxed);
466 set_timeout_and_notify(
467 self.channel(),
468 "setDefaultNavigationTimeoutNoReply",
469 timeout,
470 )
471 .await;
472 }
473
474 /// Returns the current default action timeout in milliseconds.
475 pub fn default_timeout_ms(&self) -> f64 {
476 f64::from_bits(self.default_timeout_ms.load(Ordering::Relaxed))
477 }
478
479 /// Returns the current default navigation timeout in milliseconds.
480 pub fn default_navigation_timeout_ms(&self) -> f64 {
481 f64::from_bits(self.default_navigation_timeout_ms.load(Ordering::Relaxed))
482 }
483
484 /// Returns GotoOptions with the navigation timeout filled in if not already set.
485 ///
486 /// Used internally to ensure the page's configured default navigation timeout
487 /// is used when the caller does not provide an explicit timeout.
488 fn with_navigation_timeout(&self, options: Option<GotoOptions>) -> GotoOptions {
489 let nav_timeout = self.default_navigation_timeout_ms();
490 match options {
491 Some(opts) if opts.timeout.is_some() => opts,
492 Some(mut opts) => {
493 opts.timeout = Some(std::time::Duration::from_millis(nav_timeout as u64));
494 opts
495 }
496 None => GotoOptions {
497 timeout: Some(std::time::Duration::from_millis(nav_timeout as u64)),
498 wait_until: None,
499 },
500 }
501 }
502
503 /// Returns all frames in the page, including the main frame.
504 ///
505 /// Currently returns only the main (top-level) frame. Iframe enumeration
506 /// is not yet implemented and will be added in a future release.
507 ///
508 /// # Errors
509 ///
510 /// Returns error if:
511 /// - Page has been closed
512 /// - Communication with browser process fails
513 ///
514 /// See: <https://playwright.dev/docs/api/class-page#page-frames>
515 pub async fn frames(&self) -> Result<Vec<crate::protocol::Frame>> {
516 // Start with the main frame
517 let main = self.main_frame().await?;
518 Ok(vec![main])
519 }
520
521 /// Navigates to the specified URL.
522 ///
523 /// Returns `None` when navigating to URLs that don't produce responses (e.g., data URLs,
524 /// about:blank). This matches Playwright's behavior across all language bindings.
525 ///
526 /// # Arguments
527 ///
528 /// * `url` - The URL to navigate to
529 /// * `options` - Optional navigation options (timeout, wait_until)
530 ///
531 /// # Errors
532 ///
533 /// Returns error if:
534 /// - URL is invalid
535 /// - Navigation timeout (default 30s)
536 /// - Network error
537 ///
538 /// See: <https://playwright.dev/docs/api/class-page#page-goto>
539 pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
540 // Inject the page-level navigation timeout when no explicit timeout is given
541 let options = self.with_navigation_timeout(options);
542
543 // Delegate to main frame
544 let frame = self.main_frame().await.map_err(|e| match e {
545 Error::TargetClosed { context, .. } => Error::TargetClosed {
546 target_type: "Page".to_string(),
547 context,
548 },
549 other => other,
550 })?;
551
552 let response = frame.goto(url, Some(options)).await.map_err(|e| match e {
553 Error::TargetClosed { context, .. } => Error::TargetClosed {
554 target_type: "Page".to_string(),
555 context,
556 },
557 other => other,
558 })?;
559
560 // Update the page's URL if we got a response
561 if let Some(ref resp) = response
562 && let Ok(mut page_url) = self.url.write()
563 {
564 *page_url = resp.url().to_string();
565 }
566
567 Ok(response)
568 }
569
570 /// Returns the browser context that the page belongs to.
571 pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
572 downcast_parent::<crate::protocol::BrowserContext>(self)
573 .ok_or_else(|| Error::ProtocolError("Page parent is not a BrowserContext".to_string()))
574 }
575
576 /// Pauses script execution.
577 ///
578 /// Playwright will stop executing the script and wait for the user to either press
579 /// "Resume" in the page overlay or in the debugger.
580 ///
581 /// See: <https://playwright.dev/docs/api/class-page#page-pause>
582 pub async fn pause(&self) -> Result<()> {
583 self.context()?.pause().await
584 }
585
586 /// Returns the page's title.
587 ///
588 /// See: <https://playwright.dev/docs/api/class-page#page-title>
589 pub async fn title(&self) -> Result<String> {
590 // Delegate to main frame
591 let frame = self.main_frame().await?;
592 frame.title().await
593 }
594
595 /// Returns the full HTML content of the page, including the DOCTYPE.
596 ///
597 /// This method retrieves the complete HTML markup of the page,
598 /// including the doctype declaration and all DOM elements.
599 ///
600 /// See: <https://playwright.dev/docs/api/class-page#page-content>
601 pub async fn content(&self) -> Result<String> {
602 // Delegate to main frame
603 let frame = self.main_frame().await?;
604 frame.content().await
605 }
606
607 /// Sets the content of the page.
608 ///
609 /// See: <https://playwright.dev/docs/api/class-page#page-set-content>
610 pub async fn set_content(&self, html: &str, options: Option<GotoOptions>) -> Result<()> {
611 let frame = self.main_frame().await?;
612 frame.set_content(html, options).await
613 }
614
615 /// Waits for the required load state to be reached.
616 ///
617 /// This resolves when the page reaches a required load state, `load` by default.
618 /// The navigation must have been committed when this method is called. If the current
619 /// document has already reached the required state, resolves immediately.
620 ///
621 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-load-state>
622 pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
623 let frame = self.main_frame().await?;
624 frame.wait_for_load_state(state).await
625 }
626
627 /// Waits for the main frame to navigate to a URL matching the given string or glob pattern.
628 ///
629 /// See: <https://playwright.dev/docs/api/class-page#page-wait-for-url>
630 pub async fn wait_for_url(&self, url: &str, options: Option<GotoOptions>) -> Result<()> {
631 let frame = self.main_frame().await?;
632 frame.wait_for_url(url, options).await
633 }
634
635 /// Creates a locator for finding elements on the page.
636 ///
637 /// Locators are the central piece of Playwright's auto-waiting and retry-ability.
638 /// They don't execute queries until an action is performed.
639 ///
640 /// # Arguments
641 ///
642 /// * `selector` - CSS selector or other locating strategy
643 ///
644 /// See: <https://playwright.dev/docs/api/class-page#page-locator>
645 pub async fn locator(&self, selector: &str) -> crate::protocol::Locator {
646 // Get the main frame
647 let frame = self.main_frame().await.expect("Main frame should exist");
648
649 crate::protocol::Locator::new(Arc::new(frame), selector.to_string(), self.clone())
650 }
651
652 /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) for an iframe on this page.
653 ///
654 /// The `selector` identifies the iframe element (e.g., `"iframe[name='content']"`).
655 ///
656 /// See: <https://playwright.dev/docs/api/class-page#page-frame-locator>
657 pub async fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
658 let frame = self.main_frame().await.expect("Main frame should exist");
659 crate::protocol::FrameLocator::new(Arc::new(frame), selector.to_string(), self.clone())
660 }
661
662 /// Returns a locator that matches elements containing the given text.
663 ///
664 /// By default, matching is case-insensitive and searches for a substring.
665 /// Set `exact` to `true` for case-sensitive exact matching.
666 ///
667 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-text>
668 pub async fn get_by_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
669 self.locator(&crate::protocol::locator::get_by_text_selector(text, exact))
670 .await
671 }
672
673 /// Returns a locator that matches elements by their associated label text.
674 ///
675 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-label>
676 pub async fn get_by_label(&self, text: &str, exact: bool) -> crate::protocol::Locator {
677 self.locator(&crate::protocol::locator::get_by_label_selector(
678 text, exact,
679 ))
680 .await
681 }
682
683 /// Returns a locator that matches elements by their placeholder text.
684 ///
685 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-placeholder>
686 pub async fn get_by_placeholder(&self, text: &str, exact: bool) -> crate::protocol::Locator {
687 self.locator(&crate::protocol::locator::get_by_placeholder_selector(
688 text, exact,
689 ))
690 .await
691 }
692
693 /// Returns a locator that matches elements by their alt text.
694 ///
695 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-alt-text>
696 pub async fn get_by_alt_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
697 self.locator(&crate::protocol::locator::get_by_alt_text_selector(
698 text, exact,
699 ))
700 .await
701 }
702
703 /// Returns a locator that matches elements by their title attribute.
704 ///
705 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-title>
706 pub async fn get_by_title(&self, text: &str, exact: bool) -> crate::protocol::Locator {
707 self.locator(&crate::protocol::locator::get_by_title_selector(
708 text, exact,
709 ))
710 .await
711 }
712
713 /// Returns a locator that matches elements by their `data-testid` attribute.
714 ///
715 /// Always uses exact matching (case-sensitive).
716 ///
717 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-test-id>
718 pub async fn get_by_test_id(&self, test_id: &str) -> crate::protocol::Locator {
719 self.locator(&crate::protocol::locator::get_by_test_id_selector(test_id))
720 .await
721 }
722
723 /// Returns a locator that matches elements by their ARIA role.
724 ///
725 /// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
726 pub async fn get_by_role(
727 &self,
728 role: crate::protocol::locator::AriaRole,
729 options: Option<crate::protocol::locator::GetByRoleOptions>,
730 ) -> crate::protocol::Locator {
731 self.locator(&crate::protocol::locator::get_by_role_selector(
732 role, options,
733 ))
734 .await
735 }
736
737 /// Returns the keyboard instance for low-level keyboard control.
738 ///
739 /// See: <https://playwright.dev/docs/api/class-page#page-keyboard>
740 pub fn keyboard(&self) -> crate::protocol::Keyboard {
741 crate::protocol::Keyboard::new(self.clone())
742 }
743
744 /// Returns the mouse instance for low-level mouse control.
745 ///
746 /// See: <https://playwright.dev/docs/api/class-page#page-mouse>
747 pub fn mouse(&self) -> crate::protocol::Mouse {
748 crate::protocol::Mouse::new(self.clone())
749 }
750
751 // Internal keyboard methods (called by Keyboard struct)
752
753 pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
754 self.channel()
755 .send_no_result(
756 "keyboardDown",
757 serde_json::json!({
758 "key": key
759 }),
760 )
761 .await
762 }
763
764 pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
765 self.channel()
766 .send_no_result(
767 "keyboardUp",
768 serde_json::json!({
769 "key": key
770 }),
771 )
772 .await
773 }
774
775 pub(crate) async fn keyboard_press(
776 &self,
777 key: &str,
778 options: Option<crate::protocol::KeyboardOptions>,
779 ) -> Result<()> {
780 let mut params = serde_json::json!({
781 "key": key
782 });
783
784 if let Some(opts) = options {
785 let opts_json = opts.to_json();
786 if let Some(obj) = params.as_object_mut()
787 && let Some(opts_obj) = opts_json.as_object()
788 {
789 obj.extend(opts_obj.clone());
790 }
791 }
792
793 self.channel().send_no_result("keyboardPress", params).await
794 }
795
796 pub(crate) async fn keyboard_type(
797 &self,
798 text: &str,
799 options: Option<crate::protocol::KeyboardOptions>,
800 ) -> Result<()> {
801 let mut params = serde_json::json!({
802 "text": text
803 });
804
805 if let Some(opts) = options {
806 let opts_json = opts.to_json();
807 if let Some(obj) = params.as_object_mut()
808 && let Some(opts_obj) = opts_json.as_object()
809 {
810 obj.extend(opts_obj.clone());
811 }
812 }
813
814 self.channel().send_no_result("keyboardType", params).await
815 }
816
817 pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
818 self.channel()
819 .send_no_result(
820 "keyboardInsertText",
821 serde_json::json!({
822 "text": text
823 }),
824 )
825 .await
826 }
827
828 // Internal mouse methods (called by Mouse struct)
829
830 pub(crate) async fn mouse_move(
831 &self,
832 x: i32,
833 y: i32,
834 options: Option<crate::protocol::MouseOptions>,
835 ) -> Result<()> {
836 let mut params = serde_json::json!({
837 "x": x,
838 "y": y
839 });
840
841 if let Some(opts) = options {
842 let opts_json = opts.to_json();
843 if let Some(obj) = params.as_object_mut()
844 && let Some(opts_obj) = opts_json.as_object()
845 {
846 obj.extend(opts_obj.clone());
847 }
848 }
849
850 self.channel().send_no_result("mouseMove", params).await
851 }
852
853 pub(crate) async fn mouse_click(
854 &self,
855 x: i32,
856 y: i32,
857 options: Option<crate::protocol::MouseOptions>,
858 ) -> Result<()> {
859 let mut params = serde_json::json!({
860 "x": x,
861 "y": y
862 });
863
864 if let Some(opts) = options {
865 let opts_json = opts.to_json();
866 if let Some(obj) = params.as_object_mut()
867 && let Some(opts_obj) = opts_json.as_object()
868 {
869 obj.extend(opts_obj.clone());
870 }
871 }
872
873 self.channel().send_no_result("mouseClick", params).await
874 }
875
876 pub(crate) async fn mouse_dblclick(
877 &self,
878 x: i32,
879 y: i32,
880 options: Option<crate::protocol::MouseOptions>,
881 ) -> Result<()> {
882 let mut params = serde_json::json!({
883 "x": x,
884 "y": y,
885 "clickCount": 2
886 });
887
888 if let Some(opts) = options {
889 let opts_json = opts.to_json();
890 if let Some(obj) = params.as_object_mut()
891 && let Some(opts_obj) = opts_json.as_object()
892 {
893 obj.extend(opts_obj.clone());
894 }
895 }
896
897 self.channel().send_no_result("mouseClick", params).await
898 }
899
900 pub(crate) async fn mouse_down(
901 &self,
902 options: Option<crate::protocol::MouseOptions>,
903 ) -> Result<()> {
904 let mut params = serde_json::json!({});
905
906 if let Some(opts) = options {
907 let opts_json = opts.to_json();
908 if let Some(obj) = params.as_object_mut()
909 && let Some(opts_obj) = opts_json.as_object()
910 {
911 obj.extend(opts_obj.clone());
912 }
913 }
914
915 self.channel().send_no_result("mouseDown", params).await
916 }
917
918 pub(crate) async fn mouse_up(
919 &self,
920 options: Option<crate::protocol::MouseOptions>,
921 ) -> Result<()> {
922 let mut params = serde_json::json!({});
923
924 if let Some(opts) = options {
925 let opts_json = opts.to_json();
926 if let Some(obj) = params.as_object_mut()
927 && let Some(opts_obj) = opts_json.as_object()
928 {
929 obj.extend(opts_obj.clone());
930 }
931 }
932
933 self.channel().send_no_result("mouseUp", params).await
934 }
935
936 pub(crate) async fn mouse_wheel(&self, delta_x: i32, delta_y: i32) -> Result<()> {
937 self.channel()
938 .send_no_result(
939 "mouseWheel",
940 serde_json::json!({
941 "deltaX": delta_x,
942 "deltaY": delta_y
943 }),
944 )
945 .await
946 }
947
948 /// Reloads the current page.
949 ///
950 /// # Arguments
951 ///
952 /// * `options` - Optional reload options (timeout, wait_until)
953 ///
954 /// Returns `None` when reloading pages that don't produce responses (e.g., data URLs,
955 /// about:blank). This matches Playwright's behavior across all language bindings.
956 ///
957 /// See: <https://playwright.dev/docs/api/class-page#page-reload>
958 pub async fn reload(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
959 self.navigate_history("reload", options).await
960 }
961
962 /// Navigates to the previous page in history.
963 ///
964 /// Returns the main resource response. In case of multiple server redirects, the navigation
965 /// will resolve with the response of the last redirect. If can not go back, returns `None`.
966 ///
967 /// See: <https://playwright.dev/docs/api/class-page#page-go-back>
968 pub async fn go_back(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
969 self.navigate_history("goBack", options).await
970 }
971
972 /// Navigates to the next page in history.
973 ///
974 /// Returns the main resource response. In case of multiple server redirects, the navigation
975 /// will resolve with the response of the last redirect. If can not go forward, returns `None`.
976 ///
977 /// See: <https://playwright.dev/docs/api/class-page#page-go-forward>
978 pub async fn go_forward(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
979 self.navigate_history("goForward", options).await
980 }
981
982 /// Shared implementation for reload, go_back and go_forward.
983 async fn navigate_history(
984 &self,
985 method: &str,
986 options: Option<GotoOptions>,
987 ) -> Result<Option<Response>> {
988 // Inject the page-level navigation timeout when no explicit timeout is given
989 let opts = self.with_navigation_timeout(options);
990 let mut params = serde_json::json!({});
991
992 // opts.timeout is always Some(...) because with_navigation_timeout guarantees it
993 if let Some(timeout) = opts.timeout {
994 params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
995 } else {
996 params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
997 }
998 if let Some(wait_until) = opts.wait_until {
999 params["waitUntil"] = serde_json::json!(wait_until.as_str());
1000 }
1001
1002 #[derive(Deserialize)]
1003 struct NavigationResponse {
1004 response: Option<ResponseReference>,
1005 }
1006
1007 #[derive(Deserialize)]
1008 struct ResponseReference {
1009 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
1010 guid: Arc<str>,
1011 }
1012
1013 let result: NavigationResponse = self.channel().send(method, params).await?;
1014
1015 if let Some(response_ref) = result.response {
1016 let response_arc = {
1017 let mut attempts = 0;
1018 let max_attempts = 20;
1019 loop {
1020 match self.connection().get_object(&response_ref.guid).await {
1021 Ok(obj) => break obj,
1022 Err(_) if attempts < max_attempts => {
1023 attempts += 1;
1024 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1025 }
1026 Err(e) => return Err(e),
1027 }
1028 }
1029 };
1030
1031 let initializer = response_arc.initializer();
1032
1033 let status = initializer["status"].as_u64().ok_or_else(|| {
1034 crate::error::Error::ProtocolError("Response missing status".to_string())
1035 })? as u16;
1036
1037 let headers = initializer["headers"]
1038 .as_array()
1039 .ok_or_else(|| {
1040 crate::error::Error::ProtocolError("Response missing headers".to_string())
1041 })?
1042 .iter()
1043 .filter_map(|h| {
1044 let name = h["name"].as_str()?;
1045 let value = h["value"].as_str()?;
1046 Some((name.to_string(), value.to_string()))
1047 })
1048 .collect();
1049
1050 let response = Response::new(
1051 initializer["url"]
1052 .as_str()
1053 .ok_or_else(|| {
1054 crate::error::Error::ProtocolError("Response missing url".to_string())
1055 })?
1056 .to_string(),
1057 status,
1058 initializer["statusText"].as_str().unwrap_or("").to_string(),
1059 headers,
1060 Some(response_arc),
1061 );
1062
1063 if let Ok(mut page_url) = self.url.write() {
1064 *page_url = response.url().to_string();
1065 }
1066
1067 Ok(Some(response))
1068 } else {
1069 Ok(None)
1070 }
1071 }
1072
1073 /// Returns the first element matching the selector, or None if not found.
1074 ///
1075 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
1076 pub async fn query_selector(
1077 &self,
1078 selector: &str,
1079 ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
1080 let frame = self.main_frame().await?;
1081 frame.query_selector(selector).await
1082 }
1083
1084 /// Returns all elements matching the selector.
1085 ///
1086 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
1087 pub async fn query_selector_all(
1088 &self,
1089 selector: &str,
1090 ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
1091 let frame = self.main_frame().await?;
1092 frame.query_selector_all(selector).await
1093 }
1094
1095 /// Takes a screenshot of the page and returns the image bytes.
1096 ///
1097 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1098 pub async fn screenshot(
1099 &self,
1100 options: Option<crate::protocol::ScreenshotOptions>,
1101 ) -> Result<Vec<u8>> {
1102 let params = if let Some(opts) = options {
1103 opts.to_json()
1104 } else {
1105 // Default to PNG with required timeout
1106 serde_json::json!({
1107 "type": "png",
1108 "timeout": crate::DEFAULT_TIMEOUT_MS
1109 })
1110 };
1111
1112 #[derive(Deserialize)]
1113 struct ScreenshotResponse {
1114 binary: String,
1115 }
1116
1117 let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
1118
1119 // Decode base64 to bytes
1120 let bytes = base64::prelude::BASE64_STANDARD
1121 .decode(&response.binary)
1122 .map_err(|e| {
1123 crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
1124 })?;
1125
1126 Ok(bytes)
1127 }
1128
1129 /// Takes a screenshot and saves it to a file, also returning the bytes.
1130 ///
1131 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1132 pub async fn screenshot_to_file(
1133 &self,
1134 path: &std::path::Path,
1135 options: Option<crate::protocol::ScreenshotOptions>,
1136 ) -> Result<Vec<u8>> {
1137 // Get the screenshot bytes
1138 let bytes = self.screenshot(options).await?;
1139
1140 // Write to file
1141 tokio::fs::write(path, &bytes).await.map_err(|e| {
1142 crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
1143 })?;
1144
1145 Ok(bytes)
1146 }
1147
1148 /// Evaluates JavaScript in the page context (without return value).
1149 ///
1150 /// Executes the provided JavaScript expression or function within the page's
1151 /// context without returning a value.
1152 ///
1153 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1154 pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
1155 // Delegate to the main frame
1156 let frame = self.main_frame().await?;
1157 frame.frame_evaluate_expression(expression).await
1158 }
1159
1160 /// Evaluates JavaScript in the page context with optional arguments.
1161 ///
1162 /// Executes the provided JavaScript expression or function within the page's
1163 /// context and returns the result. The return value must be JSON-serializable.
1164 ///
1165 /// # Arguments
1166 ///
1167 /// * `expression` - JavaScript code to evaluate
1168 /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1169 ///
1170 /// # Returns
1171 ///
1172 /// The result as a `serde_json::Value`
1173 ///
1174 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1175 pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
1176 &self,
1177 expression: &str,
1178 arg: Option<&T>,
1179 ) -> Result<U> {
1180 // Delegate to the main frame
1181 let frame = self.main_frame().await?;
1182 let result = frame.evaluate(expression, arg).await?;
1183 serde_json::from_value(result).map_err(Error::from)
1184 }
1185
1186 /// Evaluates a JavaScript expression and returns the result as a String.
1187 ///
1188 /// # Arguments
1189 ///
1190 /// * `expression` - JavaScript code to evaluate
1191 ///
1192 /// # Returns
1193 ///
1194 /// The result converted to a String
1195 ///
1196 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1197 pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
1198 let frame = self.main_frame().await?;
1199 frame.frame_evaluate_expression_value(expression).await
1200 }
1201
1202 /// Registers a route handler for network interception.
1203 ///
1204 /// When a request matches the specified pattern, the handler will be called
1205 /// with a Route object that can abort, continue, or fulfill the request.
1206 ///
1207 /// # Arguments
1208 ///
1209 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
1210 /// * `handler` - Async closure that handles the route
1211 ///
1212 /// See: <https://playwright.dev/docs/api/class-page#page-route>
1213 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1214 where
1215 F: Fn(Route) -> Fut + Send + Sync + 'static,
1216 Fut: Future<Output = Result<()>> + Send + 'static,
1217 {
1218 // 1. Wrap handler in Arc with type erasure
1219 let handler =
1220 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1221
1222 // 2. Store in handlers list
1223 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1224 pattern: pattern.to_string(),
1225 handler,
1226 });
1227
1228 // 3. Enable network interception via protocol
1229 self.enable_network_interception().await?;
1230
1231 Ok(())
1232 }
1233
1234 /// Updates network interception patterns for this page
1235 async fn enable_network_interception(&self) -> Result<()> {
1236 // Collect all patterns from registered handlers
1237 // Each pattern must be an object with "glob" field
1238 let patterns: Vec<serde_json::Value> = self
1239 .route_handlers
1240 .lock()
1241 .unwrap()
1242 .iter()
1243 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1244 .collect();
1245
1246 // Send protocol command to update network interception patterns
1247 // Follows playwright-python's approach
1248 self.channel()
1249 .send_no_result(
1250 "setNetworkInterceptionPatterns",
1251 serde_json::json!({
1252 "patterns": patterns
1253 }),
1254 )
1255 .await
1256 }
1257
1258 /// Removes route handler(s) matching the given URL pattern.
1259 ///
1260 /// # Arguments
1261 ///
1262 /// * `pattern` - URL pattern to remove handlers for
1263 ///
1264 /// See: <https://playwright.dev/docs/api/class-page#page-unroute>
1265 pub async fn unroute(&self, pattern: &str) -> Result<()> {
1266 self.route_handlers
1267 .lock()
1268 .unwrap()
1269 .retain(|entry| entry.pattern != pattern);
1270 self.enable_network_interception().await
1271 }
1272
1273 /// Removes all registered route handlers.
1274 ///
1275 /// # Arguments
1276 ///
1277 /// * `behavior` - Optional behavior for in-flight handlers
1278 ///
1279 /// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
1280 pub async fn unroute_all(
1281 &self,
1282 _behavior: Option<crate::protocol::route::UnrouteBehavior>,
1283 ) -> Result<()> {
1284 self.route_handlers.lock().unwrap().clear();
1285 self.enable_network_interception().await
1286 }
1287
1288 /// Handles a route event from the protocol
1289 ///
1290 /// Called by on_event when a "route" event is received.
1291 /// Supports handler chaining via `route.fallback()` — if a handler calls
1292 /// `fallback()` instead of `continue_()`, `abort()`, or `fulfill()`, the
1293 /// next matching handler in the chain is tried.
1294 async fn on_route_event(&self, route: Route) {
1295 let handlers = self.route_handlers.lock().unwrap().clone();
1296 let url = route.request().url().to_string();
1297
1298 // Find matching handler (last registered wins, with fallback chaining)
1299 for entry in handlers.iter().rev() {
1300 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
1301 let handler = entry.handler.clone();
1302 if let Err(e) = handler(route.clone()).await {
1303 tracing::warn!("Route handler error: {}", e);
1304 break;
1305 }
1306 // If handler called fallback(), try the next matching handler
1307 if !route.was_handled() {
1308 continue;
1309 }
1310 break;
1311 }
1312 }
1313 }
1314
1315 /// Registers a download event handler.
1316 ///
1317 /// The handler will be called when a download is triggered by the page.
1318 /// Downloads occur when the page initiates a file download (e.g., clicking a link
1319 /// with the download attribute, or a server response with Content-Disposition: attachment).
1320 ///
1321 /// # Arguments
1322 ///
1323 /// * `handler` - Async closure that receives the Download object
1324 ///
1325 /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
1326 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1327 where
1328 F: Fn(Download) -> Fut + Send + Sync + 'static,
1329 Fut: Future<Output = Result<()>> + Send + 'static,
1330 {
1331 // Wrap handler with type erasure
1332 let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
1333 Box::pin(handler(download))
1334 });
1335
1336 // Store handler
1337 self.download_handlers.lock().unwrap().push(handler);
1338
1339 Ok(())
1340 }
1341
1342 /// Registers a dialog event handler.
1343 ///
1344 /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
1345 /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
1346 ///
1347 /// # Arguments
1348 ///
1349 /// * `handler` - Async closure that receives the Dialog object
1350 ///
1351 /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
1352 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1353 where
1354 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1355 Fut: Future<Output = Result<()>> + Send + 'static,
1356 {
1357 // Wrap handler with type erasure
1358 let handler =
1359 Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
1360
1361 // Store handler
1362 self.dialog_handlers.lock().unwrap().push(handler);
1363
1364 // Dialog events are auto-emitted (no subscription needed)
1365
1366 Ok(())
1367 }
1368
1369 /// See: <https://playwright.dev/docs/api/class-page#page-event-request>
1370 pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
1371 where
1372 F: Fn(Request) -> Fut + Send + Sync + 'static,
1373 Fut: Future<Output = Result<()>> + Send + 'static,
1374 {
1375 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1376 Box::pin(handler(request))
1377 });
1378
1379 let needs_subscription = self.request_handlers.lock().unwrap().is_empty();
1380 if needs_subscription {
1381 _ = self.channel().update_subscription("request", true).await;
1382 }
1383 self.request_handlers.lock().unwrap().push(handler);
1384
1385 Ok(())
1386 }
1387
1388 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-finished>
1389 pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
1390 where
1391 F: Fn(Request) -> Fut + Send + Sync + 'static,
1392 Fut: Future<Output = Result<()>> + Send + 'static,
1393 {
1394 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1395 Box::pin(handler(request))
1396 });
1397
1398 let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
1399 if needs_subscription {
1400 _ = self
1401 .channel()
1402 .update_subscription("requestFinished", true)
1403 .await;
1404 }
1405 self.request_finished_handlers.lock().unwrap().push(handler);
1406
1407 Ok(())
1408 }
1409
1410 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-failed>
1411 pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
1412 where
1413 F: Fn(Request) -> Fut + Send + Sync + 'static,
1414 Fut: Future<Output = Result<()>> + Send + 'static,
1415 {
1416 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1417 Box::pin(handler(request))
1418 });
1419
1420 let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
1421 if needs_subscription {
1422 _ = self
1423 .channel()
1424 .update_subscription("requestFailed", true)
1425 .await;
1426 }
1427 self.request_failed_handlers.lock().unwrap().push(handler);
1428
1429 Ok(())
1430 }
1431
1432 /// See: <https://playwright.dev/docs/api/class-page#page-event-response>
1433 pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
1434 where
1435 F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
1436 Fut: Future<Output = Result<()>> + Send + 'static,
1437 {
1438 let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
1439 Box::pin(handler(response))
1440 });
1441
1442 let needs_subscription = self.response_handlers.lock().unwrap().is_empty();
1443 if needs_subscription {
1444 _ = self.channel().update_subscription("response", true).await;
1445 }
1446 self.response_handlers.lock().unwrap().push(handler);
1447
1448 Ok(())
1449 }
1450
1451 /// Adds a listener for the `websocket` event.
1452 ///
1453 /// The handler will be called when a WebSocket request is dispatched.
1454 ///
1455 /// # Arguments
1456 ///
1457 /// * `handler` - The function to call when the event occurs
1458 ///
1459 /// See: <https://playwright.dev/docs/api/class-page#page-on-websocket>
1460 pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
1461 where
1462 F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1463 Fut: Future<Output = Result<()>> + Send + 'static,
1464 {
1465 let handler =
1466 Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
1467 self.websocket_handlers.lock().unwrap().push(handler);
1468 Ok(())
1469 }
1470
1471 /// Exposes a Rust function to this page as `window[name]` in JavaScript.
1472 ///
1473 /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
1474 /// server fires a `bindingCall` event on the **page** channel that invokes
1475 /// `callback` with the deserialized arguments. The return value is sent back
1476 /// to JS so the `await window[name](…)` expression resolves with it.
1477 ///
1478 /// The binding is page-scoped and not visible to other pages in the same context.
1479 ///
1480 /// # Arguments
1481 ///
1482 /// * `name` – JavaScript identifier that will be available as `window[name]`.
1483 /// * `callback` – Async closure called with `Vec<serde_json::Value>` (JS arguments)
1484 /// returning `serde_json::Value` (the result).
1485 ///
1486 /// # Errors
1487 ///
1488 /// Returns error if:
1489 /// - The page has been closed.
1490 /// - Communication with the browser process fails.
1491 ///
1492 /// See: <https://playwright.dev/docs/api/class-page#page-expose-function>
1493 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1494 where
1495 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1496 Fut: Future<Output = serde_json::Value> + Send + 'static,
1497 {
1498 self.expose_binding_internal(name, false, callback).await
1499 }
1500
1501 /// Exposes a Rust function to this page as `window[name]` in JavaScript,
1502 /// with `needsHandle: true`.
1503 ///
1504 /// Identical to [`expose_function`](Self::expose_function) but the Playwright
1505 /// server passes the first argument as a `JSHandle` object rather than a plain
1506 /// value.
1507 ///
1508 /// # Arguments
1509 ///
1510 /// * `name` – JavaScript identifier.
1511 /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
1512 ///
1513 /// # Errors
1514 ///
1515 /// Returns error if:
1516 /// - The page has been closed.
1517 /// - Communication with the browser process fails.
1518 ///
1519 /// See: <https://playwright.dev/docs/api/class-page#page-expose-binding>
1520 pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1521 where
1522 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1523 Fut: Future<Output = serde_json::Value> + Send + 'static,
1524 {
1525 self.expose_binding_internal(name, true, callback).await
1526 }
1527
1528 /// Internal implementation shared by page-level expose_function and expose_binding.
1529 ///
1530 /// Both `expose_function` and `expose_binding` use `needsHandle: false` because
1531 /// the current implementation does not support JSHandle objects. Using
1532 /// `needsHandle: true` would cause the Playwright server to wrap the first
1533 /// argument as a `JSHandle`, which requires a JSHandle protocol object that
1534 /// is not yet implemented.
1535 async fn expose_binding_internal<F, Fut>(
1536 &self,
1537 name: &str,
1538 _needs_handle: bool,
1539 callback: F,
1540 ) -> Result<()>
1541 where
1542 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1543 Fut: Future<Output = serde_json::Value> + Send + 'static,
1544 {
1545 let callback: PageBindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
1546 Box::pin(callback(args)) as PageBindingCallbackFuture
1547 });
1548
1549 // Store callback before sending RPC (avoids race with early bindingCall events)
1550 self.binding_callbacks
1551 .lock()
1552 .unwrap()
1553 .insert(name.to_string(), callback);
1554
1555 // Tell the Playwright server to inject window[name] into this page.
1556 // Always use needsHandle: false — see note above.
1557 self.channel()
1558 .send_no_result(
1559 "exposeBinding",
1560 serde_json::json!({ "name": name, "needsHandle": false }),
1561 )
1562 .await
1563 }
1564
1565 /// Handles a download event from the protocol
1566 async fn on_download_event(&self, download: Download) {
1567 let handlers = self.download_handlers.lock().unwrap().clone();
1568
1569 for handler in handlers {
1570 if let Err(e) = handler(download.clone()).await {
1571 tracing::warn!("Download handler error: {}", e);
1572 }
1573 }
1574 }
1575
1576 /// Handles a dialog event from the protocol
1577 async fn on_dialog_event(&self, dialog: Dialog) {
1578 let handlers = self.dialog_handlers.lock().unwrap().clone();
1579
1580 for handler in handlers {
1581 if let Err(e) = handler(dialog.clone()).await {
1582 tracing::warn!("Dialog handler error: {}", e);
1583 }
1584 }
1585 }
1586
1587 async fn on_request_event(&self, request: Request) {
1588 let handlers = self.request_handlers.lock().unwrap().clone();
1589
1590 for handler in handlers {
1591 if let Err(e) = handler(request.clone()).await {
1592 tracing::warn!("Request handler error: {}", e);
1593 }
1594 }
1595 }
1596
1597 async fn on_request_failed_event(&self, request: Request) {
1598 let handlers = self.request_failed_handlers.lock().unwrap().clone();
1599
1600 for handler in handlers {
1601 if let Err(e) = handler(request.clone()).await {
1602 tracing::warn!("RequestFailed handler error: {}", e);
1603 }
1604 }
1605 }
1606
1607 async fn on_request_finished_event(&self, request: Request) {
1608 let handlers = self.request_finished_handlers.lock().unwrap().clone();
1609
1610 for handler in handlers {
1611 if let Err(e) = handler(request.clone()).await {
1612 tracing::warn!("RequestFinished handler error: {}", e);
1613 }
1614 }
1615 }
1616
1617 async fn on_response_event(&self, response: ResponseObject) {
1618 let handlers = self.response_handlers.lock().unwrap().clone();
1619
1620 for handler in handlers {
1621 if let Err(e) = handler(response.clone()).await {
1622 tracing::warn!("Response handler error: {}", e);
1623 }
1624 }
1625 }
1626
1627 /// Triggers dialog event (called by BrowserContext when dialog events arrive)
1628 ///
1629 /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
1630 /// This method is public so BrowserContext can forward dialog events.
1631 pub async fn trigger_dialog_event(&self, dialog: Dialog) {
1632 self.on_dialog_event(dialog).await;
1633 }
1634
1635 /// Triggers request event (called by BrowserContext when request events arrive)
1636 pub(crate) async fn trigger_request_event(&self, request: Request) {
1637 self.on_request_event(request).await;
1638 }
1639
1640 pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
1641 self.on_request_finished_event(request).await;
1642 }
1643
1644 pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
1645 self.on_request_failed_event(request).await;
1646 }
1647
1648 /// Triggers response event (called by BrowserContext when response events arrive)
1649 pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
1650 self.on_response_event(response).await;
1651 }
1652
1653 /// Adds a `<style>` tag into the page with the desired content.
1654 ///
1655 /// # Arguments
1656 ///
1657 /// * `options` - Style tag options (content, url, or path)
1658 ///
1659 /// # Returns
1660 ///
1661 /// Returns an ElementHandle pointing to the injected `<style>` tag
1662 ///
1663 /// # Example
1664 ///
1665 /// ```no_run
1666 /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
1667 /// # #[tokio::main]
1668 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1669 /// # let playwright = Playwright::launch().await?;
1670 /// # let browser = playwright.chromium().launch().await?;
1671 /// # let context = browser.new_context().await?;
1672 /// # let page = context.new_page().await?;
1673 /// use playwright_rs::protocol::AddStyleTagOptions;
1674 ///
1675 /// // With inline CSS
1676 /// page.add_style_tag(
1677 /// AddStyleTagOptions::builder()
1678 /// .content("body { background-color: red; }")
1679 /// .build()
1680 /// ).await?;
1681 ///
1682 /// // With external URL
1683 /// page.add_style_tag(
1684 /// AddStyleTagOptions::builder()
1685 /// .url("https://example.com/style.css")
1686 /// .build()
1687 /// ).await?;
1688 ///
1689 /// // From file
1690 /// page.add_style_tag(
1691 /// AddStyleTagOptions::builder()
1692 /// .path("./styles/custom.css")
1693 /// .build()
1694 /// ).await?;
1695 /// # Ok(())
1696 /// # }
1697 /// ```
1698 ///
1699 /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1700 pub async fn add_style_tag(
1701 &self,
1702 options: AddStyleTagOptions,
1703 ) -> Result<Arc<crate::protocol::ElementHandle>> {
1704 let frame = self.main_frame().await?;
1705 frame.add_style_tag(options).await
1706 }
1707
1708 /// Adds a script which would be evaluated in one of the following scenarios:
1709 /// - Whenever the page is navigated
1710 /// - Whenever a child frame is attached or navigated
1711 ///
1712 /// The script is evaluated after the document was created but before any of its scripts were run.
1713 ///
1714 /// # Arguments
1715 ///
1716 /// * `script` - JavaScript code to be injected into the page
1717 ///
1718 /// # Example
1719 ///
1720 /// ```no_run
1721 /// # use playwright_rs::protocol::Playwright;
1722 /// # #[tokio::main]
1723 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1724 /// # let playwright = Playwright::launch().await?;
1725 /// # let browser = playwright.chromium().launch().await?;
1726 /// # let context = browser.new_context().await?;
1727 /// # let page = context.new_page().await?;
1728 /// page.add_init_script("window.injected = 123;").await?;
1729 /// # Ok(())
1730 /// # }
1731 /// ```
1732 ///
1733 /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
1734 pub async fn add_init_script(&self, script: &str) -> Result<()> {
1735 self.channel()
1736 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
1737 .await
1738 }
1739
1740 /// Sets the viewport size for the page.
1741 ///
1742 /// This method allows dynamic resizing of the viewport after page creation,
1743 /// useful for testing responsive layouts at different screen sizes.
1744 ///
1745 /// # Arguments
1746 ///
1747 /// * `viewport` - The viewport dimensions (width and height in pixels)
1748 ///
1749 /// # Example
1750 ///
1751 /// ```no_run
1752 /// # use playwright_rs::protocol::{Playwright, Viewport};
1753 /// # #[tokio::main]
1754 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1755 /// # let playwright = Playwright::launch().await?;
1756 /// # let browser = playwright.chromium().launch().await?;
1757 /// # let page = browser.new_page().await?;
1758 /// // Set viewport to mobile size
1759 /// let mobile = Viewport {
1760 /// width: 375,
1761 /// height: 667,
1762 /// };
1763 /// page.set_viewport_size(mobile).await?;
1764 ///
1765 /// // Later, test desktop layout
1766 /// let desktop = Viewport {
1767 /// width: 1920,
1768 /// height: 1080,
1769 /// };
1770 /// page.set_viewport_size(desktop).await?;
1771 /// # Ok(())
1772 /// # }
1773 /// ```
1774 ///
1775 /// # Errors
1776 ///
1777 /// Returns error if:
1778 /// - Page has been closed
1779 /// - Communication with browser process fails
1780 ///
1781 /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
1782 pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
1783 // Store the new viewport locally so viewport_size() can reflect the change
1784 if let Ok(mut guard) = self.viewport.write() {
1785 *guard = Some(viewport.clone());
1786 }
1787 self.channel()
1788 .send_no_result(
1789 "setViewportSize",
1790 serde_json::json!({ "viewportSize": viewport }),
1791 )
1792 .await
1793 }
1794
1795 /// Brings this page to the front (activates the tab).
1796 ///
1797 /// Activates the page in the browser, making it the focused tab. This is
1798 /// useful in multi-page tests to ensure actions target the correct page.
1799 ///
1800 /// # Errors
1801 ///
1802 /// Returns error if:
1803 /// - Page has been closed
1804 /// - Communication with browser process fails
1805 ///
1806 /// See: <https://playwright.dev/docs/api/class-page#page-bring-to-front>
1807 pub async fn bring_to_front(&self) -> Result<()> {
1808 self.channel()
1809 .send_no_result("bringToFront", serde_json::json!({}))
1810 .await
1811 }
1812
1813 /// Sets extra HTTP headers that will be sent with every request from this page.
1814 ///
1815 /// These headers are sent in addition to headers set on the browser context via
1816 /// `BrowserContext::set_extra_http_headers()`. Page-level headers take precedence
1817 /// over context-level headers when names conflict.
1818 ///
1819 /// # Arguments
1820 ///
1821 /// * `headers` - Map of header names to values.
1822 ///
1823 /// # Errors
1824 ///
1825 /// Returns error if:
1826 /// - Page has been closed
1827 /// - Communication with browser process fails
1828 ///
1829 /// See: <https://playwright.dev/docs/api/class-page#page-set-extra-http-headers>
1830 pub async fn set_extra_http_headers(
1831 &self,
1832 headers: std::collections::HashMap<String, String>,
1833 ) -> Result<()> {
1834 // Playwright protocol expects an array of {name, value} objects
1835 // This RPC is sent on the Page channel (not the Frame channel)
1836 let headers_array: Vec<serde_json::Value> = headers
1837 .into_iter()
1838 .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
1839 .collect();
1840 self.channel()
1841 .send_no_result(
1842 "setExtraHTTPHeaders",
1843 serde_json::json!({ "headers": headers_array }),
1844 )
1845 .await
1846 }
1847
1848 /// Emulates media features for the page.
1849 ///
1850 /// This method allows emulating CSS media features such as `media`, `color-scheme`,
1851 /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
1852 ///
1853 /// To reset a specific feature to the browser default, use the `NoOverride` variant.
1854 ///
1855 /// # Arguments
1856 ///
1857 /// * `options` - Optional emulation options. If `None`, this is a no-op.
1858 ///
1859 /// # Example
1860 ///
1861 /// ```no_run
1862 /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
1863 /// # #[tokio::main]
1864 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1865 /// # let playwright = Playwright::launch().await?;
1866 /// # let browser = playwright.chromium().launch().await?;
1867 /// # let page = browser.new_page().await?;
1868 /// // Emulate print media
1869 /// page.emulate_media(Some(
1870 /// EmulateMediaOptions::builder()
1871 /// .media(Media::Print)
1872 /// .build()
1873 /// )).await?;
1874 ///
1875 /// // Emulate dark color scheme
1876 /// page.emulate_media(Some(
1877 /// EmulateMediaOptions::builder()
1878 /// .color_scheme(ColorScheme::Dark)
1879 /// .build()
1880 /// )).await?;
1881 /// # Ok(())
1882 /// # }
1883 /// ```
1884 ///
1885 /// # Errors
1886 ///
1887 /// Returns error if:
1888 /// - Page has been closed
1889 /// - Communication with browser process fails
1890 ///
1891 /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
1892 pub async fn emulate_media(&self, options: Option<EmulateMediaOptions>) -> Result<()> {
1893 let mut params = serde_json::json!({});
1894
1895 if let Some(opts) = options {
1896 if let Some(media) = opts.media {
1897 params["media"] = serde_json::to_value(media).map_err(|e| {
1898 crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
1899 })?;
1900 }
1901 if let Some(color_scheme) = opts.color_scheme {
1902 params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
1903 crate::error::Error::ProtocolError(format!(
1904 "Failed to serialize colorScheme: {}",
1905 e
1906 ))
1907 })?;
1908 }
1909 if let Some(reduced_motion) = opts.reduced_motion {
1910 params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
1911 crate::error::Error::ProtocolError(format!(
1912 "Failed to serialize reducedMotion: {}",
1913 e
1914 ))
1915 })?;
1916 }
1917 if let Some(forced_colors) = opts.forced_colors {
1918 params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
1919 crate::error::Error::ProtocolError(format!(
1920 "Failed to serialize forcedColors: {}",
1921 e
1922 ))
1923 })?;
1924 }
1925 }
1926
1927 self.channel().send_no_result("emulateMedia", params).await
1928 }
1929
1930 /// Generates a PDF of the page and returns it as bytes.
1931 ///
1932 /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
1933 /// not supported in Firefox or WebKit.
1934 ///
1935 /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
1936 /// saved to that file.
1937 ///
1938 /// # Arguments
1939 ///
1940 /// * `options` - Optional PDF generation options
1941 ///
1942 /// # Example
1943 ///
1944 /// ```no_run
1945 /// # use playwright_rs::protocol::Playwright;
1946 /// # #[tokio::main]
1947 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1948 /// # let playwright = Playwright::launch().await?;
1949 /// # let browser = playwright.chromium().launch().await?;
1950 /// # let page = browser.new_page().await?;
1951 /// let pdf_bytes = page.pdf(None).await?;
1952 /// assert!(!pdf_bytes.is_empty());
1953 /// # Ok(())
1954 /// # }
1955 /// ```
1956 ///
1957 /// # Errors
1958 ///
1959 /// Returns error if:
1960 /// - The browser is not Chromium (PDF only supported in Chromium)
1961 /// - Page has been closed
1962 /// - Communication with browser process fails
1963 ///
1964 /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
1965 pub async fn pdf(&self, options: Option<PdfOptions>) -> Result<Vec<u8>> {
1966 let mut params = serde_json::json!({});
1967 let mut save_path: Option<std::path::PathBuf> = None;
1968
1969 if let Some(opts) = options {
1970 // Capture the file path before consuming opts
1971 save_path = opts.path;
1972
1973 if let Some(scale) = opts.scale {
1974 params["scale"] = serde_json::json!(scale);
1975 }
1976 if let Some(v) = opts.display_header_footer {
1977 params["displayHeaderFooter"] = serde_json::json!(v);
1978 }
1979 if let Some(v) = opts.header_template {
1980 params["headerTemplate"] = serde_json::json!(v);
1981 }
1982 if let Some(v) = opts.footer_template {
1983 params["footerTemplate"] = serde_json::json!(v);
1984 }
1985 if let Some(v) = opts.print_background {
1986 params["printBackground"] = serde_json::json!(v);
1987 }
1988 if let Some(v) = opts.landscape {
1989 params["landscape"] = serde_json::json!(v);
1990 }
1991 if let Some(v) = opts.page_ranges {
1992 params["pageRanges"] = serde_json::json!(v);
1993 }
1994 if let Some(v) = opts.format {
1995 params["format"] = serde_json::json!(v);
1996 }
1997 if let Some(v) = opts.width {
1998 params["width"] = serde_json::json!(v);
1999 }
2000 if let Some(v) = opts.height {
2001 params["height"] = serde_json::json!(v);
2002 }
2003 if let Some(v) = opts.prefer_css_page_size {
2004 params["preferCSSPageSize"] = serde_json::json!(v);
2005 }
2006 if let Some(margin) = opts.margin {
2007 params["margin"] = serde_json::to_value(margin).map_err(|e| {
2008 crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
2009 })?;
2010 }
2011 }
2012
2013 #[derive(Deserialize)]
2014 struct PdfResponse {
2015 pdf: String,
2016 }
2017
2018 let response: PdfResponse = self.channel().send("pdf", params).await?;
2019
2020 // Decode base64 to bytes
2021 let pdf_bytes = base64::engine::general_purpose::STANDARD
2022 .decode(&response.pdf)
2023 .map_err(|e| {
2024 crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
2025 })?;
2026
2027 // If a path was specified, save the PDF to disk as well
2028 if let Some(path) = save_path {
2029 tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
2030 crate::error::Error::InvalidArgument(format!(
2031 "Failed to write PDF to '{}': {}",
2032 path.display(),
2033 e
2034 ))
2035 })?;
2036 }
2037
2038 Ok(pdf_bytes)
2039 }
2040
2041 /// Adds a `<script>` tag into the page with the desired URL or content.
2042 ///
2043 /// # Arguments
2044 ///
2045 /// * `options` - Optional script tag options (content, url, or path).
2046 /// If `None`, returns an error because no source is specified.
2047 ///
2048 /// At least one of `content`, `url`, or `path` must be provided.
2049 ///
2050 /// # Example
2051 ///
2052 /// ```no_run
2053 /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
2054 /// # #[tokio::main]
2055 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2056 /// # let playwright = Playwright::launch().await?;
2057 /// # let browser = playwright.chromium().launch().await?;
2058 /// # let context = browser.new_context().await?;
2059 /// # let page = context.new_page().await?;
2060 /// // With inline JavaScript
2061 /// page.add_script_tag(Some(
2062 /// AddScriptTagOptions::builder()
2063 /// .content("window.myVar = 42;")
2064 /// .build()
2065 /// )).await?;
2066 ///
2067 /// // With external URL
2068 /// page.add_script_tag(Some(
2069 /// AddScriptTagOptions::builder()
2070 /// .url("https://example.com/script.js")
2071 /// .build()
2072 /// )).await?;
2073 /// # Ok(())
2074 /// # }
2075 /// ```
2076 ///
2077 /// # Errors
2078 ///
2079 /// Returns error if:
2080 /// - `options` is `None` or no content/url/path is specified
2081 /// - Page has been closed
2082 /// - Script loading fails (e.g., invalid URL)
2083 ///
2084 /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
2085 pub async fn add_script_tag(
2086 &self,
2087 options: Option<AddScriptTagOptions>,
2088 ) -> Result<Arc<crate::protocol::ElementHandle>> {
2089 let opts = options.ok_or_else(|| {
2090 Error::InvalidArgument(
2091 "At least one of content, url, or path must be specified".to_string(),
2092 )
2093 })?;
2094 let frame = self.main_frame().await?;
2095 frame.add_script_tag(opts).await
2096 }
2097
2098 /// Returns the current viewport size of the page, or `None` if no viewport is set.
2099 ///
2100 /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
2101 /// returns the dimensions configured at context creation time or updated via
2102 /// `set_viewport_size()`.
2103 ///
2104 /// # Example
2105 ///
2106 /// ```ignore
2107 /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
2108 /// # #[tokio::main]
2109 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2110 /// # let playwright = Playwright::launch().await?;
2111 /// # let browser = playwright.chromium().launch().await?;
2112 /// let context = browser.new_context_with_options(
2113 /// BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
2114 /// ).await?;
2115 /// let page = context.new_page().await?;
2116 /// let size = page.viewport_size().expect("Viewport should be set");
2117 /// assert_eq!(size.width, 1280);
2118 /// assert_eq!(size.height, 720);
2119 /// # Ok(())
2120 /// # }
2121 /// ```
2122 ///
2123 /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
2124 pub fn viewport_size(&self) -> Option<Viewport> {
2125 self.viewport.read().ok()?.clone()
2126 }
2127}
2128
2129impl ChannelOwner for Page {
2130 fn guid(&self) -> &str {
2131 self.base.guid()
2132 }
2133
2134 fn type_name(&self) -> &str {
2135 self.base.type_name()
2136 }
2137
2138 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2139 self.base.parent()
2140 }
2141
2142 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2143 self.base.connection()
2144 }
2145
2146 fn initializer(&self) -> &Value {
2147 self.base.initializer()
2148 }
2149
2150 fn channel(&self) -> &Channel {
2151 self.base.channel()
2152 }
2153
2154 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2155 self.base.dispose(reason)
2156 }
2157
2158 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2159 self.base.adopt(child)
2160 }
2161
2162 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2163 self.base.add_child(guid, child)
2164 }
2165
2166 fn remove_child(&self, guid: &str) {
2167 self.base.remove_child(guid)
2168 }
2169
2170 fn on_event(&self, method: &str, params: Value) {
2171 match method {
2172 "navigated" => {
2173 // Update URL when page navigates
2174 if let Some(url_value) = params.get("url")
2175 && let Some(url_str) = url_value.as_str()
2176 && let Ok(mut url) = self.url.write()
2177 {
2178 *url = url_str.to_string();
2179 }
2180 }
2181 "route" => {
2182 // Handle network routing event
2183 if let Some(route_guid) = params
2184 .get("route")
2185 .and_then(|v| v.get("guid"))
2186 .and_then(|v| v.as_str())
2187 {
2188 // Get the Route object from connection's registry
2189 let connection = self.connection();
2190 let route_guid_owned = route_guid.to_string();
2191 let self_clone = self.clone();
2192
2193 tokio::spawn(async move {
2194 // Get and downcast Route object
2195 let route: Route =
2196 match connection.get_typed::<Route>(&route_guid_owned).await {
2197 Ok(r) => r,
2198 Err(e) => {
2199 tracing::warn!("Failed to get route object: {}", e);
2200 return;
2201 }
2202 };
2203
2204 // Set APIRequestContext on the route for fetch() support.
2205 // Page's parent is BrowserContext, which has the request context.
2206 if let Some(ctx) =
2207 downcast_parent::<crate::protocol::BrowserContext>(&self_clone)
2208 && let Ok(api_ctx) = ctx.request().await
2209 {
2210 route.set_api_request_context(api_ctx);
2211 }
2212
2213 // Call the route handler and wait for completion
2214 self_clone.on_route_event(route).await;
2215 });
2216 }
2217 }
2218 "download" => {
2219 // Handle download event
2220 // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
2221 let url = params
2222 .get("url")
2223 .and_then(|v| v.as_str())
2224 .unwrap_or("")
2225 .to_string();
2226
2227 let suggested_filename = params
2228 .get("suggestedFilename")
2229 .and_then(|v| v.as_str())
2230 .unwrap_or("")
2231 .to_string();
2232
2233 if let Some(artifact_guid) = params
2234 .get("artifact")
2235 .and_then(|v| v.get("guid"))
2236 .and_then(|v| v.as_str())
2237 {
2238 let connection = self.connection();
2239 let artifact_guid_owned = artifact_guid.to_string();
2240 let self_clone = self.clone();
2241
2242 tokio::spawn(async move {
2243 // Wait for Artifact object to be created
2244 let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
2245 Ok(obj) => obj,
2246 Err(e) => {
2247 tracing::warn!("Failed to get artifact object: {}", e);
2248 return;
2249 }
2250 };
2251
2252 // Create Download wrapper from Artifact + event params
2253 let download = Download::from_artifact(
2254 artifact_arc,
2255 url,
2256 suggested_filename,
2257 self_clone.clone(),
2258 );
2259
2260 // Call the download handlers
2261 self_clone.on_download_event(download).await;
2262 });
2263 }
2264 }
2265 "dialog" => {
2266 // Dialog events are handled by BrowserContext and forwarded to Page
2267 // This case should not be reached, but keeping for completeness
2268 }
2269 "webSocket" => {
2270 if let Some(ws_guid) = params
2271 .get("webSocket")
2272 .and_then(|v| v.get("guid"))
2273 .and_then(|v| v.as_str())
2274 {
2275 let connection = self.connection();
2276 let ws_guid_owned = ws_guid.to_string();
2277 let self_clone = self.clone();
2278
2279 tokio::spawn(async move {
2280 // Get and downcast WebSocket object
2281 let ws: WebSocket =
2282 match connection.get_typed::<WebSocket>(&ws_guid_owned).await {
2283 Ok(ws) => ws,
2284 Err(e) => {
2285 tracing::warn!("Failed to get WebSocket object: {}", e);
2286 return;
2287 }
2288 };
2289
2290 // Call handlers
2291 let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
2292 for handler in handlers {
2293 let ws_clone = ws.clone();
2294 tokio::spawn(async move {
2295 if let Err(e) = handler(ws_clone).await {
2296 tracing::error!("Error in websocket handler: {}", e);
2297 }
2298 });
2299 }
2300 });
2301 }
2302 }
2303 "bindingCall" => {
2304 // A JS caller on this page invoked a page-level exposed function.
2305 // Event format: {binding: {guid: "..."}}
2306 if let Some(binding_guid) = params
2307 .get("binding")
2308 .and_then(|v| v.get("guid"))
2309 .and_then(|v| v.as_str())
2310 {
2311 let connection = self.connection();
2312 let binding_guid_owned = binding_guid.to_string();
2313 let binding_callbacks = self.binding_callbacks.clone();
2314
2315 tokio::spawn(async move {
2316 let binding_call: crate::protocol::BindingCall = match connection
2317 .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
2318 .await
2319 {
2320 Ok(bc) => bc,
2321 Err(e) => {
2322 tracing::warn!("Failed to get BindingCall object: {}", e);
2323 return;
2324 }
2325 };
2326
2327 let name = binding_call.name().to_string();
2328
2329 // Look up page-level callback
2330 let callback = {
2331 let callbacks = binding_callbacks.lock().unwrap();
2332 callbacks.get(&name).cloned()
2333 };
2334
2335 let Some(callback) = callback else {
2336 // No page-level handler — the context-level handler on
2337 // BrowserContext::on_event("bindingCall") will handle it.
2338 return;
2339 };
2340
2341 // Deserialize args from Playwright protocol format
2342 let raw_args = binding_call.args();
2343 let args = crate::protocol::browser_context::BrowserContext::deserialize_binding_args_pub(raw_args);
2344
2345 // Call callback and serialize result
2346 let result_value = callback(args).await;
2347 let serialized =
2348 crate::protocol::evaluate_conversion::serialize_argument(&result_value);
2349
2350 if let Err(e) = binding_call.resolve(serialized).await {
2351 tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
2352 }
2353 });
2354 }
2355 }
2356 "close" => {
2357 // Server-initiated close (e.g. context was closed)
2358 self.is_closed.store(true, Ordering::Relaxed);
2359 }
2360 _ => {
2361 // Other events will be handled in future phases
2362 // Events: load, domcontentloaded, crash, etc.
2363 }
2364 }
2365 }
2366
2367 fn was_collected(&self) -> bool {
2368 self.base.was_collected()
2369 }
2370
2371 fn as_any(&self) -> &dyn Any {
2372 self
2373 }
2374}
2375
2376impl std::fmt::Debug for Page {
2377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2378 f.debug_struct("Page")
2379 .field("guid", &self.guid())
2380 .field("url", &self.url())
2381 .finish()
2382 }
2383}
2384
2385/// Options for page.goto() and page.reload()
2386#[derive(Debug, Clone)]
2387pub struct GotoOptions {
2388 /// Maximum operation time in milliseconds
2389 pub timeout: Option<std::time::Duration>,
2390 /// When to consider operation succeeded
2391 pub wait_until: Option<WaitUntil>,
2392}
2393
2394impl GotoOptions {
2395 /// Creates new GotoOptions with default values
2396 pub fn new() -> Self {
2397 Self {
2398 timeout: None,
2399 wait_until: None,
2400 }
2401 }
2402
2403 /// Sets the timeout
2404 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
2405 self.timeout = Some(timeout);
2406 self
2407 }
2408
2409 /// Sets the wait_until option
2410 pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
2411 self.wait_until = Some(wait_until);
2412 self
2413 }
2414}
2415
2416impl Default for GotoOptions {
2417 fn default() -> Self {
2418 Self::new()
2419 }
2420}
2421
2422/// When to consider navigation succeeded
2423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2424pub enum WaitUntil {
2425 /// Consider operation to be finished when the `load` event is fired
2426 Load,
2427 /// Consider operation to be finished when the `DOMContentLoaded` event is fired
2428 DomContentLoaded,
2429 /// Consider operation to be finished when there are no network connections for at least 500ms
2430 NetworkIdle,
2431 /// Consider operation to be finished when the commit event is fired
2432 Commit,
2433}
2434
2435impl WaitUntil {
2436 pub(crate) fn as_str(&self) -> &'static str {
2437 match self {
2438 WaitUntil::Load => "load",
2439 WaitUntil::DomContentLoaded => "domcontentloaded",
2440 WaitUntil::NetworkIdle => "networkidle",
2441 WaitUntil::Commit => "commit",
2442 }
2443 }
2444}
2445
2446/// Options for adding a style tag to the page
2447///
2448/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
2449#[derive(Debug, Clone, Default)]
2450pub struct AddStyleTagOptions {
2451 /// Raw CSS content to inject
2452 pub content: Option<String>,
2453 /// URL of the `<link>` tag to add
2454 pub url: Option<String>,
2455 /// Path to a CSS file to inject
2456 pub path: Option<String>,
2457}
2458
2459impl AddStyleTagOptions {
2460 /// Creates a new builder for AddStyleTagOptions
2461 pub fn builder() -> AddStyleTagOptionsBuilder {
2462 AddStyleTagOptionsBuilder::default()
2463 }
2464
2465 /// Validates that at least one option is specified
2466 pub(crate) fn validate(&self) -> Result<()> {
2467 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
2468 return Err(Error::InvalidArgument(
2469 "At least one of content, url, or path must be specified".to_string(),
2470 ));
2471 }
2472 Ok(())
2473 }
2474}
2475
2476/// Builder for AddStyleTagOptions
2477#[derive(Debug, Clone, Default)]
2478pub struct AddStyleTagOptionsBuilder {
2479 content: Option<String>,
2480 url: Option<String>,
2481 path: Option<String>,
2482}
2483
2484impl AddStyleTagOptionsBuilder {
2485 /// Sets the CSS content to inject
2486 pub fn content(mut self, content: impl Into<String>) -> Self {
2487 self.content = Some(content.into());
2488 self
2489 }
2490
2491 /// Sets the URL of the stylesheet
2492 pub fn url(mut self, url: impl Into<String>) -> Self {
2493 self.url = Some(url.into());
2494 self
2495 }
2496
2497 /// Sets the path to a CSS file
2498 pub fn path(mut self, path: impl Into<String>) -> Self {
2499 self.path = Some(path.into());
2500 self
2501 }
2502
2503 /// Builds the AddStyleTagOptions
2504 pub fn build(self) -> AddStyleTagOptions {
2505 AddStyleTagOptions {
2506 content: self.content,
2507 url: self.url,
2508 path: self.path,
2509 }
2510 }
2511}
2512
2513// ============================================================================
2514// AddScriptTagOptions
2515// ============================================================================
2516
2517/// Options for adding a `<script>` tag to the page.
2518///
2519/// At least one of `content`, `url`, or `path` must be specified.
2520///
2521/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
2522#[derive(Debug, Clone, Default)]
2523pub struct AddScriptTagOptions {
2524 /// Raw JavaScript content to inject
2525 pub content: Option<String>,
2526 /// URL of the `<script>` tag to add
2527 pub url: Option<String>,
2528 /// Path to a JavaScript file to inject (file contents will be read and sent as content)
2529 pub path: Option<String>,
2530 /// Script type attribute (e.g., `"module"`)
2531 pub type_: Option<String>,
2532}
2533
2534impl AddScriptTagOptions {
2535 /// Creates a new builder for AddScriptTagOptions
2536 pub fn builder() -> AddScriptTagOptionsBuilder {
2537 AddScriptTagOptionsBuilder::default()
2538 }
2539
2540 /// Validates that at least one option is specified
2541 pub(crate) fn validate(&self) -> Result<()> {
2542 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
2543 return Err(Error::InvalidArgument(
2544 "At least one of content, url, or path must be specified".to_string(),
2545 ));
2546 }
2547 Ok(())
2548 }
2549}
2550
2551/// Builder for AddScriptTagOptions
2552#[derive(Debug, Clone, Default)]
2553pub struct AddScriptTagOptionsBuilder {
2554 content: Option<String>,
2555 url: Option<String>,
2556 path: Option<String>,
2557 type_: Option<String>,
2558}
2559
2560impl AddScriptTagOptionsBuilder {
2561 /// Sets the JavaScript content to inject
2562 pub fn content(mut self, content: impl Into<String>) -> Self {
2563 self.content = Some(content.into());
2564 self
2565 }
2566
2567 /// Sets the URL of the script to load
2568 pub fn url(mut self, url: impl Into<String>) -> Self {
2569 self.url = Some(url.into());
2570 self
2571 }
2572
2573 /// Sets the path to a JavaScript file to inject
2574 pub fn path(mut self, path: impl Into<String>) -> Self {
2575 self.path = Some(path.into());
2576 self
2577 }
2578
2579 /// Sets the script type attribute (e.g., `"module"`)
2580 pub fn type_(mut self, type_: impl Into<String>) -> Self {
2581 self.type_ = Some(type_.into());
2582 self
2583 }
2584
2585 /// Builds the AddScriptTagOptions
2586 pub fn build(self) -> AddScriptTagOptions {
2587 AddScriptTagOptions {
2588 content: self.content,
2589 url: self.url,
2590 path: self.path,
2591 type_: self.type_,
2592 }
2593 }
2594}
2595
2596// ============================================================================
2597// EmulateMediaOptions and related enums
2598// ============================================================================
2599
2600/// Media type for `page.emulate_media()`.
2601///
2602/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2603#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2604#[serde(rename_all = "lowercase")]
2605pub enum Media {
2606 /// Emulate screen media type
2607 Screen,
2608 /// Emulate print media type
2609 Print,
2610 /// Reset media emulation to browser default (sends `"no-override"` to protocol)
2611 #[serde(rename = "no-override")]
2612 NoOverride,
2613}
2614
2615/// Preferred color scheme for `page.emulate_media()`.
2616///
2617/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2618#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2619pub enum ColorScheme {
2620 /// Emulate light color scheme
2621 #[serde(rename = "light")]
2622 Light,
2623 /// Emulate dark color scheme
2624 #[serde(rename = "dark")]
2625 Dark,
2626 /// Emulate no preference for color scheme
2627 #[serde(rename = "no-preference")]
2628 NoPreference,
2629 /// Reset color scheme to browser default
2630 #[serde(rename = "no-override")]
2631 NoOverride,
2632}
2633
2634/// Reduced motion preference for `page.emulate_media()`.
2635///
2636/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2637#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2638pub enum ReducedMotion {
2639 /// Emulate reduced motion preference
2640 #[serde(rename = "reduce")]
2641 Reduce,
2642 /// Emulate no preference for reduced motion
2643 #[serde(rename = "no-preference")]
2644 NoPreference,
2645 /// Reset reduced motion to browser default
2646 #[serde(rename = "no-override")]
2647 NoOverride,
2648}
2649
2650/// Forced colors preference for `page.emulate_media()`.
2651///
2652/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2653#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2654pub enum ForcedColors {
2655 /// Emulate active forced colors
2656 #[serde(rename = "active")]
2657 Active,
2658 /// Emulate no forced colors
2659 #[serde(rename = "none")]
2660 None_,
2661 /// Reset forced colors to browser default
2662 #[serde(rename = "no-override")]
2663 NoOverride,
2664}
2665
2666/// Options for `page.emulate_media()`.
2667///
2668/// All fields are optional. Fields that are `None` are omitted from the protocol
2669/// message (meaning they are not changed). To reset a field to browser default,
2670/// use the `NoOverride` variant.
2671///
2672/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2673#[derive(Debug, Clone, Default)]
2674pub struct EmulateMediaOptions {
2675 /// Media type to emulate (screen, print, or no-override)
2676 pub media: Option<Media>,
2677 /// Color scheme preference to emulate
2678 pub color_scheme: Option<ColorScheme>,
2679 /// Reduced motion preference to emulate
2680 pub reduced_motion: Option<ReducedMotion>,
2681 /// Forced colors preference to emulate
2682 pub forced_colors: Option<ForcedColors>,
2683}
2684
2685impl EmulateMediaOptions {
2686 /// Creates a new builder for EmulateMediaOptions
2687 pub fn builder() -> EmulateMediaOptionsBuilder {
2688 EmulateMediaOptionsBuilder::default()
2689 }
2690}
2691
2692/// Builder for EmulateMediaOptions
2693#[derive(Debug, Clone, Default)]
2694pub struct EmulateMediaOptionsBuilder {
2695 media: Option<Media>,
2696 color_scheme: Option<ColorScheme>,
2697 reduced_motion: Option<ReducedMotion>,
2698 forced_colors: Option<ForcedColors>,
2699}
2700
2701impl EmulateMediaOptionsBuilder {
2702 /// Sets the media type to emulate
2703 pub fn media(mut self, media: Media) -> Self {
2704 self.media = Some(media);
2705 self
2706 }
2707
2708 /// Sets the color scheme preference
2709 pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
2710 self.color_scheme = Some(color_scheme);
2711 self
2712 }
2713
2714 /// Sets the reduced motion preference
2715 pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
2716 self.reduced_motion = Some(reduced_motion);
2717 self
2718 }
2719
2720 /// Sets the forced colors preference
2721 pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
2722 self.forced_colors = Some(forced_colors);
2723 self
2724 }
2725
2726 /// Builds the EmulateMediaOptions
2727 pub fn build(self) -> EmulateMediaOptions {
2728 EmulateMediaOptions {
2729 media: self.media,
2730 color_scheme: self.color_scheme,
2731 reduced_motion: self.reduced_motion,
2732 forced_colors: self.forced_colors,
2733 }
2734 }
2735}
2736
2737// ============================================================================
2738// PdfOptions
2739// ============================================================================
2740
2741/// Margin options for PDF generation.
2742///
2743/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2744#[derive(Debug, Clone, Default, Serialize)]
2745pub struct PdfMargin {
2746 /// Top margin (e.g. `"1in"`)
2747 #[serde(skip_serializing_if = "Option::is_none")]
2748 pub top: Option<String>,
2749 /// Right margin
2750 #[serde(skip_serializing_if = "Option::is_none")]
2751 pub right: Option<String>,
2752 /// Bottom margin
2753 #[serde(skip_serializing_if = "Option::is_none")]
2754 pub bottom: Option<String>,
2755 /// Left margin
2756 #[serde(skip_serializing_if = "Option::is_none")]
2757 pub left: Option<String>,
2758}
2759
2760/// Options for generating a PDF from a page.
2761///
2762/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
2763/// Firefox or WebKit will result in an error.
2764///
2765/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2766#[derive(Debug, Clone, Default)]
2767pub struct PdfOptions {
2768 /// If specified, the PDF will also be saved to this file path.
2769 pub path: Option<std::path::PathBuf>,
2770 /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
2771 pub scale: Option<f64>,
2772 /// Whether to display header and footer (default false).
2773 pub display_header_footer: Option<bool>,
2774 /// HTML template for the print header. Should be valid HTML.
2775 pub header_template: Option<String>,
2776 /// HTML template for the print footer.
2777 pub footer_template: Option<String>,
2778 /// Whether to print background graphics (default false).
2779 pub print_background: Option<bool>,
2780 /// Paper orientation — `true` for landscape (default false).
2781 pub landscape: Option<bool>,
2782 /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
2783 pub page_ranges: Option<String>,
2784 /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
2785 pub format: Option<String>,
2786 /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
2787 pub width: Option<String>,
2788 /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
2789 pub height: Option<String>,
2790 /// Whether or not to prefer page size as defined by CSS.
2791 pub prefer_css_page_size: Option<bool>,
2792 /// Paper margins, defaulting to none.
2793 pub margin: Option<PdfMargin>,
2794}
2795
2796impl PdfOptions {
2797 /// Creates a new builder for PdfOptions
2798 pub fn builder() -> PdfOptionsBuilder {
2799 PdfOptionsBuilder::default()
2800 }
2801}
2802
2803/// Builder for PdfOptions
2804#[derive(Debug, Clone, Default)]
2805pub struct PdfOptionsBuilder {
2806 path: Option<std::path::PathBuf>,
2807 scale: Option<f64>,
2808 display_header_footer: Option<bool>,
2809 header_template: Option<String>,
2810 footer_template: Option<String>,
2811 print_background: Option<bool>,
2812 landscape: Option<bool>,
2813 page_ranges: Option<String>,
2814 format: Option<String>,
2815 width: Option<String>,
2816 height: Option<String>,
2817 prefer_css_page_size: Option<bool>,
2818 margin: Option<PdfMargin>,
2819}
2820
2821impl PdfOptionsBuilder {
2822 /// Sets the file path for saving the PDF
2823 pub fn path(mut self, path: std::path::PathBuf) -> Self {
2824 self.path = Some(path);
2825 self
2826 }
2827
2828 /// Sets the scale of the webpage rendering
2829 pub fn scale(mut self, scale: f64) -> Self {
2830 self.scale = Some(scale);
2831 self
2832 }
2833
2834 /// Sets whether to display header and footer
2835 pub fn display_header_footer(mut self, display: bool) -> Self {
2836 self.display_header_footer = Some(display);
2837 self
2838 }
2839
2840 /// Sets the HTML template for the print header
2841 pub fn header_template(mut self, template: impl Into<String>) -> Self {
2842 self.header_template = Some(template.into());
2843 self
2844 }
2845
2846 /// Sets the HTML template for the print footer
2847 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
2848 self.footer_template = Some(template.into());
2849 self
2850 }
2851
2852 /// Sets whether to print background graphics
2853 pub fn print_background(mut self, print: bool) -> Self {
2854 self.print_background = Some(print);
2855 self
2856 }
2857
2858 /// Sets whether to use landscape orientation
2859 pub fn landscape(mut self, landscape: bool) -> Self {
2860 self.landscape = Some(landscape);
2861 self
2862 }
2863
2864 /// Sets the page ranges to print
2865 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
2866 self.page_ranges = Some(ranges.into());
2867 self
2868 }
2869
2870 /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
2871 pub fn format(mut self, format: impl Into<String>) -> Self {
2872 self.format = Some(format.into());
2873 self
2874 }
2875
2876 /// Sets the paper width
2877 pub fn width(mut self, width: impl Into<String>) -> Self {
2878 self.width = Some(width.into());
2879 self
2880 }
2881
2882 /// Sets the paper height
2883 pub fn height(mut self, height: impl Into<String>) -> Self {
2884 self.height = Some(height.into());
2885 self
2886 }
2887
2888 /// Sets whether to prefer page size as defined by CSS
2889 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
2890 self.prefer_css_page_size = Some(prefer);
2891 self
2892 }
2893
2894 /// Sets the paper margins
2895 pub fn margin(mut self, margin: PdfMargin) -> Self {
2896 self.margin = Some(margin);
2897 self
2898 }
2899
2900 /// Builds the PdfOptions
2901 pub fn build(self) -> PdfOptions {
2902 PdfOptions {
2903 path: self.path,
2904 scale: self.scale,
2905 display_header_footer: self.display_header_footer,
2906 header_template: self.header_template,
2907 footer_template: self.footer_template,
2908 print_background: self.print_background,
2909 landscape: self.landscape,
2910 page_ranges: self.page_ranges,
2911 format: self.format,
2912 width: self.width,
2913 height: self.height,
2914 prefer_css_page_size: self.prefer_css_page_size,
2915 margin: self.margin,
2916 }
2917 }
2918}
2919
2920/// Response from navigation operations.
2921///
2922/// Returned from `page.goto()`, `page.reload()`, `page.go_back()`, and similar
2923/// navigation methods. Provides access to the HTTP response status, headers, and body.
2924///
2925/// See: <https://playwright.dev/docs/api/class-response>
2926#[derive(Clone)]
2927pub struct Response {
2928 url: String,
2929 status: u16,
2930 status_text: String,
2931 ok: bool,
2932 headers: std::collections::HashMap<String, String>,
2933 /// Reference to the backing channel owner for RPC calls (body, rawHeaders, etc.)
2934 /// Stored as the generic trait object so it can be downcast to ResponseObject when needed.
2935 response_channel_owner: Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
2936}
2937
2938impl Response {
2939 /// Creates a new Response from protocol data.
2940 ///
2941 /// This is used internally when constructing a Response from the protocol
2942 /// initializer (e.g., after `goto` or `reload`).
2943 pub(crate) fn new(
2944 url: String,
2945 status: u16,
2946 status_text: String,
2947 headers: std::collections::HashMap<String, String>,
2948 response_channel_owner: Option<
2949 std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>,
2950 >,
2951 ) -> Self {
2952 Self {
2953 url,
2954 status,
2955 status_text,
2956 ok: (200..300).contains(&status),
2957 headers,
2958 response_channel_owner,
2959 }
2960 }
2961}
2962
2963impl Response {
2964 /// Returns the URL of the response.
2965 ///
2966 /// See: <https://playwright.dev/docs/api/class-response#response-url>
2967 pub fn url(&self) -> &str {
2968 &self.url
2969 }
2970
2971 /// Returns the HTTP status code.
2972 ///
2973 /// See: <https://playwright.dev/docs/api/class-response#response-status>
2974 pub fn status(&self) -> u16 {
2975 self.status
2976 }
2977
2978 /// Returns the HTTP status text.
2979 ///
2980 /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
2981 pub fn status_text(&self) -> &str {
2982 &self.status_text
2983 }
2984
2985 /// Returns whether the response was successful (status 200-299).
2986 ///
2987 /// See: <https://playwright.dev/docs/api/class-response#response-ok>
2988 pub fn ok(&self) -> bool {
2989 self.ok
2990 }
2991
2992 /// Returns the response headers as a HashMap.
2993 ///
2994 /// Note: these are the headers from the protocol initializer. For the full
2995 /// raw headers (including duplicates), use `headers_array()` or `all_headers()`.
2996 ///
2997 /// See: <https://playwright.dev/docs/api/class-response#response-headers>
2998 pub fn headers(&self) -> &std::collections::HashMap<String, String> {
2999 &self.headers
3000 }
3001
3002 /// Returns the [`Request`] that triggered this response.
3003 ///
3004 /// Navigates the protocol object hierarchy: ResponseObject → parent (Request).
3005 ///
3006 /// See: <https://playwright.dev/docs/api/class-response#response-request>
3007 pub fn request(&self) -> Option<crate::protocol::Request> {
3008 let owner = self.response_channel_owner.as_ref()?;
3009 downcast_parent::<crate::protocol::Request>(&**owner)
3010 }
3011
3012 /// Returns the [`Frame`](crate::protocol::Frame) that initiated the request for this response.
3013 ///
3014 /// Navigates the protocol object hierarchy: ResponseObject → Request → Frame.
3015 ///
3016 /// See: <https://playwright.dev/docs/api/class-response#response-frame>
3017 pub fn frame(&self) -> Option<crate::protocol::Frame> {
3018 let request = self.request()?;
3019 request.frame()
3020 }
3021
3022 /// Returns the backing `ResponseObject`, or an error if unavailable.
3023 pub(crate) fn response_object(&self) -> crate::error::Result<crate::protocol::ResponseObject> {
3024 let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
3025 crate::error::Error::ProtocolError(
3026 "Response has no backing protocol object".to_string(),
3027 )
3028 })?;
3029 arc.as_any()
3030 .downcast_ref::<crate::protocol::ResponseObject>()
3031 .cloned()
3032 .ok_or_else(|| crate::error::Error::TypeMismatch {
3033 guid: arc.guid().to_string(),
3034 expected: "ResponseObject".to_string(),
3035 actual: arc.type_name().to_string(),
3036 })
3037 }
3038
3039 /// Returns TLS/SSL security details for HTTPS connections, or `None` for HTTP.
3040 ///
3041 /// See: <https://playwright.dev/docs/api/class-response#response-security-details>
3042 pub async fn security_details(
3043 &self,
3044 ) -> crate::error::Result<Option<crate::protocol::response::SecurityDetails>> {
3045 self.response_object()?.security_details().await
3046 }
3047
3048 /// Returns the server's IP address and port, or `None`.
3049 ///
3050 /// See: <https://playwright.dev/docs/api/class-response#response-server-addr>
3051 pub async fn server_addr(
3052 &self,
3053 ) -> crate::error::Result<Option<crate::protocol::response::RemoteAddr>> {
3054 self.response_object()?.server_addr().await
3055 }
3056
3057 /// Waits for this response to finish loading.
3058 ///
3059 /// For responses obtained from navigation methods (`goto`, `reload`), the response
3060 /// is already finished when returned. For responses from `on_response` handlers,
3061 /// the body may still be loading.
3062 ///
3063 /// See: <https://playwright.dev/docs/api/class-response#response-finished>
3064 pub async fn finished(&self) -> crate::error::Result<()> {
3065 // The Playwright protocol dispatches `requestFinished` as a separate event
3066 // rather than exposing a `finished` RPC method on Response.
3067 // For responses from goto/reload, the response is already complete.
3068 // TODO: For on_response handlers, implement proper waiting via requestFinished event.
3069 Ok(())
3070 }
3071
3072 /// Returns the response body as raw bytes.
3073 ///
3074 /// Makes an RPC call to the Playwright server to fetch the response body.
3075 ///
3076 /// # Errors
3077 ///
3078 /// Returns an error if:
3079 /// - No backing protocol object is available (edge case)
3080 /// - The RPC call to the server fails
3081 /// - The base64 response cannot be decoded
3082 ///
3083 /// See: <https://playwright.dev/docs/api/class-response#response-body>
3084 pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
3085 self.response_object()?.body().await
3086 }
3087
3088 /// Returns the response body as a UTF-8 string.
3089 ///
3090 /// Calls `body()` then converts bytes to a UTF-8 string.
3091 ///
3092 /// # Errors
3093 ///
3094 /// Returns an error if:
3095 /// - `body()` fails
3096 /// - The body is not valid UTF-8
3097 ///
3098 /// See: <https://playwright.dev/docs/api/class-response#response-text>
3099 pub async fn text(&self) -> crate::error::Result<String> {
3100 let bytes = self.body().await?;
3101 String::from_utf8(bytes).map_err(|e| {
3102 crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
3103 })
3104 }
3105
3106 /// Parses the response body as JSON and deserializes it into type `T`.
3107 ///
3108 /// Calls `text()` then uses `serde_json` to deserialize the body.
3109 ///
3110 /// # Errors
3111 ///
3112 /// Returns an error if:
3113 /// - `text()` fails
3114 /// - The body is not valid JSON or doesn't match the expected type
3115 ///
3116 /// See: <https://playwright.dev/docs/api/class-response#response-json>
3117 pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
3118 let text = self.text().await?;
3119 serde_json::from_str(&text).map_err(|e| {
3120 crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
3121 })
3122 }
3123
3124 /// Returns all response headers as name-value pairs, preserving duplicates.
3125 ///
3126 /// Makes an RPC call for `"rawHeaders"` which returns the complete header list.
3127 ///
3128 /// # Errors
3129 ///
3130 /// Returns an error if:
3131 /// - No backing protocol object is available (edge case)
3132 /// - The RPC call to the server fails
3133 ///
3134 /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
3135 pub async fn headers_array(
3136 &self,
3137 ) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
3138 self.response_object()?.raw_headers().await
3139 }
3140
3141 /// Returns all response headers merged into a HashMap with lowercase keys.
3142 ///
3143 /// When multiple headers have the same name, their values are joined with `, `.
3144 /// This matches the behavior of `response.allHeaders()` in other Playwright bindings.
3145 ///
3146 /// # Errors
3147 ///
3148 /// Returns an error if:
3149 /// - No backing protocol object is available (edge case)
3150 /// - The RPC call to the server fails
3151 ///
3152 /// See: <https://playwright.dev/docs/api/class-response#response-all-headers>
3153 pub async fn all_headers(
3154 &self,
3155 ) -> crate::error::Result<std::collections::HashMap<String, String>> {
3156 let entries = self.headers_array().await?;
3157 let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
3158 for entry in entries {
3159 let key = entry.name.to_lowercase();
3160 map.entry(key)
3161 .and_modify(|v| {
3162 v.push_str(", ");
3163 v.push_str(&entry.value);
3164 })
3165 .or_insert(entry.value);
3166 }
3167 Ok(map)
3168 }
3169
3170 /// Returns the value for a single response header, or `None` if not present.
3171 ///
3172 /// The lookup is case-insensitive.
3173 ///
3174 /// # Errors
3175 ///
3176 /// Returns an error if:
3177 /// - No backing protocol object is available (edge case)
3178 /// - The RPC call to the server fails
3179 ///
3180 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
3181 /// Returns the value for a single response header, or `None` if not present.
3182 ///
3183 /// The lookup is case-insensitive. When multiple headers share the same name,
3184 /// their values are joined with `, ` (matching Playwright's behavior).
3185 ///
3186 /// Uses the raw headers from the server for accurate results.
3187 ///
3188 /// # Errors
3189 ///
3190 /// Returns an error if the underlying `headers_array()` RPC call fails.
3191 ///
3192 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
3193 pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
3194 let entries = self.headers_array().await?;
3195 let name_lower = name.to_lowercase();
3196 let mut values: Vec<String> = entries
3197 .into_iter()
3198 .filter(|h| h.name.to_lowercase() == name_lower)
3199 .map(|h| h.value)
3200 .collect();
3201
3202 if values.is_empty() {
3203 Ok(None)
3204 } else if values.len() == 1 {
3205 Ok(Some(values.remove(0)))
3206 } else {
3207 Ok(Some(values.join(", ")))
3208 }
3209 }
3210}
3211
3212impl std::fmt::Debug for Response {
3213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3214 f.debug_struct("Response")
3215 .field("url", &self.url)
3216 .field("status", &self.status)
3217 .field("status_text", &self.status_text)
3218 .field("ok", &self.ok)
3219 .finish_non_exhaustive()
3220 }
3221}
3222
3223/// Shared helper: store timeout locally and notify the Playwright server.
3224/// Used by both Page and BrowserContext timeout setters.
3225pub(crate) async fn set_timeout_and_notify(
3226 channel: &crate::server::channel::Channel,
3227 method: &str,
3228 timeout: f64,
3229) {
3230 if let Err(e) = channel
3231 .send_no_result(method, serde_json::json!({ "timeout": timeout }))
3232 .await
3233 {
3234 tracing::warn!("{} send error: {}", method, e);
3235 }
3236}