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 {
1043 url: 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 status_text: initializer["statusText"].as_str().unwrap_or("").to_string(),
1051 ok: (200..300).contains(&status),
1052 headers,
1053 response_channel_owner: Some(response_arc),
1054 };
1055
1056 if let Ok(mut page_url) = self.url.write() {
1057 *page_url = response.url().to_string();
1058 }
1059
1060 Ok(Some(response))
1061 } else {
1062 Ok(None)
1063 }
1064 }
1065
1066 /// Returns the first element matching the selector, or None if not found.
1067 ///
1068 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector>
1069 pub async fn query_selector(
1070 &self,
1071 selector: &str,
1072 ) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
1073 let frame = self.main_frame().await?;
1074 frame.query_selector(selector).await
1075 }
1076
1077 /// Returns all elements matching the selector.
1078 ///
1079 /// See: <https://playwright.dev/docs/api/class-page#page-query-selector-all>
1080 pub async fn query_selector_all(
1081 &self,
1082 selector: &str,
1083 ) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
1084 let frame = self.main_frame().await?;
1085 frame.query_selector_all(selector).await
1086 }
1087
1088 /// Takes a screenshot of the page and returns the image bytes.
1089 ///
1090 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1091 pub async fn screenshot(
1092 &self,
1093 options: Option<crate::protocol::ScreenshotOptions>,
1094 ) -> Result<Vec<u8>> {
1095 let params = if let Some(opts) = options {
1096 opts.to_json()
1097 } else {
1098 // Default to PNG with required timeout
1099 serde_json::json!({
1100 "type": "png",
1101 "timeout": crate::DEFAULT_TIMEOUT_MS
1102 })
1103 };
1104
1105 #[derive(Deserialize)]
1106 struct ScreenshotResponse {
1107 binary: String,
1108 }
1109
1110 let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
1111
1112 // Decode base64 to bytes
1113 let bytes = base64::prelude::BASE64_STANDARD
1114 .decode(&response.binary)
1115 .map_err(|e| {
1116 crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
1117 })?;
1118
1119 Ok(bytes)
1120 }
1121
1122 /// Takes a screenshot and saves it to a file, also returning the bytes.
1123 ///
1124 /// See: <https://playwright.dev/docs/api/class-page#page-screenshot>
1125 pub async fn screenshot_to_file(
1126 &self,
1127 path: &std::path::Path,
1128 options: Option<crate::protocol::ScreenshotOptions>,
1129 ) -> Result<Vec<u8>> {
1130 // Get the screenshot bytes
1131 let bytes = self.screenshot(options).await?;
1132
1133 // Write to file
1134 tokio::fs::write(path, &bytes).await.map_err(|e| {
1135 crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
1136 })?;
1137
1138 Ok(bytes)
1139 }
1140
1141 /// Evaluates JavaScript in the page context (without return value).
1142 ///
1143 /// Executes the provided JavaScript expression or function within the page's
1144 /// context without returning a value.
1145 ///
1146 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1147 pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
1148 // Delegate to the main frame
1149 let frame = self.main_frame().await?;
1150 frame.frame_evaluate_expression(expression).await
1151 }
1152
1153 /// Evaluates JavaScript in the page context with optional arguments.
1154 ///
1155 /// Executes the provided JavaScript expression or function within the page's
1156 /// context and returns the result. The return value must be JSON-serializable.
1157 ///
1158 /// # Arguments
1159 ///
1160 /// * `expression` - JavaScript code to evaluate
1161 /// * `arg` - Optional argument to pass to the expression (must implement Serialize)
1162 ///
1163 /// # Returns
1164 ///
1165 /// The result as a `serde_json::Value`
1166 ///
1167 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1168 pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
1169 &self,
1170 expression: &str,
1171 arg: Option<&T>,
1172 ) -> Result<U> {
1173 // Delegate to the main frame
1174 let frame = self.main_frame().await?;
1175 let result = frame.evaluate(expression, arg).await?;
1176 serde_json::from_value(result).map_err(Error::from)
1177 }
1178
1179 /// Evaluates a JavaScript expression and returns the result as a String.
1180 ///
1181 /// # Arguments
1182 ///
1183 /// * `expression` - JavaScript code to evaluate
1184 ///
1185 /// # Returns
1186 ///
1187 /// The result converted to a String
1188 ///
1189 /// See: <https://playwright.dev/docs/api/class-page#page-evaluate>
1190 pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
1191 let frame = self.main_frame().await?;
1192 frame.frame_evaluate_expression_value(expression).await
1193 }
1194
1195 /// Registers a route handler for network interception.
1196 ///
1197 /// When a request matches the specified pattern, the handler will be called
1198 /// with a Route object that can abort, continue, or fulfill the request.
1199 ///
1200 /// # Arguments
1201 ///
1202 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
1203 /// * `handler` - Async closure that handles the route
1204 ///
1205 /// See: <https://playwright.dev/docs/api/class-page#page-route>
1206 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1207 where
1208 F: Fn(Route) -> Fut + Send + Sync + 'static,
1209 Fut: Future<Output = Result<()>> + Send + 'static,
1210 {
1211 // 1. Wrap handler in Arc with type erasure
1212 let handler =
1213 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1214
1215 // 2. Store in handlers list
1216 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1217 pattern: pattern.to_string(),
1218 handler,
1219 });
1220
1221 // 3. Enable network interception via protocol
1222 self.enable_network_interception().await?;
1223
1224 Ok(())
1225 }
1226
1227 /// Updates network interception patterns for this page
1228 async fn enable_network_interception(&self) -> Result<()> {
1229 // Collect all patterns from registered handlers
1230 // Each pattern must be an object with "glob" field
1231 let patterns: Vec<serde_json::Value> = self
1232 .route_handlers
1233 .lock()
1234 .unwrap()
1235 .iter()
1236 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
1237 .collect();
1238
1239 // Send protocol command to update network interception patterns
1240 // Follows playwright-python's approach
1241 self.channel()
1242 .send_no_result(
1243 "setNetworkInterceptionPatterns",
1244 serde_json::json!({
1245 "patterns": patterns
1246 }),
1247 )
1248 .await
1249 }
1250
1251 /// Removes route handler(s) matching the given URL pattern.
1252 ///
1253 /// # Arguments
1254 ///
1255 /// * `pattern` - URL pattern to remove handlers for
1256 ///
1257 /// See: <https://playwright.dev/docs/api/class-page#page-unroute>
1258 pub async fn unroute(&self, pattern: &str) -> Result<()> {
1259 self.route_handlers
1260 .lock()
1261 .unwrap()
1262 .retain(|entry| entry.pattern != pattern);
1263 self.enable_network_interception().await
1264 }
1265
1266 /// Removes all registered route handlers.
1267 ///
1268 /// # Arguments
1269 ///
1270 /// * `behavior` - Optional behavior for in-flight handlers
1271 ///
1272 /// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
1273 pub async fn unroute_all(
1274 &self,
1275 _behavior: Option<crate::protocol::route::UnrouteBehavior>,
1276 ) -> Result<()> {
1277 self.route_handlers.lock().unwrap().clear();
1278 self.enable_network_interception().await
1279 }
1280
1281 /// Handles a route event from the protocol
1282 ///
1283 /// Called by on_event when a "route" event is received.
1284 /// Supports handler chaining via `route.fallback()` — if a handler calls
1285 /// `fallback()` instead of `continue_()`, `abort()`, or `fulfill()`, the
1286 /// next matching handler in the chain is tried.
1287 async fn on_route_event(&self, route: Route) {
1288 let handlers = self.route_handlers.lock().unwrap().clone();
1289 let url = route.request().url().to_string();
1290
1291 // Find matching handler (last registered wins, with fallback chaining)
1292 for entry in handlers.iter().rev() {
1293 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
1294 let handler = entry.handler.clone();
1295 if let Err(e) = handler(route.clone()).await {
1296 tracing::warn!("Route handler error: {}", e);
1297 break;
1298 }
1299 // If handler called fallback(), try the next matching handler
1300 if !route.was_handled() {
1301 continue;
1302 }
1303 break;
1304 }
1305 }
1306 }
1307
1308 /// Registers a download event handler.
1309 ///
1310 /// The handler will be called when a download is triggered by the page.
1311 /// Downloads occur when the page initiates a file download (e.g., clicking a link
1312 /// with the download attribute, or a server response with Content-Disposition: attachment).
1313 ///
1314 /// # Arguments
1315 ///
1316 /// * `handler` - Async closure that receives the Download object
1317 ///
1318 /// See: <https://playwright.dev/docs/api/class-page#page-event-download>
1319 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1320 where
1321 F: Fn(Download) -> Fut + Send + Sync + 'static,
1322 Fut: Future<Output = Result<()>> + Send + 'static,
1323 {
1324 // Wrap handler with type erasure
1325 let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
1326 Box::pin(handler(download))
1327 });
1328
1329 // Store handler
1330 self.download_handlers.lock().unwrap().push(handler);
1331
1332 Ok(())
1333 }
1334
1335 /// Registers a dialog event handler.
1336 ///
1337 /// The handler will be called when a JavaScript dialog is triggered (alert, confirm, prompt, or beforeunload).
1338 /// The dialog must be explicitly accepted or dismissed, otherwise the page will freeze.
1339 ///
1340 /// # Arguments
1341 ///
1342 /// * `handler` - Async closure that receives the Dialog object
1343 ///
1344 /// See: <https://playwright.dev/docs/api/class-page#page-event-dialog>
1345 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1346 where
1347 F: Fn(Dialog) -> Fut + Send + Sync + 'static,
1348 Fut: Future<Output = Result<()>> + Send + 'static,
1349 {
1350 // Wrap handler with type erasure
1351 let handler =
1352 Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
1353
1354 // Store handler
1355 self.dialog_handlers.lock().unwrap().push(handler);
1356
1357 // Dialog events are auto-emitted (no subscription needed)
1358
1359 Ok(())
1360 }
1361
1362 /// See: <https://playwright.dev/docs/api/class-page#page-event-request>
1363 pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
1364 where
1365 F: Fn(Request) -> Fut + Send + Sync + 'static,
1366 Fut: Future<Output = Result<()>> + Send + 'static,
1367 {
1368 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1369 Box::pin(handler(request))
1370 });
1371
1372 let needs_subscription = self.request_handlers.lock().unwrap().is_empty();
1373 if needs_subscription {
1374 _ = self.channel().update_subscription("request", true).await;
1375 }
1376 self.request_handlers.lock().unwrap().push(handler);
1377
1378 Ok(())
1379 }
1380
1381 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-finished>
1382 pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
1383 where
1384 F: Fn(Request) -> Fut + Send + Sync + 'static,
1385 Fut: Future<Output = Result<()>> + Send + 'static,
1386 {
1387 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1388 Box::pin(handler(request))
1389 });
1390
1391 let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
1392 if needs_subscription {
1393 _ = self
1394 .channel()
1395 .update_subscription("requestFinished", true)
1396 .await;
1397 }
1398 self.request_finished_handlers.lock().unwrap().push(handler);
1399
1400 Ok(())
1401 }
1402
1403 /// See: <https://playwright.dev/docs/api/class-page#page-event-request-failed>
1404 pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
1405 where
1406 F: Fn(Request) -> Fut + Send + Sync + 'static,
1407 Fut: Future<Output = Result<()>> + Send + 'static,
1408 {
1409 let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
1410 Box::pin(handler(request))
1411 });
1412
1413 let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
1414 if needs_subscription {
1415 _ = self
1416 .channel()
1417 .update_subscription("requestFailed", true)
1418 .await;
1419 }
1420 self.request_failed_handlers.lock().unwrap().push(handler);
1421
1422 Ok(())
1423 }
1424
1425 /// See: <https://playwright.dev/docs/api/class-page#page-event-response>
1426 pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
1427 where
1428 F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
1429 Fut: Future<Output = Result<()>> + Send + 'static,
1430 {
1431 let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
1432 Box::pin(handler(response))
1433 });
1434
1435 let needs_subscription = self.response_handlers.lock().unwrap().is_empty();
1436 if needs_subscription {
1437 _ = self.channel().update_subscription("response", true).await;
1438 }
1439 self.response_handlers.lock().unwrap().push(handler);
1440
1441 Ok(())
1442 }
1443
1444 /// Adds a listener for the `websocket` event.
1445 ///
1446 /// The handler will be called when a WebSocket request is dispatched.
1447 ///
1448 /// # Arguments
1449 ///
1450 /// * `handler` - The function to call when the event occurs
1451 ///
1452 /// See: <https://playwright.dev/docs/api/class-page#page-on-websocket>
1453 pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
1454 where
1455 F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
1456 Fut: Future<Output = Result<()>> + Send + 'static,
1457 {
1458 let handler =
1459 Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
1460 self.websocket_handlers.lock().unwrap().push(handler);
1461 Ok(())
1462 }
1463
1464 /// Handles a download event from the protocol
1465 async fn on_download_event(&self, download: Download) {
1466 let handlers = self.download_handlers.lock().unwrap().clone();
1467
1468 for handler in handlers {
1469 if let Err(e) = handler(download.clone()).await {
1470 tracing::warn!("Download handler error: {}", e);
1471 }
1472 }
1473 }
1474
1475 /// Handles a dialog event from the protocol
1476 async fn on_dialog_event(&self, dialog: Dialog) {
1477 let handlers = self.dialog_handlers.lock().unwrap().clone();
1478
1479 for handler in handlers {
1480 if let Err(e) = handler(dialog.clone()).await {
1481 tracing::warn!("Dialog handler error: {}", e);
1482 }
1483 }
1484 }
1485
1486 async fn on_request_event(&self, request: Request) {
1487 let handlers = self.request_handlers.lock().unwrap().clone();
1488
1489 for handler in handlers {
1490 if let Err(e) = handler(request.clone()).await {
1491 tracing::warn!("Request handler error: {}", e);
1492 }
1493 }
1494 }
1495
1496 async fn on_request_failed_event(&self, request: Request) {
1497 let handlers = self.request_failed_handlers.lock().unwrap().clone();
1498
1499 for handler in handlers {
1500 if let Err(e) = handler(request.clone()).await {
1501 tracing::warn!("RequestFailed handler error: {}", e);
1502 }
1503 }
1504 }
1505
1506 async fn on_request_finished_event(&self, request: Request) {
1507 let handlers = self.request_finished_handlers.lock().unwrap().clone();
1508
1509 for handler in handlers {
1510 if let Err(e) = handler(request.clone()).await {
1511 tracing::warn!("RequestFinished handler error: {}", e);
1512 }
1513 }
1514 }
1515
1516 async fn on_response_event(&self, response: ResponseObject) {
1517 let handlers = self.response_handlers.lock().unwrap().clone();
1518
1519 for handler in handlers {
1520 if let Err(e) = handler(response.clone()).await {
1521 tracing::warn!("Response handler error: {}", e);
1522 }
1523 }
1524 }
1525
1526 /// Triggers dialog event (called by BrowserContext when dialog events arrive)
1527 ///
1528 /// Dialog events are sent to BrowserContext and forwarded to the associated Page.
1529 /// This method is public so BrowserContext can forward dialog events.
1530 pub async fn trigger_dialog_event(&self, dialog: Dialog) {
1531 self.on_dialog_event(dialog).await;
1532 }
1533
1534 /// Triggers request event (called by BrowserContext when request events arrive)
1535 pub(crate) async fn trigger_request_event(&self, request: Request) {
1536 self.on_request_event(request).await;
1537 }
1538
1539 pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
1540 self.on_request_finished_event(request).await;
1541 }
1542
1543 pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
1544 self.on_request_failed_event(request).await;
1545 }
1546
1547 /// Triggers response event (called by BrowserContext when response events arrive)
1548 pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
1549 self.on_response_event(response).await;
1550 }
1551
1552 /// Adds a `<style>` tag into the page with the desired content.
1553 ///
1554 /// # Arguments
1555 ///
1556 /// * `options` - Style tag options (content, url, or path)
1557 ///
1558 /// # Returns
1559 ///
1560 /// Returns an ElementHandle pointing to the injected `<style>` tag
1561 ///
1562 /// # Example
1563 ///
1564 /// ```no_run
1565 /// # use playwright_rs::protocol::{Playwright, AddStyleTagOptions};
1566 /// # #[tokio::main]
1567 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1568 /// # let playwright = Playwright::launch().await?;
1569 /// # let browser = playwright.chromium().launch().await?;
1570 /// # let context = browser.new_context().await?;
1571 /// # let page = context.new_page().await?;
1572 /// use playwright_rs::protocol::AddStyleTagOptions;
1573 ///
1574 /// // With inline CSS
1575 /// page.add_style_tag(
1576 /// AddStyleTagOptions::builder()
1577 /// .content("body { background-color: red; }")
1578 /// .build()
1579 /// ).await?;
1580 ///
1581 /// // With external URL
1582 /// page.add_style_tag(
1583 /// AddStyleTagOptions::builder()
1584 /// .url("https://example.com/style.css")
1585 /// .build()
1586 /// ).await?;
1587 ///
1588 /// // From file
1589 /// page.add_style_tag(
1590 /// AddStyleTagOptions::builder()
1591 /// .path("./styles/custom.css")
1592 /// .build()
1593 /// ).await?;
1594 /// # Ok(())
1595 /// # }
1596 /// ```
1597 ///
1598 /// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
1599 pub async fn add_style_tag(
1600 &self,
1601 options: AddStyleTagOptions,
1602 ) -> Result<Arc<crate::protocol::ElementHandle>> {
1603 let frame = self.main_frame().await?;
1604 frame.add_style_tag(options).await
1605 }
1606
1607 /// Adds a script which would be evaluated in one of the following scenarios:
1608 /// - Whenever the page is navigated
1609 /// - Whenever a child frame is attached or navigated
1610 ///
1611 /// The script is evaluated after the document was created but before any of its scripts were run.
1612 ///
1613 /// # Arguments
1614 ///
1615 /// * `script` - JavaScript code to be injected into the page
1616 ///
1617 /// # Example
1618 ///
1619 /// ```no_run
1620 /// # use playwright_rs::protocol::Playwright;
1621 /// # #[tokio::main]
1622 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1623 /// # let playwright = Playwright::launch().await?;
1624 /// # let browser = playwright.chromium().launch().await?;
1625 /// # let context = browser.new_context().await?;
1626 /// # let page = context.new_page().await?;
1627 /// page.add_init_script("window.injected = 123;").await?;
1628 /// # Ok(())
1629 /// # }
1630 /// ```
1631 ///
1632 /// See: <https://playwright.dev/docs/api/class-page#page-add-init-script>
1633 pub async fn add_init_script(&self, script: &str) -> Result<()> {
1634 self.channel()
1635 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
1636 .await
1637 }
1638
1639 /// Sets the viewport size for the page.
1640 ///
1641 /// This method allows dynamic resizing of the viewport after page creation,
1642 /// useful for testing responsive layouts at different screen sizes.
1643 ///
1644 /// # Arguments
1645 ///
1646 /// * `viewport` - The viewport dimensions (width and height in pixels)
1647 ///
1648 /// # Example
1649 ///
1650 /// ```no_run
1651 /// # use playwright_rs::protocol::{Playwright, Viewport};
1652 /// # #[tokio::main]
1653 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1654 /// # let playwright = Playwright::launch().await?;
1655 /// # let browser = playwright.chromium().launch().await?;
1656 /// # let page = browser.new_page().await?;
1657 /// // Set viewport to mobile size
1658 /// let mobile = Viewport {
1659 /// width: 375,
1660 /// height: 667,
1661 /// };
1662 /// page.set_viewport_size(mobile).await?;
1663 ///
1664 /// // Later, test desktop layout
1665 /// let desktop = Viewport {
1666 /// width: 1920,
1667 /// height: 1080,
1668 /// };
1669 /// page.set_viewport_size(desktop).await?;
1670 /// # Ok(())
1671 /// # }
1672 /// ```
1673 ///
1674 /// # Errors
1675 ///
1676 /// Returns error if:
1677 /// - Page has been closed
1678 /// - Communication with browser process fails
1679 ///
1680 /// See: <https://playwright.dev/docs/api/class-page#page-set-viewport-size>
1681 pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
1682 // Store the new viewport locally so viewport_size() can reflect the change
1683 if let Ok(mut guard) = self.viewport.write() {
1684 *guard = Some(viewport.clone());
1685 }
1686 self.channel()
1687 .send_no_result(
1688 "setViewportSize",
1689 serde_json::json!({ "viewportSize": viewport }),
1690 )
1691 .await
1692 }
1693
1694 /// Brings this page to the front (activates the tab).
1695 ///
1696 /// Activates the page in the browser, making it the focused tab. This is
1697 /// useful in multi-page tests to ensure actions target the correct page.
1698 ///
1699 /// # Errors
1700 ///
1701 /// Returns error if:
1702 /// - Page has been closed
1703 /// - Communication with browser process fails
1704 ///
1705 /// See: <https://playwright.dev/docs/api/class-page#page-bring-to-front>
1706 pub async fn bring_to_front(&self) -> Result<()> {
1707 self.channel()
1708 .send_no_result("bringToFront", serde_json::json!({}))
1709 .await
1710 }
1711
1712 /// Sets extra HTTP headers that will be sent with every request from this page.
1713 ///
1714 /// These headers are sent in addition to headers set on the browser context via
1715 /// `BrowserContext::set_extra_http_headers()`. Page-level headers take precedence
1716 /// over context-level headers when names conflict.
1717 ///
1718 /// # Arguments
1719 ///
1720 /// * `headers` - Map of header names to values.
1721 ///
1722 /// # Errors
1723 ///
1724 /// Returns error if:
1725 /// - Page has been closed
1726 /// - Communication with browser process fails
1727 ///
1728 /// See: <https://playwright.dev/docs/api/class-page#page-set-extra-http-headers>
1729 pub async fn set_extra_http_headers(
1730 &self,
1731 headers: std::collections::HashMap<String, String>,
1732 ) -> Result<()> {
1733 // Playwright protocol expects an array of {name, value} objects
1734 // This RPC is sent on the Page channel (not the Frame channel)
1735 let headers_array: Vec<serde_json::Value> = headers
1736 .into_iter()
1737 .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
1738 .collect();
1739 self.channel()
1740 .send_no_result(
1741 "setExtraHTTPHeaders",
1742 serde_json::json!({ "headers": headers_array }),
1743 )
1744 .await
1745 }
1746
1747 /// Emulates media features for the page.
1748 ///
1749 /// This method allows emulating CSS media features such as `media`, `color-scheme`,
1750 /// `reduced-motion`, and `forced-colors`. Pass `None` to call with no changes.
1751 ///
1752 /// To reset a specific feature to the browser default, use the `NoOverride` variant.
1753 ///
1754 /// # Arguments
1755 ///
1756 /// * `options` - Optional emulation options. If `None`, this is a no-op.
1757 ///
1758 /// # Example
1759 ///
1760 /// ```no_run
1761 /// # use playwright_rs::protocol::{Playwright, EmulateMediaOptions, Media, ColorScheme};
1762 /// # #[tokio::main]
1763 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1764 /// # let playwright = Playwright::launch().await?;
1765 /// # let browser = playwright.chromium().launch().await?;
1766 /// # let page = browser.new_page().await?;
1767 /// // Emulate print media
1768 /// page.emulate_media(Some(
1769 /// EmulateMediaOptions::builder()
1770 /// .media(Media::Print)
1771 /// .build()
1772 /// )).await?;
1773 ///
1774 /// // Emulate dark color scheme
1775 /// page.emulate_media(Some(
1776 /// EmulateMediaOptions::builder()
1777 /// .color_scheme(ColorScheme::Dark)
1778 /// .build()
1779 /// )).await?;
1780 /// # Ok(())
1781 /// # }
1782 /// ```
1783 ///
1784 /// # Errors
1785 ///
1786 /// Returns error if:
1787 /// - Page has been closed
1788 /// - Communication with browser process fails
1789 ///
1790 /// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
1791 pub async fn emulate_media(&self, options: Option<EmulateMediaOptions>) -> Result<()> {
1792 let mut params = serde_json::json!({});
1793
1794 if let Some(opts) = options {
1795 if let Some(media) = opts.media {
1796 params["media"] = serde_json::to_value(media).map_err(|e| {
1797 crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
1798 })?;
1799 }
1800 if let Some(color_scheme) = opts.color_scheme {
1801 params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
1802 crate::error::Error::ProtocolError(format!(
1803 "Failed to serialize colorScheme: {}",
1804 e
1805 ))
1806 })?;
1807 }
1808 if let Some(reduced_motion) = opts.reduced_motion {
1809 params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
1810 crate::error::Error::ProtocolError(format!(
1811 "Failed to serialize reducedMotion: {}",
1812 e
1813 ))
1814 })?;
1815 }
1816 if let Some(forced_colors) = opts.forced_colors {
1817 params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
1818 crate::error::Error::ProtocolError(format!(
1819 "Failed to serialize forcedColors: {}",
1820 e
1821 ))
1822 })?;
1823 }
1824 }
1825
1826 self.channel().send_no_result("emulateMedia", params).await
1827 }
1828
1829 /// Generates a PDF of the page and returns it as bytes.
1830 ///
1831 /// Note: Generating a PDF is only supported in Chromium headless. PDF generation is
1832 /// not supported in Firefox or WebKit.
1833 ///
1834 /// The PDF bytes are returned. If `options.path` is set, the PDF will also be
1835 /// saved to that file.
1836 ///
1837 /// # Arguments
1838 ///
1839 /// * `options` - Optional PDF generation options
1840 ///
1841 /// # Example
1842 ///
1843 /// ```no_run
1844 /// # use playwright_rs::protocol::Playwright;
1845 /// # #[tokio::main]
1846 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1847 /// # let playwright = Playwright::launch().await?;
1848 /// # let browser = playwright.chromium().launch().await?;
1849 /// # let page = browser.new_page().await?;
1850 /// let pdf_bytes = page.pdf(None).await?;
1851 /// assert!(!pdf_bytes.is_empty());
1852 /// # Ok(())
1853 /// # }
1854 /// ```
1855 ///
1856 /// # Errors
1857 ///
1858 /// Returns error if:
1859 /// - The browser is not Chromium (PDF only supported in Chromium)
1860 /// - Page has been closed
1861 /// - Communication with browser process fails
1862 ///
1863 /// See: <https://playwright.dev/docs/api/class-page#page-pdf>
1864 pub async fn pdf(&self, options: Option<PdfOptions>) -> Result<Vec<u8>> {
1865 let mut params = serde_json::json!({});
1866 let mut save_path: Option<std::path::PathBuf> = None;
1867
1868 if let Some(opts) = options {
1869 // Capture the file path before consuming opts
1870 save_path = opts.path;
1871
1872 if let Some(scale) = opts.scale {
1873 params["scale"] = serde_json::json!(scale);
1874 }
1875 if let Some(v) = opts.display_header_footer {
1876 params["displayHeaderFooter"] = serde_json::json!(v);
1877 }
1878 if let Some(v) = opts.header_template {
1879 params["headerTemplate"] = serde_json::json!(v);
1880 }
1881 if let Some(v) = opts.footer_template {
1882 params["footerTemplate"] = serde_json::json!(v);
1883 }
1884 if let Some(v) = opts.print_background {
1885 params["printBackground"] = serde_json::json!(v);
1886 }
1887 if let Some(v) = opts.landscape {
1888 params["landscape"] = serde_json::json!(v);
1889 }
1890 if let Some(v) = opts.page_ranges {
1891 params["pageRanges"] = serde_json::json!(v);
1892 }
1893 if let Some(v) = opts.format {
1894 params["format"] = serde_json::json!(v);
1895 }
1896 if let Some(v) = opts.width {
1897 params["width"] = serde_json::json!(v);
1898 }
1899 if let Some(v) = opts.height {
1900 params["height"] = serde_json::json!(v);
1901 }
1902 if let Some(v) = opts.prefer_css_page_size {
1903 params["preferCSSPageSize"] = serde_json::json!(v);
1904 }
1905 if let Some(margin) = opts.margin {
1906 params["margin"] = serde_json::to_value(margin).map_err(|e| {
1907 crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
1908 })?;
1909 }
1910 }
1911
1912 #[derive(Deserialize)]
1913 struct PdfResponse {
1914 pdf: String,
1915 }
1916
1917 let response: PdfResponse = self.channel().send("pdf", params).await?;
1918
1919 // Decode base64 to bytes
1920 let pdf_bytes = base64::engine::general_purpose::STANDARD
1921 .decode(&response.pdf)
1922 .map_err(|e| {
1923 crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
1924 })?;
1925
1926 // If a path was specified, save the PDF to disk as well
1927 if let Some(path) = save_path {
1928 tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
1929 crate::error::Error::InvalidArgument(format!(
1930 "Failed to write PDF to '{}': {}",
1931 path.display(),
1932 e
1933 ))
1934 })?;
1935 }
1936
1937 Ok(pdf_bytes)
1938 }
1939
1940 /// Adds a `<script>` tag into the page with the desired URL or content.
1941 ///
1942 /// # Arguments
1943 ///
1944 /// * `options` - Optional script tag options (content, url, or path).
1945 /// If `None`, returns an error because no source is specified.
1946 ///
1947 /// At least one of `content`, `url`, or `path` must be provided.
1948 ///
1949 /// # Example
1950 ///
1951 /// ```no_run
1952 /// # use playwright_rs::protocol::{Playwright, AddScriptTagOptions};
1953 /// # #[tokio::main]
1954 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1955 /// # let playwright = Playwright::launch().await?;
1956 /// # let browser = playwright.chromium().launch().await?;
1957 /// # let context = browser.new_context().await?;
1958 /// # let page = context.new_page().await?;
1959 /// // With inline JavaScript
1960 /// page.add_script_tag(Some(
1961 /// AddScriptTagOptions::builder()
1962 /// .content("window.myVar = 42;")
1963 /// .build()
1964 /// )).await?;
1965 ///
1966 /// // With external URL
1967 /// page.add_script_tag(Some(
1968 /// AddScriptTagOptions::builder()
1969 /// .url("https://example.com/script.js")
1970 /// .build()
1971 /// )).await?;
1972 /// # Ok(())
1973 /// # }
1974 /// ```
1975 ///
1976 /// # Errors
1977 ///
1978 /// Returns error if:
1979 /// - `options` is `None` or no content/url/path is specified
1980 /// - Page has been closed
1981 /// - Script loading fails (e.g., invalid URL)
1982 ///
1983 /// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
1984 pub async fn add_script_tag(
1985 &self,
1986 options: Option<AddScriptTagOptions>,
1987 ) -> Result<Arc<crate::protocol::ElementHandle>> {
1988 let opts = options.ok_or_else(|| {
1989 Error::InvalidArgument(
1990 "At least one of content, url, or path must be specified".to_string(),
1991 )
1992 })?;
1993 let frame = self.main_frame().await?;
1994 frame.add_script_tag(opts).await
1995 }
1996
1997 /// Returns the current viewport size of the page, or `None` if no viewport is set.
1998 ///
1999 /// Returns `None` when the context was created with `no_viewport: true`. Otherwise
2000 /// returns the dimensions configured at context creation time or updated via
2001 /// `set_viewport_size()`.
2002 ///
2003 /// # Example
2004 ///
2005 /// ```ignore
2006 /// # use playwright_rs::protocol::{Playwright, BrowserContextOptions, Viewport};
2007 /// # #[tokio::main]
2008 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2009 /// # let playwright = Playwright::launch().await?;
2010 /// # let browser = playwright.chromium().launch().await?;
2011 /// let context = browser.new_context_with_options(
2012 /// BrowserContextOptions::builder().viewport(Viewport { width: 1280, height: 720 }).build()
2013 /// ).await?;
2014 /// let page = context.new_page().await?;
2015 /// let size = page.viewport_size().expect("Viewport should be set");
2016 /// assert_eq!(size.width, 1280);
2017 /// assert_eq!(size.height, 720);
2018 /// # Ok(())
2019 /// # }
2020 /// ```
2021 ///
2022 /// See: <https://playwright.dev/docs/api/class-page#page-viewport-size>
2023 pub fn viewport_size(&self) -> Option<Viewport> {
2024 self.viewport.read().ok()?.clone()
2025 }
2026}
2027
2028impl ChannelOwner for Page {
2029 fn guid(&self) -> &str {
2030 self.base.guid()
2031 }
2032
2033 fn type_name(&self) -> &str {
2034 self.base.type_name()
2035 }
2036
2037 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2038 self.base.parent()
2039 }
2040
2041 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2042 self.base.connection()
2043 }
2044
2045 fn initializer(&self) -> &Value {
2046 self.base.initializer()
2047 }
2048
2049 fn channel(&self) -> &Channel {
2050 self.base.channel()
2051 }
2052
2053 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2054 self.base.dispose(reason)
2055 }
2056
2057 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2058 self.base.adopt(child)
2059 }
2060
2061 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2062 self.base.add_child(guid, child)
2063 }
2064
2065 fn remove_child(&self, guid: &str) {
2066 self.base.remove_child(guid)
2067 }
2068
2069 fn on_event(&self, method: &str, params: Value) {
2070 match method {
2071 "navigated" => {
2072 // Update URL when page navigates
2073 if let Some(url_value) = params.get("url") {
2074 if let Some(url_str) = url_value.as_str() {
2075 if let Ok(mut url) = self.url.write() {
2076 *url = url_str.to_string();
2077 }
2078 }
2079 }
2080 }
2081 "route" => {
2082 // Handle network routing event
2083 if let Some(route_guid) = params
2084 .get("route")
2085 .and_then(|v| v.get("guid"))
2086 .and_then(|v| v.as_str())
2087 {
2088 // Get the Route object from connection's registry
2089 let connection = self.connection();
2090 let route_guid_owned = route_guid.to_string();
2091 let self_clone = self.clone();
2092
2093 tokio::spawn(async move {
2094 // Wait for Route object to be created
2095 let route_arc = match connection.get_object(&route_guid_owned).await {
2096 Ok(obj) => obj,
2097 Err(e) => {
2098 tracing::warn!("Failed to get route object: {}", e);
2099 return;
2100 }
2101 };
2102
2103 // Downcast to Route
2104 let route = match route_arc.as_any().downcast_ref::<Route>() {
2105 Some(r) => r.clone(),
2106 None => {
2107 tracing::warn!("Failed to downcast to Route");
2108 return;
2109 }
2110 };
2111
2112 // Set APIRequestContext on the route for fetch() support.
2113 // Page's parent is BrowserContext, which has the request context.
2114 if let Some(parent) = self_clone.parent() {
2115 if let Some(ctx) = parent
2116 .as_any()
2117 .downcast_ref::<crate::protocol::BrowserContext>()
2118 {
2119 if let Ok(api_ctx) = ctx.request().await {
2120 route.set_api_request_context(api_ctx);
2121 }
2122 }
2123 }
2124
2125 // Call the route handler and wait for completion
2126 self_clone.on_route_event(route).await;
2127 });
2128 }
2129 }
2130 "download" => {
2131 // Handle download event
2132 // Event params: {url, suggestedFilename, artifact: {guid: "..."}}
2133 let url = params
2134 .get("url")
2135 .and_then(|v| v.as_str())
2136 .unwrap_or("")
2137 .to_string();
2138
2139 let suggested_filename = params
2140 .get("suggestedFilename")
2141 .and_then(|v| v.as_str())
2142 .unwrap_or("")
2143 .to_string();
2144
2145 if let Some(artifact_guid) = params
2146 .get("artifact")
2147 .and_then(|v| v.get("guid"))
2148 .and_then(|v| v.as_str())
2149 {
2150 let connection = self.connection();
2151 let artifact_guid_owned = artifact_guid.to_string();
2152 let self_clone = self.clone();
2153
2154 tokio::spawn(async move {
2155 // Wait for Artifact object to be created
2156 let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
2157 Ok(obj) => obj,
2158 Err(e) => {
2159 tracing::warn!("Failed to get artifact object: {}", e);
2160 return;
2161 }
2162 };
2163
2164 // Create Download wrapper from Artifact + event params
2165 let download =
2166 Download::from_artifact(artifact_arc, url, suggested_filename);
2167
2168 // Call the download handlers
2169 self_clone.on_download_event(download).await;
2170 });
2171 }
2172 }
2173 "dialog" => {
2174 // Dialog events are handled by BrowserContext and forwarded to Page
2175 // This case should not be reached, but keeping for completeness
2176 }
2177 "webSocket" => {
2178 if let Some(ws_guid) = params
2179 .get("webSocket")
2180 .and_then(|v| v.get("guid"))
2181 .and_then(|v| v.as_str())
2182 {
2183 let connection = self.connection();
2184 let ws_guid_owned = ws_guid.to_string();
2185 let self_clone = self.clone();
2186
2187 tokio::spawn(async move {
2188 // Wait for WebSocket object to be created
2189 let ws_arc = match connection.get_object(&ws_guid_owned).await {
2190 Ok(obj) => obj,
2191 Err(e) => {
2192 tracing::warn!("Failed to get WebSocket object: {}", e);
2193 return;
2194 }
2195 };
2196
2197 // Downcast to WebSocket
2198 let ws = if let Some(ws) = ws_arc.as_any().downcast_ref::<WebSocket>() {
2199 ws.clone()
2200 } else {
2201 tracing::warn!("Expected WebSocket object, got {}", ws_arc.type_name());
2202 return;
2203 };
2204
2205 // Call handlers
2206 let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
2207 for handler in handlers {
2208 let ws_clone = ws.clone();
2209 tokio::spawn(async move {
2210 if let Err(e) = handler(ws_clone).await {
2211 tracing::error!("Error in websocket handler: {}", e);
2212 }
2213 });
2214 }
2215 });
2216 }
2217 }
2218 "close" => {
2219 // Server-initiated close (e.g. context was closed)
2220 self.is_closed.store(true, Ordering::Relaxed);
2221 }
2222 _ => {
2223 // Other events will be handled in future phases
2224 // Events: load, domcontentloaded, crash, etc.
2225 }
2226 }
2227 }
2228
2229 fn was_collected(&self) -> bool {
2230 self.base.was_collected()
2231 }
2232
2233 fn as_any(&self) -> &dyn Any {
2234 self
2235 }
2236}
2237
2238impl std::fmt::Debug for Page {
2239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2240 f.debug_struct("Page")
2241 .field("guid", &self.guid())
2242 .field("url", &self.url())
2243 .finish()
2244 }
2245}
2246
2247/// Options for page.goto() and page.reload()
2248#[derive(Debug, Clone)]
2249pub struct GotoOptions {
2250 /// Maximum operation time in milliseconds
2251 pub timeout: Option<std::time::Duration>,
2252 /// When to consider operation succeeded
2253 pub wait_until: Option<WaitUntil>,
2254}
2255
2256impl GotoOptions {
2257 /// Creates new GotoOptions with default values
2258 pub fn new() -> Self {
2259 Self {
2260 timeout: None,
2261 wait_until: None,
2262 }
2263 }
2264
2265 /// Sets the timeout
2266 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
2267 self.timeout = Some(timeout);
2268 self
2269 }
2270
2271 /// Sets the wait_until option
2272 pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
2273 self.wait_until = Some(wait_until);
2274 self
2275 }
2276}
2277
2278impl Default for GotoOptions {
2279 fn default() -> Self {
2280 Self::new()
2281 }
2282}
2283
2284/// When to consider navigation succeeded
2285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2286pub enum WaitUntil {
2287 /// Consider operation to be finished when the `load` event is fired
2288 Load,
2289 /// Consider operation to be finished when the `DOMContentLoaded` event is fired
2290 DomContentLoaded,
2291 /// Consider operation to be finished when there are no network connections for at least 500ms
2292 NetworkIdle,
2293 /// Consider operation to be finished when the commit event is fired
2294 Commit,
2295}
2296
2297impl WaitUntil {
2298 pub(crate) fn as_str(&self) -> &'static str {
2299 match self {
2300 WaitUntil::Load => "load",
2301 WaitUntil::DomContentLoaded => "domcontentloaded",
2302 WaitUntil::NetworkIdle => "networkidle",
2303 WaitUntil::Commit => "commit",
2304 }
2305 }
2306}
2307
2308/// Options for adding a style tag to the page
2309///
2310/// See: <https://playwright.dev/docs/api/class-page#page-add-style-tag>
2311#[derive(Debug, Clone, Default)]
2312pub struct AddStyleTagOptions {
2313 /// Raw CSS content to inject
2314 pub content: Option<String>,
2315 /// URL of the `<link>` tag to add
2316 pub url: Option<String>,
2317 /// Path to a CSS file to inject
2318 pub path: Option<String>,
2319}
2320
2321impl AddStyleTagOptions {
2322 /// Creates a new builder for AddStyleTagOptions
2323 pub fn builder() -> AddStyleTagOptionsBuilder {
2324 AddStyleTagOptionsBuilder::default()
2325 }
2326
2327 /// Validates that at least one option is specified
2328 pub(crate) fn validate(&self) -> Result<()> {
2329 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
2330 return Err(Error::InvalidArgument(
2331 "At least one of content, url, or path must be specified".to_string(),
2332 ));
2333 }
2334 Ok(())
2335 }
2336}
2337
2338/// Builder for AddStyleTagOptions
2339#[derive(Debug, Clone, Default)]
2340pub struct AddStyleTagOptionsBuilder {
2341 content: Option<String>,
2342 url: Option<String>,
2343 path: Option<String>,
2344}
2345
2346impl AddStyleTagOptionsBuilder {
2347 /// Sets the CSS content to inject
2348 pub fn content(mut self, content: impl Into<String>) -> Self {
2349 self.content = Some(content.into());
2350 self
2351 }
2352
2353 /// Sets the URL of the stylesheet
2354 pub fn url(mut self, url: impl Into<String>) -> Self {
2355 self.url = Some(url.into());
2356 self
2357 }
2358
2359 /// Sets the path to a CSS file
2360 pub fn path(mut self, path: impl Into<String>) -> Self {
2361 self.path = Some(path.into());
2362 self
2363 }
2364
2365 /// Builds the AddStyleTagOptions
2366 pub fn build(self) -> AddStyleTagOptions {
2367 AddStyleTagOptions {
2368 content: self.content,
2369 url: self.url,
2370 path: self.path,
2371 }
2372 }
2373}
2374
2375// ============================================================================
2376// AddScriptTagOptions
2377// ============================================================================
2378
2379/// Options for adding a `<script>` tag to the page.
2380///
2381/// At least one of `content`, `url`, or `path` must be specified.
2382///
2383/// See: <https://playwright.dev/docs/api/class-page#page-add-script-tag>
2384#[derive(Debug, Clone, Default)]
2385pub struct AddScriptTagOptions {
2386 /// Raw JavaScript content to inject
2387 pub content: Option<String>,
2388 /// URL of the `<script>` tag to add
2389 pub url: Option<String>,
2390 /// Path to a JavaScript file to inject (file contents will be read and sent as content)
2391 pub path: Option<String>,
2392 /// Script type attribute (e.g., `"module"`)
2393 pub type_: Option<String>,
2394}
2395
2396impl AddScriptTagOptions {
2397 /// Creates a new builder for AddScriptTagOptions
2398 pub fn builder() -> AddScriptTagOptionsBuilder {
2399 AddScriptTagOptionsBuilder::default()
2400 }
2401
2402 /// Validates that at least one option is specified
2403 pub(crate) fn validate(&self) -> Result<()> {
2404 if self.content.is_none() && self.url.is_none() && self.path.is_none() {
2405 return Err(Error::InvalidArgument(
2406 "At least one of content, url, or path must be specified".to_string(),
2407 ));
2408 }
2409 Ok(())
2410 }
2411}
2412
2413/// Builder for AddScriptTagOptions
2414#[derive(Debug, Clone, Default)]
2415pub struct AddScriptTagOptionsBuilder {
2416 content: Option<String>,
2417 url: Option<String>,
2418 path: Option<String>,
2419 type_: Option<String>,
2420}
2421
2422impl AddScriptTagOptionsBuilder {
2423 /// Sets the JavaScript content to inject
2424 pub fn content(mut self, content: impl Into<String>) -> Self {
2425 self.content = Some(content.into());
2426 self
2427 }
2428
2429 /// Sets the URL of the script to load
2430 pub fn url(mut self, url: impl Into<String>) -> Self {
2431 self.url = Some(url.into());
2432 self
2433 }
2434
2435 /// Sets the path to a JavaScript file to inject
2436 pub fn path(mut self, path: impl Into<String>) -> Self {
2437 self.path = Some(path.into());
2438 self
2439 }
2440
2441 /// Sets the script type attribute (e.g., `"module"`)
2442 pub fn type_(mut self, type_: impl Into<String>) -> Self {
2443 self.type_ = Some(type_.into());
2444 self
2445 }
2446
2447 /// Builds the AddScriptTagOptions
2448 pub fn build(self) -> AddScriptTagOptions {
2449 AddScriptTagOptions {
2450 content: self.content,
2451 url: self.url,
2452 path: self.path,
2453 type_: self.type_,
2454 }
2455 }
2456}
2457
2458// ============================================================================
2459// EmulateMediaOptions and related enums
2460// ============================================================================
2461
2462/// Media type for `page.emulate_media()`.
2463///
2464/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2465#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2466#[serde(rename_all = "lowercase")]
2467pub enum Media {
2468 /// Emulate screen media type
2469 Screen,
2470 /// Emulate print media type
2471 Print,
2472 /// Reset media emulation to browser default (sends `"no-override"` to protocol)
2473 #[serde(rename = "no-override")]
2474 NoOverride,
2475}
2476
2477/// Preferred color scheme for `page.emulate_media()`.
2478///
2479/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2480#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2481pub enum ColorScheme {
2482 /// Emulate light color scheme
2483 #[serde(rename = "light")]
2484 Light,
2485 /// Emulate dark color scheme
2486 #[serde(rename = "dark")]
2487 Dark,
2488 /// Emulate no preference for color scheme
2489 #[serde(rename = "no-preference")]
2490 NoPreference,
2491 /// Reset color scheme to browser default
2492 #[serde(rename = "no-override")]
2493 NoOverride,
2494}
2495
2496/// Reduced motion preference for `page.emulate_media()`.
2497///
2498/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2499#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2500pub enum ReducedMotion {
2501 /// Emulate reduced motion preference
2502 #[serde(rename = "reduce")]
2503 Reduce,
2504 /// Emulate no preference for reduced motion
2505 #[serde(rename = "no-preference")]
2506 NoPreference,
2507 /// Reset reduced motion to browser default
2508 #[serde(rename = "no-override")]
2509 NoOverride,
2510}
2511
2512/// Forced colors preference for `page.emulate_media()`.
2513///
2514/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2515#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2516pub enum ForcedColors {
2517 /// Emulate active forced colors
2518 #[serde(rename = "active")]
2519 Active,
2520 /// Emulate no forced colors
2521 #[serde(rename = "none")]
2522 None_,
2523 /// Reset forced colors to browser default
2524 #[serde(rename = "no-override")]
2525 NoOverride,
2526}
2527
2528/// Options for `page.emulate_media()`.
2529///
2530/// All fields are optional. Fields that are `None` are omitted from the protocol
2531/// message (meaning they are not changed). To reset a field to browser default,
2532/// use the `NoOverride` variant.
2533///
2534/// See: <https://playwright.dev/docs/api/class-page#page-emulate-media>
2535#[derive(Debug, Clone, Default)]
2536pub struct EmulateMediaOptions {
2537 /// Media type to emulate (screen, print, or no-override)
2538 pub media: Option<Media>,
2539 /// Color scheme preference to emulate
2540 pub color_scheme: Option<ColorScheme>,
2541 /// Reduced motion preference to emulate
2542 pub reduced_motion: Option<ReducedMotion>,
2543 /// Forced colors preference to emulate
2544 pub forced_colors: Option<ForcedColors>,
2545}
2546
2547impl EmulateMediaOptions {
2548 /// Creates a new builder for EmulateMediaOptions
2549 pub fn builder() -> EmulateMediaOptionsBuilder {
2550 EmulateMediaOptionsBuilder::default()
2551 }
2552}
2553
2554/// Builder for EmulateMediaOptions
2555#[derive(Debug, Clone, Default)]
2556pub struct EmulateMediaOptionsBuilder {
2557 media: Option<Media>,
2558 color_scheme: Option<ColorScheme>,
2559 reduced_motion: Option<ReducedMotion>,
2560 forced_colors: Option<ForcedColors>,
2561}
2562
2563impl EmulateMediaOptionsBuilder {
2564 /// Sets the media type to emulate
2565 pub fn media(mut self, media: Media) -> Self {
2566 self.media = Some(media);
2567 self
2568 }
2569
2570 /// Sets the color scheme preference
2571 pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
2572 self.color_scheme = Some(color_scheme);
2573 self
2574 }
2575
2576 /// Sets the reduced motion preference
2577 pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
2578 self.reduced_motion = Some(reduced_motion);
2579 self
2580 }
2581
2582 /// Sets the forced colors preference
2583 pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
2584 self.forced_colors = Some(forced_colors);
2585 self
2586 }
2587
2588 /// Builds the EmulateMediaOptions
2589 pub fn build(self) -> EmulateMediaOptions {
2590 EmulateMediaOptions {
2591 media: self.media,
2592 color_scheme: self.color_scheme,
2593 reduced_motion: self.reduced_motion,
2594 forced_colors: self.forced_colors,
2595 }
2596 }
2597}
2598
2599// ============================================================================
2600// PdfOptions
2601// ============================================================================
2602
2603/// Margin options for PDF generation.
2604///
2605/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2606#[derive(Debug, Clone, Default, Serialize)]
2607pub struct PdfMargin {
2608 /// Top margin (e.g. `"1in"`)
2609 #[serde(skip_serializing_if = "Option::is_none")]
2610 pub top: Option<String>,
2611 /// Right margin
2612 #[serde(skip_serializing_if = "Option::is_none")]
2613 pub right: Option<String>,
2614 /// Bottom margin
2615 #[serde(skip_serializing_if = "Option::is_none")]
2616 pub bottom: Option<String>,
2617 /// Left margin
2618 #[serde(skip_serializing_if = "Option::is_none")]
2619 pub left: Option<String>,
2620}
2621
2622/// Options for generating a PDF from a page.
2623///
2624/// Note: PDF generation is only supported by Chromium. Calling `page.pdf()` on
2625/// Firefox or WebKit will result in an error.
2626///
2627/// See: <https://playwright.dev/docs/api/class-page#page-pdf>
2628#[derive(Debug, Clone, Default)]
2629pub struct PdfOptions {
2630 /// If specified, the PDF will also be saved to this file path.
2631 pub path: Option<std::path::PathBuf>,
2632 /// Scale of the webpage rendering, between 0.1 and 2 (default 1).
2633 pub scale: Option<f64>,
2634 /// Whether to display header and footer (default false).
2635 pub display_header_footer: Option<bool>,
2636 /// HTML template for the print header. Should be valid HTML.
2637 pub header_template: Option<String>,
2638 /// HTML template for the print footer.
2639 pub footer_template: Option<String>,
2640 /// Whether to print background graphics (default false).
2641 pub print_background: Option<bool>,
2642 /// Paper orientation — `true` for landscape (default false).
2643 pub landscape: Option<bool>,
2644 /// Paper ranges to print, e.g. `"1-5, 8"`. Defaults to empty string (all pages).
2645 pub page_ranges: Option<String>,
2646 /// Paper format, e.g. `"Letter"` or `"A4"`. Overrides `width`/`height`.
2647 pub format: Option<String>,
2648 /// Paper width in CSS units, e.g. `"8.5in"`. Overrides `format`.
2649 pub width: Option<String>,
2650 /// Paper height in CSS units, e.g. `"11in"`. Overrides `format`.
2651 pub height: Option<String>,
2652 /// Whether or not to prefer page size as defined by CSS.
2653 pub prefer_css_page_size: Option<bool>,
2654 /// Paper margins, defaulting to none.
2655 pub margin: Option<PdfMargin>,
2656}
2657
2658impl PdfOptions {
2659 /// Creates a new builder for PdfOptions
2660 pub fn builder() -> PdfOptionsBuilder {
2661 PdfOptionsBuilder::default()
2662 }
2663}
2664
2665/// Builder for PdfOptions
2666#[derive(Debug, Clone, Default)]
2667pub struct PdfOptionsBuilder {
2668 path: Option<std::path::PathBuf>,
2669 scale: Option<f64>,
2670 display_header_footer: Option<bool>,
2671 header_template: Option<String>,
2672 footer_template: Option<String>,
2673 print_background: Option<bool>,
2674 landscape: Option<bool>,
2675 page_ranges: Option<String>,
2676 format: Option<String>,
2677 width: Option<String>,
2678 height: Option<String>,
2679 prefer_css_page_size: Option<bool>,
2680 margin: Option<PdfMargin>,
2681}
2682
2683impl PdfOptionsBuilder {
2684 /// Sets the file path for saving the PDF
2685 pub fn path(mut self, path: std::path::PathBuf) -> Self {
2686 self.path = Some(path);
2687 self
2688 }
2689
2690 /// Sets the scale of the webpage rendering
2691 pub fn scale(mut self, scale: f64) -> Self {
2692 self.scale = Some(scale);
2693 self
2694 }
2695
2696 /// Sets whether to display header and footer
2697 pub fn display_header_footer(mut self, display: bool) -> Self {
2698 self.display_header_footer = Some(display);
2699 self
2700 }
2701
2702 /// Sets the HTML template for the print header
2703 pub fn header_template(mut self, template: impl Into<String>) -> Self {
2704 self.header_template = Some(template.into());
2705 self
2706 }
2707
2708 /// Sets the HTML template for the print footer
2709 pub fn footer_template(mut self, template: impl Into<String>) -> Self {
2710 self.footer_template = Some(template.into());
2711 self
2712 }
2713
2714 /// Sets whether to print background graphics
2715 pub fn print_background(mut self, print: bool) -> Self {
2716 self.print_background = Some(print);
2717 self
2718 }
2719
2720 /// Sets whether to use landscape orientation
2721 pub fn landscape(mut self, landscape: bool) -> Self {
2722 self.landscape = Some(landscape);
2723 self
2724 }
2725
2726 /// Sets the page ranges to print
2727 pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
2728 self.page_ranges = Some(ranges.into());
2729 self
2730 }
2731
2732 /// Sets the paper format (e.g., `"Letter"`, `"A4"`)
2733 pub fn format(mut self, format: impl Into<String>) -> Self {
2734 self.format = Some(format.into());
2735 self
2736 }
2737
2738 /// Sets the paper width
2739 pub fn width(mut self, width: impl Into<String>) -> Self {
2740 self.width = Some(width.into());
2741 self
2742 }
2743
2744 /// Sets the paper height
2745 pub fn height(mut self, height: impl Into<String>) -> Self {
2746 self.height = Some(height.into());
2747 self
2748 }
2749
2750 /// Sets whether to prefer page size as defined by CSS
2751 pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
2752 self.prefer_css_page_size = Some(prefer);
2753 self
2754 }
2755
2756 /// Sets the paper margins
2757 pub fn margin(mut self, margin: PdfMargin) -> Self {
2758 self.margin = Some(margin);
2759 self
2760 }
2761
2762 /// Builds the PdfOptions
2763 pub fn build(self) -> PdfOptions {
2764 PdfOptions {
2765 path: self.path,
2766 scale: self.scale,
2767 display_header_footer: self.display_header_footer,
2768 header_template: self.header_template,
2769 footer_template: self.footer_template,
2770 print_background: self.print_background,
2771 landscape: self.landscape,
2772 page_ranges: self.page_ranges,
2773 format: self.format,
2774 width: self.width,
2775 height: self.height,
2776 prefer_css_page_size: self.prefer_css_page_size,
2777 margin: self.margin,
2778 }
2779 }
2780}
2781
2782/// Response from navigation operations.
2783///
2784/// Returned from `page.goto()`, `page.reload()`, `page.go_back()`, and similar
2785/// navigation methods. Provides access to the HTTP response status, headers, and body.
2786///
2787/// See: <https://playwright.dev/docs/api/class-response>
2788#[derive(Clone)]
2789pub struct Response {
2790 /// URL of the response
2791 pub url: String,
2792 /// HTTP status code
2793 pub status: u16,
2794 /// HTTP status text
2795 pub status_text: String,
2796 /// Whether the response was successful (status 200-299)
2797 pub ok: bool,
2798 /// Response headers (from initializer, may not include all raw headers)
2799 pub headers: std::collections::HashMap<String, String>,
2800 /// Reference to the backing channel owner for RPC calls (body, rawHeaders, etc.)
2801 /// Stored as the generic trait object so it can be downcast to ResponseObject when needed.
2802 pub(crate) response_channel_owner:
2803 Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
2804}
2805
2806impl Response {
2807 /// Returns the URL of the response.
2808 ///
2809 /// See: <https://playwright.dev/docs/api/class-response#response-url>
2810 pub fn url(&self) -> &str {
2811 &self.url
2812 }
2813
2814 /// Returns the HTTP status code.
2815 ///
2816 /// See: <https://playwright.dev/docs/api/class-response#response-status>
2817 pub fn status(&self) -> u16 {
2818 self.status
2819 }
2820
2821 /// Returns the HTTP status text.
2822 ///
2823 /// See: <https://playwright.dev/docs/api/class-response#response-status-text>
2824 pub fn status_text(&self) -> &str {
2825 &self.status_text
2826 }
2827
2828 /// Returns whether the response was successful (status 200-299).
2829 ///
2830 /// See: <https://playwright.dev/docs/api/class-response#response-ok>
2831 pub fn ok(&self) -> bool {
2832 self.ok
2833 }
2834
2835 /// Returns the response headers as a HashMap.
2836 ///
2837 /// Note: these are the headers from the protocol initializer. For the full
2838 /// raw headers (including duplicates), use `headers_array()` or `all_headers()`.
2839 ///
2840 /// See: <https://playwright.dev/docs/api/class-response#response-headers>
2841 pub fn headers(&self) -> &std::collections::HashMap<String, String> {
2842 &self.headers
2843 }
2844
2845 /// Returns the response body as raw bytes.
2846 ///
2847 /// Makes an RPC call to the Playwright server to fetch the response body.
2848 ///
2849 /// # Errors
2850 ///
2851 /// Returns an error if:
2852 /// - No backing protocol object is available (edge case)
2853 /// - The RPC call to the server fails
2854 /// - The base64 response cannot be decoded
2855 ///
2856 /// See: <https://playwright.dev/docs/api/class-response#response-body>
2857 pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
2858 let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
2859 crate::error::Error::ProtocolError(
2860 "Response has no backing protocol object for body()".to_string(),
2861 )
2862 })?;
2863 let obj = arc
2864 .as_any()
2865 .downcast_ref::<crate::protocol::ResponseObject>()
2866 .ok_or_else(|| {
2867 crate::error::Error::ProtocolError(
2868 "Response backing object is not a ResponseObject".to_string(),
2869 )
2870 })?
2871 .clone();
2872 obj.body().await
2873 }
2874
2875 /// Returns the response body as a UTF-8 string.
2876 ///
2877 /// Calls `body()` then converts bytes to a UTF-8 string.
2878 ///
2879 /// # Errors
2880 ///
2881 /// Returns an error if:
2882 /// - `body()` fails
2883 /// - The body is not valid UTF-8
2884 ///
2885 /// See: <https://playwright.dev/docs/api/class-response#response-text>
2886 pub async fn text(&self) -> crate::error::Result<String> {
2887 let bytes = self.body().await?;
2888 String::from_utf8(bytes).map_err(|e| {
2889 crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
2890 })
2891 }
2892
2893 /// Parses the response body as JSON and deserializes it into type `T`.
2894 ///
2895 /// Calls `text()` then uses `serde_json` to deserialize the body.
2896 ///
2897 /// # Errors
2898 ///
2899 /// Returns an error if:
2900 /// - `text()` fails
2901 /// - The body is not valid JSON or doesn't match the expected type
2902 ///
2903 /// See: <https://playwright.dev/docs/api/class-response#response-json>
2904 pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
2905 let text = self.text().await?;
2906 serde_json::from_str(&text).map_err(|e| {
2907 crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
2908 })
2909 }
2910
2911 /// Returns all response headers as name-value pairs, preserving duplicates.
2912 ///
2913 /// Makes an RPC call for `"rawHeaders"` which returns the complete header list.
2914 ///
2915 /// # Errors
2916 ///
2917 /// Returns an error if:
2918 /// - No backing protocol object is available (edge case)
2919 /// - The RPC call to the server fails
2920 ///
2921 /// See: <https://playwright.dev/docs/api/class-response#response-headers-array>
2922 pub async fn headers_array(
2923 &self,
2924 ) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
2925 let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
2926 crate::error::Error::ProtocolError(
2927 "Response has no backing protocol object for headers_array()".to_string(),
2928 )
2929 })?;
2930 let obj = arc
2931 .as_any()
2932 .downcast_ref::<crate::protocol::ResponseObject>()
2933 .ok_or_else(|| {
2934 crate::error::Error::ProtocolError(
2935 "Response backing object is not a ResponseObject".to_string(),
2936 )
2937 })?
2938 .clone();
2939 obj.raw_headers().await
2940 }
2941
2942 /// Returns all response headers merged into a HashMap with lowercase keys.
2943 ///
2944 /// When multiple headers have the same name, their values are joined with `, `.
2945 /// This matches the behavior of `response.allHeaders()` in other Playwright bindings.
2946 ///
2947 /// # Errors
2948 ///
2949 /// Returns an error if:
2950 /// - No backing protocol object is available (edge case)
2951 /// - The RPC call to the server fails
2952 ///
2953 /// See: <https://playwright.dev/docs/api/class-response#response-all-headers>
2954 pub async fn all_headers(
2955 &self,
2956 ) -> crate::error::Result<std::collections::HashMap<String, String>> {
2957 let entries = self.headers_array().await?;
2958 let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
2959 for entry in entries {
2960 let key = entry.name.to_lowercase();
2961 map.entry(key)
2962 .and_modify(|v| {
2963 v.push_str(", ");
2964 v.push_str(&entry.value);
2965 })
2966 .or_insert(entry.value);
2967 }
2968 Ok(map)
2969 }
2970
2971 /// Returns the value for a single response header, or `None` if not present.
2972 ///
2973 /// The lookup is case-insensitive.
2974 ///
2975 /// # Errors
2976 ///
2977 /// Returns an error if:
2978 /// - No backing protocol object is available (edge case)
2979 /// - The RPC call to the server fails
2980 ///
2981 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
2982 /// Returns the value for a single response header, or `None` if not present.
2983 ///
2984 /// The lookup is case-insensitive. When multiple headers share the same name,
2985 /// their values are joined with `, ` (matching Playwright's behavior).
2986 ///
2987 /// Uses the raw headers from the server for accurate results.
2988 ///
2989 /// # Errors
2990 ///
2991 /// Returns an error if the underlying `headers_array()` RPC call fails.
2992 ///
2993 /// See: <https://playwright.dev/docs/api/class-response#response-header-value>
2994 pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
2995 let entries = self.headers_array().await?;
2996 let name_lower = name.to_lowercase();
2997 let mut values: Vec<String> = entries
2998 .into_iter()
2999 .filter(|h| h.name.to_lowercase() == name_lower)
3000 .map(|h| h.value)
3001 .collect();
3002
3003 if values.is_empty() {
3004 Ok(None)
3005 } else if values.len() == 1 {
3006 Ok(Some(values.remove(0)))
3007 } else {
3008 Ok(Some(values.join(", ")))
3009 }
3010 }
3011}
3012
3013impl std::fmt::Debug for Response {
3014 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3015 f.debug_struct("Response")
3016 .field("url", &self.url)
3017 .field("status", &self.status)
3018 .field("status_text", &self.status_text)
3019 .field("ok", &self.ok)
3020 .finish_non_exhaustive()
3021 }
3022}
3023
3024/// Shared helper: store timeout locally and notify the Playwright server.
3025/// Used by both Page and BrowserContext timeout setters.
3026pub(crate) async fn set_timeout_and_notify(
3027 channel: &crate::server::channel::Channel,
3028 method: &str,
3029 timeout: f64,
3030) {
3031 if let Err(e) = channel
3032 .send_no_result(method, serde_json::json!({ "timeout": timeout }))
3033 .await
3034 {
3035 tracing::warn!("{} send error: {}", method, e);
3036 }
3037}