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