playwright_rs/protocol/browser_context.rs
1// BrowserContext protocol object
2//
3// Represents an isolated browser context (session) within a browser instance.
4// Multiple contexts can exist in a single browser, each with its own cookies,
5// cache, and local storage.
6
7use crate::api::launch_options::IgnoreDefaultArgs;
8use crate::error::{Error, Result};
9use crate::protocol::api_request_context::APIRequestContext;
10use crate::protocol::cdp_session::CDPSession;
11use crate::protocol::event_waiter::EventWaiter;
12use crate::protocol::route::UnrouteBehavior;
13use crate::protocol::tracing::Tracing;
14use crate::protocol::{
15 Browser, Download, Frame, Page, ProxySettings, Request, ResponseObject, Route,
16};
17use crate::server::channel::Channel;
18use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
19use crate::server::connection::ConnectionExt;
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use std::any::Any;
23use std::collections::HashMap;
24use std::future::Future;
25use std::pin::Pin;
26use std::sync::atomic::{AtomicBool, Ordering};
27use std::sync::{Arc, Mutex};
28
29use crate::protocol::event_registry::{EventRegistry, Handler};
30use tokio::sync::oneshot;
31
32/// BrowserContext represents an isolated browser session.
33///
34/// Contexts are isolated environments within a browser instance. Each context
35/// has its own cookies, cache, and local storage, enabling independent sessions
36/// without interference.
37///
38/// # Example
39///
40/// ```no_run
41/// use playwright_rs::protocol::Playwright;
42///
43/// #[tokio::main]
44/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
45/// let playwright = Playwright::launch().await?;
46/// let browser = playwright.chromium().launch().await?;
47///
48/// // Create isolated contexts
49/// let context1 = browser.new_context().await?;
50/// let context2 = browser.new_context().await?;
51///
52/// // Create pages in each context
53/// let page1 = context1.new_page().await?;
54/// let page2 = context2.new_page().await?;
55///
56/// // Access all pages in a context
57/// let pages = context1.pages();
58/// assert_eq!(pages.len(), 1);
59///
60/// // Access the browser from a context
61/// let ctx_browser = context1.browser().unwrap();
62/// assert_eq!(ctx_browser.name(), browser.name());
63///
64/// // App mode: access initial page created automatically
65/// let chromium = playwright.chromium();
66/// let app_context = chromium
67/// .launch_persistent_context_with_options(
68/// "/tmp/app-data",
69/// playwright_rs::protocol::BrowserContextOptions::builder()
70/// .args(vec!["--app=https://example.com".to_string()])
71/// .headless(true)
72/// .build()
73/// )
74/// .await?;
75///
76/// // Get the initial page (don't create a new one!)
77/// let app_pages = app_context.pages();
78/// if !app_pages.is_empty() {
79/// let initial_page = &app_pages[0];
80/// // Use the initial page...
81/// }
82///
83/// // Cleanup
84/// context1.close().await?;
85/// context2.close().await?;
86/// app_context.close().await?;
87/// browser.close().await?;
88/// Ok(())
89/// }
90/// ```
91///
92/// See: <https://playwright.dev/docs/api/class-browsercontext>
93/// Type alias for boxed route handler future
94type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
95
96/// Type alias for boxed binding callback future
97type BindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
98
99/// Type alias for boxed service worker handler future
100type ServiceWorkerHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
101
102/// Context-level service worker event handler
103type ServiceWorkerHandler =
104 Arc<dyn Fn(crate::protocol::Worker) -> ServiceWorkerHandlerFuture + Send + Sync>;
105
106/// Context-level event handlers for the 1.60 lifecycle events. These are not
107/// wire events on the context channel; they are forwarded from each page's
108/// own events (see `wire_*` helpers), matching how the upstream clients
109/// synthesize them.
110type CtxHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
111/// Context `download` handler (receives the page's `Download`).
112type DownloadHandler = Arc<dyn Fn(Download) -> CtxHandlerFuture + Send + Sync>;
113/// Context frame handler (`frameAttached`/`frameDetached`/`frameNavigated`).
114type CtxFrameHandler = Arc<dyn Fn(Frame) -> CtxHandlerFuture + Send + Sync>;
115/// Context page-lifecycle handler (`pageLoad`/`pageClose`), receives the `Page`.
116type PageEventHandler = Arc<dyn Fn(Page) -> CtxHandlerFuture + Send + Sync>;
117
118/// Binding callback: receives deserialized JS args, returns a JSON value
119type BindingCallback = Arc<dyn Fn(Vec<serde_json::Value>) -> BindingCallbackFuture + Send + Sync>;
120
121/// Type alias for boxed WebSocketRoute handler future
122type WsRouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
123
124/// Storage for a single route handler
125#[derive(Clone)]
126struct RouteHandlerEntry {
127 pattern: String,
128 handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
129}
130
131/// Storage for a single WebSocket route handler entry
132#[derive(Clone)]
133struct ContextWsRouteHandlerEntry {
134 pattern: String,
135 handler: Arc<dyn Fn(crate::protocol::WebSocketRoute) -> WsRouteHandlerFuture + Send + Sync>,
136}
137
138#[derive(Clone)]
139pub struct BrowserContext {
140 base: ChannelOwnerImpl,
141 /// Browser instance that owns this context (None for persistent contexts)
142 browser: Option<Browser>,
143 /// All open pages in this context
144 pages: Arc<Mutex<Vec<Page>>>,
145 /// Route handlers for context-level network interception
146 route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
147 /// APIRequestContext GUID from initializer (resolved lazily)
148 request_context_guid: Option<String>,
149 /// Tracing GUID from initializer (resolved lazily)
150 tracing_guid: Option<String>,
151 /// Debugger GUID from initializer (resolved lazily)
152 debugger_guid: Option<String>,
153 /// Default action timeout for all pages in this context (milliseconds), stored as f64 bits.
154 default_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
155 /// Default navigation timeout for all pages in this context (milliseconds), stored as f64 bits.
156 default_navigation_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
157 /// `page` event: handlers and one-shot `expect_page` waiters.
158 page_events: Arc<EventRegistry<Page>>,
159 /// `close` event: one-time transition; `dispatch_all` wakes every waiter.
160 close_events: Arc<EventRegistry<()>>,
161 /// `request` event: handlers and one-shot waiters.
162 request: Arc<EventRegistry<Request>>,
163 /// `requestFinished` event handlers (no `expect_*`; waiter queue stays empty).
164 request_finished: Arc<EventRegistry<Request>>,
165 /// `requestFailed` event handlers (no `expect_*`; waiter queue stays empty).
166 request_failed: Arc<EventRegistry<Request>>,
167 /// `response` event: handlers and one-shot waiters.
168 response: Arc<EventRegistry<ResponseObject>>,
169 /// `dialog` event handlers (no `expect_*`; waiter queue stays empty).
170 dialog: Arc<EventRegistry<crate::protocol::Dialog>>,
171 /// Registered binding callbacks keyed by name (for expose_function / expose_binding)
172 binding_callbacks: Arc<Mutex<HashMap<String, BindingCallback>>>,
173 /// `console` event: handlers and one-shot `expect_console_message` waiters.
174 console: Arc<EventRegistry<crate::protocol::ConsoleMessage>>,
175 /// `pageError`-derived weberror event: handlers and one-shot waiters.
176 weberror: Arc<EventRegistry<crate::protocol::WebError>>,
177 /// Context-level service worker event handlers (fired when a service worker is registered)
178 serviceworker_handlers: Arc<Mutex<Vec<ServiceWorkerHandler>>>,
179 /// Context-level lifecycle handlers, forwarded from each page's events.
180 download_handlers: Arc<Mutex<Vec<DownloadHandler>>>,
181 frame_attached_handlers: Arc<Mutex<Vec<CtxFrameHandler>>>,
182 frame_detached_handlers: Arc<Mutex<Vec<CtxFrameHandler>>>,
183 frame_navigated_handlers: Arc<Mutex<Vec<CtxFrameHandler>>>,
184 page_load_handlers: Arc<Mutex<Vec<PageEventHandler>>>,
185 page_close_handlers: Arc<Mutex<Vec<PageEventHandler>>>,
186 /// One-shot senders waiting for the next "serviceworker" event (expect_event("serviceworker"))
187 serviceworker_waiters: Arc<Mutex<Vec<oneshot::Sender<crate::protocol::Worker>>>>,
188 /// Active service workers tracked via "serviceWorker" events
189 service_workers_list: Arc<Mutex<Vec<crate::protocol::Worker>>>,
190 /// WebSocketRoute handlers for route_web_socket()
191 ws_route_handlers: Arc<Mutex<Vec<ContextWsRouteHandlerEntry>>>,
192 /// Whether this context has been closed.
193 /// Set to true when close() is called or a "close" event is received.
194 is_closed: Arc<AtomicBool>,
195}
196
197impl BrowserContext {
198 /// Creates a new BrowserContext from protocol initialization
199 ///
200 /// This is called by the object factory when the server sends a `__create__` message
201 /// for a BrowserContext object.
202 ///
203 /// # Arguments
204 ///
205 /// * `parent` - The parent Browser object
206 /// * `type_name` - The protocol type name ("BrowserContext")
207 /// * `guid` - The unique identifier for this context
208 /// * `initializer` - The initialization data from the server
209 ///
210 /// # Errors
211 ///
212 /// Returns error if initializer is malformed
213 pub fn new(
214 parent: Arc<dyn ChannelOwner>,
215 type_name: String,
216 guid: Arc<str>,
217 initializer: Value,
218 ) -> Result<Self> {
219 // Extract APIRequestContext GUID from initializer before moving it
220 let request_context_guid = initializer
221 .get("requestContext")
222 .and_then(|v| v.get("guid"))
223 .and_then(|v| v.as_str())
224 .map(|s| s.to_string());
225
226 // Extract Tracing GUID from initializer before moving it
227 let tracing_guid = initializer
228 .get("tracing")
229 .and_then(|v| v.get("guid"))
230 .and_then(|v| v.as_str())
231 .map(|s| s.to_string());
232
233 // Extract Debugger GUID from initializer before moving it
234 let debugger_guid = initializer
235 .get("debugger")
236 .and_then(|v| v.get("guid"))
237 .and_then(|v| v.as_str())
238 .map(|s| s.to_string());
239
240 let base = ChannelOwnerImpl::new(
241 ParentOrConnection::Parent(parent.clone()),
242 type_name,
243 guid,
244 initializer,
245 );
246
247 // Store browser reference if parent is a Browser
248 // Returns None only for special contexts (Android, Electron) where parent is not a Browser
249 // For both regular contexts and persistent contexts, parent is a Browser instance
250 let browser = parent.as_any().downcast_ref::<Browser>().cloned();
251
252 let context = Self {
253 base,
254 browser,
255 pages: Arc::new(Mutex::new(Vec::new())),
256 route_handlers: Arc::new(Mutex::new(Vec::new())),
257 request_context_guid,
258 tracing_guid,
259 debugger_guid,
260 default_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
261 crate::DEFAULT_TIMEOUT_MS.to_bits(),
262 )),
263 default_navigation_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
264 crate::DEFAULT_TIMEOUT_MS.to_bits(),
265 )),
266 page_events: EventRegistry::new("page"),
267 close_events: EventRegistry::new("close"),
268 request: EventRegistry::new("request"),
269 request_finished: EventRegistry::new("requestFinished"),
270 request_failed: EventRegistry::new("requestFailed"),
271 response: EventRegistry::new("response"),
272 dialog: EventRegistry::new("dialog"),
273 binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
274 console: EventRegistry::new("console"),
275 weberror: EventRegistry::new("weberror"),
276 serviceworker_handlers: Arc::new(Mutex::new(Vec::new())),
277 download_handlers: Arc::new(Mutex::new(Vec::new())),
278 frame_attached_handlers: Arc::new(Mutex::new(Vec::new())),
279 frame_detached_handlers: Arc::new(Mutex::new(Vec::new())),
280 frame_navigated_handlers: Arc::new(Mutex::new(Vec::new())),
281 page_load_handlers: Arc::new(Mutex::new(Vec::new())),
282 page_close_handlers: Arc::new(Mutex::new(Vec::new())),
283 serviceworker_waiters: Arc::new(Mutex::new(Vec::new())),
284 service_workers_list: Arc::new(Mutex::new(Vec::new())),
285 ws_route_handlers: Arc::new(Mutex::new(Vec::new())),
286 is_closed: Arc::new(AtomicBool::new(false)),
287 };
288
289 // Enable dialog and console event subscriptions eagerly.
290 // Console events must be subscribed to receive them without a registered handler,
291 // enabling the console_messages() and page_errors() passive accumulators on Page.
292 let channel = context.channel().clone();
293 tokio::spawn(async move {
294 _ = channel.update_subscription("dialog", true).await;
295 _ = channel.update_subscription("console", true).await;
296 });
297
298 // Note: Selectors registration is done by the caller (e.g. Browser::new_context())
299 // after this object is returned, so that add_context() can be awaited properly.
300
301 Ok(context)
302 }
303
304 /// Returns the channel for sending protocol messages
305 ///
306 /// Used internally for sending RPC calls to the context.
307 fn channel(&self) -> &Channel {
308 self.base.channel()
309 }
310
311 /// Adds a script which would be evaluated in one of the following scenarios:
312 ///
313 /// - Whenever a page is created in the browser context or is navigated.
314 /// - Whenever a child frame is attached or navigated in any page in the browser context.
315 ///
316 /// The script is evaluated after the document was created but before any of its scripts
317 /// were run. This is useful to amend the JavaScript environment, e.g. to seed Math.random.
318 ///
319 /// # Arguments
320 ///
321 /// * `script` - Script to be evaluated in all pages in the browser context.
322 ///
323 /// # Errors
324 ///
325 /// Returns error if:
326 /// - Context has been closed
327 /// - Communication with browser process fails
328 ///
329 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script>
330 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
331 pub async fn add_init_script(&self, script: &str) -> Result<()> {
332 self.channel()
333 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
334 .await
335 }
336
337 /// Creates a new page in this browser context.
338 ///
339 /// Pages are isolated tabs/windows within a context. Each page starts
340 /// at "about:blank" and can be navigated independently.
341 ///
342 /// # Errors
343 ///
344 /// Returns error if:
345 /// - Context has been closed
346 /// - Communication with browser process fails
347 ///
348 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
349 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
350 pub async fn new_page(&self) -> Result<Page> {
351 // Response contains the GUID of the created Page
352 #[derive(Deserialize)]
353 struct NewPageResponse {
354 page: GuidRef,
355 }
356
357 #[derive(Deserialize)]
358 struct GuidRef {
359 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
360 guid: Arc<str>,
361 }
362
363 // Send newPage RPC to server
364 let response: NewPageResponse = self
365 .channel()
366 .send("newPage", serde_json::json!({}))
367 .await?;
368
369 // Retrieve and downcast the Page object from the connection registry
370 let page: Page = self
371 .connection()
372 .get_typed::<Page>(&response.page.guid)
373 .await?;
374
375 // Note: Don't track the page here - it will be tracked via the "page" event
376 // that Playwright server sends automatically when a page is created.
377 // Tracking it here would create duplicates.
378
379 // Propagate context-level timeout defaults to the new page
380 let ctx_timeout = self.default_timeout_ms();
381 let ctx_nav_timeout = self.default_navigation_timeout_ms();
382 if ctx_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
383 page.set_default_timeout(ctx_timeout).await;
384 }
385 if ctx_nav_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
386 page.set_default_navigation_timeout(ctx_nav_timeout).await;
387 }
388
389 Ok(page)
390 }
391
392 /// Returns all open pages in the context.
393 ///
394 /// This method provides a snapshot of all currently active pages that belong
395 /// to this browser context instance. Pages created via `new_page()` and popup
396 /// pages opened through user interactions are included.
397 ///
398 /// In persistent contexts launched with `--app=url`, this will include the
399 /// initial page created automatically by Playwright.
400 ///
401 /// # Errors
402 ///
403 /// This method does not return errors. It provides a snapshot of pages at
404 /// the time of invocation.
405 ///
406 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-pages>
407 pub fn pages(&self) -> Vec<Page> {
408 self.pages.lock().unwrap().clone()
409 }
410
411 /// Returns all active service workers registered in this browser context.
412 ///
413 /// Service workers are accumulated as they are registered (`serviceWorker` event).
414 /// Each call returns a snapshot of the current list.
415 ///
416 /// Note: Testing service workers typically requires HTTPS. In plain HTTP or
417 /// `about:blank` contexts this list is empty.
418 ///
419 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-service-workers>
420 pub fn service_workers(&self) -> Vec<crate::protocol::Worker> {
421 self.service_workers_list.lock().unwrap().clone()
422 }
423
424 /// Returns the browser instance that owns this context.
425 ///
426 /// Returns `None` only for contexts created outside of normal browser
427 /// (e.g., Android or Electron contexts). For both regular contexts and
428 /// persistent contexts, this returns the owning Browser instance.
429 ///
430 /// # Errors
431 ///
432 /// This method does not return errors.
433 ///
434 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-browser>
435 pub fn browser(&self) -> Option<Browser> {
436 self.browser.clone()
437 }
438
439 /// Returns the APIRequestContext associated with this context.
440 ///
441 /// The APIRequestContext is created automatically by the server for each
442 /// BrowserContext. It enables performing HTTP requests and is used internally
443 /// by `Route::fetch()`.
444 ///
445 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-request>
446 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
447 pub async fn request(&self) -> Result<APIRequestContext> {
448 let guid = self.request_context_guid.as_ref().ok_or_else(|| {
449 crate::error::Error::ProtocolError(
450 "No APIRequestContext available for this context".to_string(),
451 )
452 })?;
453
454 self.connection().get_typed::<APIRequestContext>(guid).await
455 }
456
457 /// Creates a new Chrome DevTools Protocol session for the given page.
458 ///
459 /// CDPSession provides low-level access to the Chrome DevTools Protocol.
460 /// This method is only available in Chromium-based browsers.
461 ///
462 /// # Arguments
463 ///
464 /// * `page` - The page to create a CDP session for
465 ///
466 /// # Errors
467 ///
468 /// Returns error if:
469 /// - The browser is not Chromium-based
470 /// - Context has been closed
471 /// - Communication with browser process fails
472 ///
473 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-cdp-session>
474 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), page_guid = %page.guid()))]
475 pub async fn new_cdp_session(&self, page: &Page) -> Result<CDPSession> {
476 #[derive(serde::Deserialize)]
477 struct NewCDPSessionResponse {
478 session: GuidRef,
479 }
480
481 #[derive(serde::Deserialize)]
482 struct GuidRef {
483 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
484 guid: Arc<str>,
485 }
486
487 let response: NewCDPSessionResponse = self
488 .channel()
489 .send(
490 "newCDPSession",
491 serde_json::json!({ "page": { "guid": page.guid() } }),
492 )
493 .await?;
494
495 self.connection()
496 .get_typed::<CDPSession>(&response.session.guid)
497 .await
498 }
499
500 /// Returns the Tracing object for this browser context.
501 ///
502 /// The Tracing object is created automatically by the Playwright server for each
503 /// BrowserContext. Use it to start and stop trace recording.
504 ///
505 /// # Errors
506 ///
507 /// Returns error if no Tracing object is available for this context (rare,
508 /// should not happen in normal usage).
509 ///
510 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-tracing>
511 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
512 pub async fn tracing(&self) -> Result<Tracing> {
513 let guid = self.tracing_guid.as_ref().ok_or_else(|| {
514 crate::error::Error::ProtocolError(
515 "No Tracing object available for this context".to_string(),
516 )
517 })?;
518
519 self.connection().get_typed::<Tracing>(guid).await
520 }
521
522 /// Returns the [`Debugger`](crate::protocol::Debugger) for this context.
523 ///
524 /// The Debugger surfaces programmatic control of Playwright Inspector's
525 /// "PAUSED" overlay — `request_pause`, `resume`, `next`, `run_to`, and a
526 /// `pausedStateChanged` event. Used by IDE integrations and
527 /// inspector-style tools.
528 ///
529 /// See: <https://playwright.dev/docs/api/class-debugger>
530 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
531 pub async fn debugger(&self) -> Result<crate::protocol::Debugger> {
532 let guid = self.debugger_guid.as_ref().ok_or_else(|| {
533 crate::error::Error::ProtocolError(
534 "No Debugger object available for this context".to_string(),
535 )
536 })?;
537 self.connection()
538 .get_typed::<crate::protocol::Debugger>(guid)
539 .await
540 }
541
542 /// Returns the Clock object for this browser context.
543 ///
544 /// The Clock object enables fake timer control — install fake timers,
545 /// fast-forward time, pause/resume, and set fixed or system time.
546 ///
547 /// `page.clock()` delegates to this method via the page's parent context.
548 ///
549 /// See: <https://playwright.dev/docs/api/class-clock>
550 pub fn clock(&self) -> crate::protocol::clock::Clock {
551 crate::protocol::clock::Clock::new(self.channel().clone())
552 }
553
554 /// Manage the context's virtual WebAuthn authenticator:
555 /// install it, then register / list / delete passkeys for
556 /// `navigator.credentials` testing.
557 ///
558 /// See: <https://playwright.dev/docs/api/class-credentials>
559 pub fn credentials(&self) -> crate::protocol::Credentials {
560 crate::protocol::Credentials::new(self.channel().clone())
561 }
562
563 /// Closes the browser context and all its pages.
564 ///
565 /// This is a graceful operation that sends a close command to the context
566 /// and waits for it to shut down properly.
567 ///
568 /// # Errors
569 ///
570 /// Returns error if:
571 /// - Context has already been closed
572 /// - Communication with browser process fails
573 ///
574 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
575 #[tracing::instrument(level = "info", skip_all, fields(guid = %self.guid()))]
576 pub async fn close(&self) -> Result<()> {
577 // Unregister from Selectors coordinator so closed channels are not sent future messages.
578 let selectors = self.connection().selectors();
579 selectors.remove_context(self.channel());
580
581 // Send close RPC to server
582 let result = self
583 .channel()
584 .send_no_result("close", serde_json::json!({}))
585 .await;
586 // Mark as closed regardless of error (best-effort)
587 self.is_closed.store(true, Ordering::Relaxed);
588 result
589 }
590
591 /// Sets the default timeout for all operations in this browser context.
592 ///
593 /// This applies to all pages already open in this context as well as pages
594 /// created subsequently. Pass `0` to disable timeouts.
595 ///
596 /// # Arguments
597 ///
598 /// * `timeout` - Timeout in milliseconds
599 ///
600 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout>
601 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
602 pub async fn set_default_timeout(&self, timeout: f64) {
603 self.default_timeout_ms
604 .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
605 let pages: Vec<Page> = self.pages.lock().unwrap().clone();
606 for page in pages {
607 page.set_default_timeout(timeout).await;
608 }
609 crate::protocol::page::set_timeout_and_notify(
610 self.channel(),
611 "setDefaultTimeoutNoReply",
612 timeout,
613 )
614 .await;
615 }
616
617 /// Sets the default timeout for navigation operations in this browser context.
618 ///
619 /// This applies to all pages already open in this context as well as pages
620 /// created subsequently. Pass `0` to disable timeouts.
621 ///
622 /// # Arguments
623 ///
624 /// * `timeout` - Timeout in milliseconds
625 ///
626 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout>
627 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
628 pub async fn set_default_navigation_timeout(&self, timeout: f64) {
629 self.default_navigation_timeout_ms
630 .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
631 let pages: Vec<Page> = self.pages.lock().unwrap().clone();
632 for page in pages {
633 page.set_default_navigation_timeout(timeout).await;
634 }
635 crate::protocol::page::set_timeout_and_notify(
636 self.channel(),
637 "setDefaultNavigationTimeoutNoReply",
638 timeout,
639 )
640 .await;
641 }
642
643 /// Returns the context's current default action timeout in milliseconds.
644 fn default_timeout_ms(&self) -> f64 {
645 f64::from_bits(
646 self.default_timeout_ms
647 .load(std::sync::atomic::Ordering::Relaxed),
648 )
649 }
650
651 /// Returns the context's current default navigation timeout in milliseconds.
652 fn default_navigation_timeout_ms(&self) -> f64 {
653 f64::from_bits(
654 self.default_navigation_timeout_ms
655 .load(std::sync::atomic::Ordering::Relaxed),
656 )
657 }
658
659 /// Pauses the browser context.
660 ///
661 /// This pauses the execution of all pages in the context.
662 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
663 pub async fn pause(&self) -> Result<()> {
664 self.channel()
665 .send_no_result("pause", serde_json::Value::Null)
666 .await
667 }
668
669 /// Returns storage state for this browser context.
670 ///
671 /// Contains current cookies and local storage snapshots.
672 ///
673 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
674 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
675 pub async fn storage_state(
676 &self,
677 options: impl Into<Option<StorageStateOptions>>,
678 ) -> Result<StorageState> {
679 let params = serde_json::to_value(options.into().unwrap_or_default())
680 .map_err(|e| Error::ProtocolError(format!("Failed to serialize options: {e}")))?;
681 let response: StorageState = self.channel().send("storageState", params).await?;
682 Ok(response)
683 }
684
685 /// Replaces this context's storage state in-place via the driver's
686 /// `setStorageState`, matching `browserContext.setStorageState()` in the
687 /// JS/Python APIs. Useful for restoring authentication state without
688 /// recreating the context.
689 ///
690 /// This is a **replace**, not a merge, and the driver is thorough about
691 /// it. Beyond installing the cookies, origins and passkeys carried by
692 /// `state`, it also:
693 ///
694 /// - clears the HTTP cache;
695 /// - clears storage (localStorage, sessionStorage, IndexedDB, service
696 /// workers) for **every origin the context has visited**, not only the
697 /// origins listed in `state`;
698 /// - when `state` carries no `credentials`, disposes an installed
699 /// virtual authenticator along with its passkeys. Capture the state
700 /// with [`StorageStateOptions::credentials`] if the context being
701 /// restored into should keep WebAuthn working.
702 ///
703 /// No client-visible page is opened: the driver uses an internal page,
704 /// navigated to each origin, to apply origin-scoped state.
705 ///
706 /// # Errors
707 ///
708 /// Returns an error if the state fails to serialize or the driver
709 /// rejects it, or if the context has closed.
710 ///
711 /// # Example
712 ///
713 /// ```no_run
714 /// # use playwright_rs::Playwright;
715 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
716 /// # let pw = Playwright::launch().await?;
717 /// # let browser = pw.chromium().launch().await?;
718 /// # let context = browser.new_context().await?;
719 /// use playwright_rs::protocol::{Cookie, StorageState};
720 ///
721 /// // Restore session cookie
722 /// let state = StorageState::default().cookies(vec![
723 /// Cookie::new("session", "token123")
724 /// .domain("example.com")
725 /// .path("/")
726 /// .http_only(true)
727 /// .secure(true)
728 /// .same_site("Lax"),
729 /// ]);
730 /// context.set_storage_state(state).await?;
731 /// # Ok(())
732 /// # }
733 /// ```
734 ///
735 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-storage-state>
736 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
737 pub async fn set_storage_state(&self, state: StorageState) -> Result<()> {
738 // Delegates to the driver rather than reconstructing the state
739 // client-side. The previous implementation cleared cookies, re-added
740 // them, then opened a throwaway page per origin to replay
741 // localStorage through `evaluate`. That could only ever restore what
742 // it knew how to replay, so WebAuthn passkeys and
743 // IndexedDB were silently dropped, and every origin cost a page
744 // navigation.
745 let storage_state = serde_json::to_value(&state)
746 .map_err(|e| Error::ProtocolError(format!("Failed to serialize storage state: {e}")))?;
747
748 self.channel()
749 .send_no_result(
750 "setStorageState",
751 serde_json::json!({ "storageState": storage_state }),
752 )
753 .await
754 }
755
756 /// Returns whether this browser context has been closed.
757 ///
758 /// Returns `true` after [`close()`](Self::close) has been called on this context, or after the
759 /// context receives a close event from the server (e.g. when the browser is closed).
760 ///
761 /// Note: this reflects eventual state. If the context was closed by a server-initiated
762 /// event, `is_closed()` becomes `true` only after the "close" event has been received
763 /// and processed.
764 ///
765 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-is-closed>
766 pub fn is_closed(&self) -> bool {
767 self.is_closed.load(Ordering::Relaxed)
768 }
769
770 /// Adds cookies into this browser context.
771 ///
772 /// All pages within this context will have these cookies installed. Cookies can be granularly specified
773 /// with `name`, `value`, `url`, `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`.
774 ///
775 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies>
776 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = cookies.len()))]
777 pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
778 self.channel()
779 .send_no_result(
780 "addCookies",
781 serde_json::json!({
782 "cookies": cookies
783 }),
784 )
785 .await
786 }
787
788 /// Returns cookies for this browser context, optionally filtered by URLs.
789 ///
790 /// If `urls` is `None` or empty, all cookies are returned.
791 ///
792 /// # Arguments
793 ///
794 /// * `urls` - Optional list of URLs to filter cookies by
795 ///
796 /// # Errors
797 ///
798 /// Returns error if:
799 /// - Context has been closed
800 /// - Communication with browser process fails
801 ///
802 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-cookies>
803 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = tracing::field::Empty))]
804 pub async fn cookies(&self, urls: Option<&[&str]>) -> Result<Vec<Cookie>> {
805 let url_list: Vec<&str> = urls.unwrap_or(&[]).to_vec();
806 #[derive(serde::Deserialize)]
807 struct CookiesResponse {
808 cookies: Vec<Cookie>,
809 }
810 let response: CookiesResponse = self
811 .channel()
812 .send("cookies", serde_json::json!({ "urls": url_list }))
813 .await?;
814 Ok(response.cookies)
815 }
816
817 /// Clears cookies from this browser context, with optional filters.
818 ///
819 /// When called with no options, all cookies are removed. Use `ClearCookiesOptions`
820 /// to filter which cookies to clear by name, domain, or path.
821 ///
822 /// # Arguments
823 ///
824 /// * `options` - Optional filters for which cookies to clear
825 ///
826 /// # Errors
827 ///
828 /// Returns error if:
829 /// - Context has been closed
830 /// - Communication with browser process fails
831 ///
832 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
833 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
834 pub async fn clear_cookies(
835 &self,
836 options: impl Into<Option<ClearCookiesOptions>>,
837 ) -> Result<()> {
838 let options = options.into();
839 let params = match options {
840 None => serde_json::json!({}),
841 Some(opts) => serde_json::to_value(opts).unwrap_or(serde_json::json!({})),
842 };
843 self.channel().send_no_result("clearCookies", params).await
844 }
845
846 /// Sets extra HTTP headers that will be sent with every request from this context.
847 ///
848 /// These headers are merged with per-page extra headers set with `page.set_extra_http_headers()`.
849 /// If the page has specific headers that conflict, page-level headers take precedence.
850 ///
851 /// # Arguments
852 ///
853 /// * `headers` - Map of header names to values. All header names are lowercased.
854 ///
855 /// # Errors
856 ///
857 /// Returns error if:
858 /// - Context has been closed
859 /// - Communication with browser process fails
860 ///
861 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-extra-http-headers>
862 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = headers.len()))]
863 pub async fn set_extra_http_headers(&self, headers: HashMap<String, String>) -> Result<()> {
864 // Playwright protocol expects an array of {name, value} objects
865 let headers_array: Vec<serde_json::Value> = headers
866 .into_iter()
867 .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
868 .collect();
869 self.channel()
870 .send_no_result(
871 "setExtraHTTPHeaders",
872 serde_json::json!({ "headers": headers_array }),
873 )
874 .await
875 }
876
877 /// Grants browser permissions to the context.
878 ///
879 /// Permissions are granted for all pages in the context. The optional `origin`
880 /// in `GrantPermissionsOptions` restricts the grant to a specific URL origin.
881 ///
882 /// Common permissions: `"geolocation"`, `"notifications"`, `"camera"`,
883 /// `"microphone"`, `"clipboard-read"`, `"clipboard-write"`.
884 ///
885 /// # Arguments
886 ///
887 /// * `permissions` - List of permission strings to grant
888 /// * `options` - Optional options, including `origin` to restrict the grant
889 ///
890 /// # Errors
891 ///
892 /// Returns error if:
893 /// - Permission name is not recognised
894 /// - Context has been closed
895 /// - Communication with browser process fails
896 ///
897 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
898 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
899 pub async fn grant_permissions(
900 &self,
901 permissions: &[&str],
902 options: impl Into<Option<GrantPermissionsOptions>>,
903 ) -> Result<()> {
904 let options = options.into();
905 let mut params = serde_json::json!({ "permissions": permissions });
906 if let Some(opts) = options
907 && let Some(origin) = opts.origin
908 {
909 params["origin"] = serde_json::Value::String(origin);
910 }
911 self.channel()
912 .send_no_result("grantPermissions", params)
913 .await
914 }
915
916 /// Clears all permission overrides for this browser context.
917 ///
918 /// Reverts all permissions previously set with `grant_permissions()` back to
919 /// the browser default state.
920 ///
921 /// # Errors
922 ///
923 /// Returns error if:
924 /// - Context has been closed
925 /// - Communication with browser process fails
926 ///
927 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-permissions>
928 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
929 pub async fn clear_permissions(&self) -> Result<()> {
930 self.channel()
931 .send_no_result("clearPermissions", serde_json::json!({}))
932 .await
933 }
934
935 /// Sets or clears the geolocation for all pages in this context.
936 ///
937 /// Pass `Some(Geolocation { ... })` to set a specific location, or `None` to
938 /// clear the override and let the browser handle location requests naturally.
939 ///
940 /// Note: Geolocation access requires the `"geolocation"` permission to be granted
941 /// via `grant_permissions()` for navigator.geolocation to succeed.
942 ///
943 /// # Arguments
944 ///
945 /// * `geolocation` - Location to set, or `None` to clear
946 ///
947 /// # Errors
948 ///
949 /// Returns error if:
950 /// - Latitude or longitude is out of range
951 /// - Context has been closed
952 /// - Communication with browser process fails
953 ///
954 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-geolocation>
955 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
956 pub async fn set_geolocation(&self, geolocation: Option<Geolocation>) -> Result<()> {
957 // Playwright protocol: omit the "geolocation" key entirely to clear;
958 // passing null causes a validation error on the server side.
959 let params = match geolocation {
960 Some(geo) => serde_json::json!({ "geolocation": geo }),
961 None => serde_json::json!({}),
962 };
963 self.channel()
964 .send_no_result("setGeolocation", params)
965 .await
966 }
967
968 /// Toggles the offline mode for this browser context.
969 ///
970 /// When `true`, all network requests from pages in this context will fail with
971 /// a network error. Set to `false` to restore network connectivity.
972 ///
973 /// # Arguments
974 ///
975 /// * `offline` - `true` to go offline, `false` to go back online
976 ///
977 /// # Errors
978 ///
979 /// Returns error if:
980 /// - Context has been closed
981 /// - Communication with browser process fails
982 ///
983 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-offline>
984 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), offline))]
985 pub async fn set_offline(&self, offline: bool) -> Result<()> {
986 self.channel()
987 .send_no_result("setOffline", serde_json::json!({ "offline": offline }))
988 .await
989 }
990
991 /// Registers a route handler for context-level network interception.
992 ///
993 /// Routes registered on a context apply to all pages within the context.
994 /// Page-level routes take precedence over context-level routes.
995 ///
996 /// # Arguments
997 ///
998 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
999 /// * `handler` - Async closure that handles the route
1000 ///
1001 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route>
1002 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
1003 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
1004 where
1005 F: Fn(Route) -> Fut + Send + Sync + 'static,
1006 Fut: Future<Output = Result<()>> + Send + 'static,
1007 {
1008 let handler =
1009 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
1010
1011 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
1012 pattern: pattern.to_string(),
1013 handler,
1014 });
1015
1016 self.enable_network_interception().await
1017 }
1018
1019 /// Removes route handler(s) matching the given URL pattern.
1020 ///
1021 /// # Arguments
1022 ///
1023 /// * `pattern` - URL pattern to remove handlers for
1024 ///
1025 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute>
1026 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
1027 pub async fn unroute(&self, pattern: &str) -> Result<()> {
1028 self.route_handlers
1029 .lock()
1030 .unwrap()
1031 .retain(|entry| entry.pattern != pattern);
1032 self.enable_network_interception().await
1033 }
1034
1035 /// Removes all registered route handlers.
1036 ///
1037 /// # Arguments
1038 ///
1039 /// * `behavior` - Optional behavior for in-flight handlers
1040 ///
1041 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute-all>
1042 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1043 pub async fn unroute_all(&self, _behavior: Option<UnrouteBehavior>) -> Result<()> {
1044 self.route_handlers.lock().unwrap().clear();
1045 self.enable_network_interception().await
1046 }
1047
1048 /// Replays network requests from a HAR file recorded previously.
1049 ///
1050 /// Requests matching `options.url` (or all requests if omitted) will be
1051 /// served from the archive for every page in this context. Unmatched
1052 /// requests are either aborted or passed through depending on
1053 /// `options.not_found` (`"abort"` is the default).
1054 ///
1055 /// # Arguments
1056 ///
1057 /// * `har_path` - Path to the `.har` file on disk
1058 /// * `options` - Optional settings (url filter, not_found policy, update mode)
1059 ///
1060 /// # Errors
1061 ///
1062 /// Returns error if:
1063 /// - `har_path` does not exist or cannot be read by the Playwright server
1064 /// - The Playwright server fails to open the archive
1065 ///
1066 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har>
1067 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1068 pub async fn route_from_har(
1069 &self,
1070 har_path: &str,
1071 options: impl Into<Option<crate::protocol::RouteFromHarOptions>>,
1072 ) -> Result<()> {
1073 let options = options.into();
1074 let opts = options.unwrap_or_default();
1075 let not_found = opts.not_found.unwrap_or_else(|| "abort".to_string());
1076 let url_filter = opts.url.clone();
1077
1078 let abs_path = std::path::Path::new(har_path).canonicalize().map_err(|e| {
1079 Error::InvalidPath(format!(
1080 "route_from_har: cannot resolve '{}': {}",
1081 har_path, e
1082 ))
1083 })?;
1084 let abs_str = abs_path.to_string_lossy().into_owned();
1085
1086 let connection = self.connection();
1087 let local_utils = {
1088 let all = connection.all_objects_sync();
1089 all.into_iter()
1090 .find(|o| o.type_name() == "LocalUtils")
1091 .and_then(|o| {
1092 o.as_any()
1093 .downcast_ref::<crate::protocol::LocalUtils>()
1094 .cloned()
1095 })
1096 .ok_or_else(|| {
1097 Error::ProtocolError(
1098 "route_from_har: LocalUtils not found in connection registry".to_string(),
1099 )
1100 })?
1101 };
1102
1103 let har_id = local_utils.har_open(&abs_str).await?;
1104
1105 let pattern = url_filter.unwrap_or_else(|| "**/*".to_string());
1106
1107 let har_id_clone = har_id.clone();
1108 let local_utils_clone = local_utils.clone();
1109 let not_found_clone = not_found.clone();
1110
1111 self.route(&pattern, move |route| {
1112 let har_id = har_id_clone.clone();
1113 let local_utils = local_utils_clone.clone();
1114 let not_found = not_found_clone.clone();
1115 async move {
1116 let request = route.request();
1117 let req_url = request.url().to_string();
1118 let req_method = request.method().to_string();
1119
1120 let headers: Vec<serde_json::Value> = request
1121 .headers()
1122 .iter()
1123 .map(|(k, v)| serde_json::json!({"name": k, "value": v}))
1124 .collect();
1125
1126 let lookup = local_utils
1127 .har_lookup(
1128 &har_id,
1129 &req_url,
1130 &req_method,
1131 headers,
1132 None,
1133 request.is_navigation_request(),
1134 )
1135 .await;
1136
1137 match lookup {
1138 Err(e) => {
1139 tracing::warn!("har_lookup error for {}: {}", req_url, e);
1140 route.continue_(None).await
1141 }
1142 Ok(result) => match result.action.as_str() {
1143 "redirect" => {
1144 let redirect_url = result.redirect_url.unwrap_or_default();
1145 let opts = crate::protocol::ContinueOptions::builder()
1146 .url(redirect_url)
1147 .build();
1148 route.continue_(Some(opts)).await
1149 }
1150 "fulfill" => {
1151 let status = result.status.unwrap_or(200);
1152
1153 let body_bytes = result.body.as_deref().map(|b64| {
1154 use base64::Engine;
1155 base64::engine::general_purpose::STANDARD
1156 .decode(b64)
1157 .unwrap_or_default()
1158 });
1159
1160 let mut headers_map = std::collections::HashMap::new();
1161 if let Some(raw_headers) = result.headers {
1162 for h in raw_headers {
1163 if let (Some(name), Some(value)) = (
1164 h.get("name").and_then(|v| v.as_str()),
1165 h.get("value").and_then(|v| v.as_str()),
1166 ) {
1167 headers_map.insert(name.to_string(), value.to_string());
1168 }
1169 }
1170 }
1171
1172 let mut builder =
1173 crate::protocol::FulfillOptions::builder().status(status);
1174
1175 if !headers_map.is_empty() {
1176 builder = builder.headers(headers_map);
1177 }
1178
1179 if let Some(body) = body_bytes {
1180 builder = builder.body(body);
1181 }
1182
1183 route.fulfill(Some(builder.build())).await
1184 }
1185 _ => {
1186 if not_found == "fallback" {
1187 route.fallback(None).await
1188 } else {
1189 route.abort(None).await
1190 }
1191 }
1192 },
1193 }
1194 }
1195 })
1196 .await
1197 }
1198
1199 /// Adds a listener for the `page` event.
1200 ///
1201 /// The handler is called whenever a new page is created in this context,
1202 /// including popup pages opened through user interactions.
1203 ///
1204 /// # Arguments
1205 ///
1206 /// * `handler` - Async function that receives the new `Page`
1207 ///
1208 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page>
1209 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1210 /// Subscribe to `reg`'s event if nothing is listening yet.
1211 ///
1212 /// Same contract as `Page::subscribe_if_idle`: the server only pushes an
1213 /// event once asked, so the first handler or `expect_*` turns it on, and
1214 /// the name comes from the registry rather than being restated per site.
1215 async fn subscribe_if_idle<T>(&self, reg: &EventRegistry<T>) {
1216 if reg.is_idle() {
1217 _ = self.channel().update_subscription(reg.name(), true).await;
1218 }
1219 }
1220
1221 pub async fn on_page<F, Fut>(&self, handler: F) -> Result<()>
1222 where
1223 F: Fn(Page) -> Fut + Send + Sync + 'static,
1224 Fut: Future<Output = Result<()>> + Send + 'static,
1225 {
1226 let handler: Handler<Page> = Arc::new(move |page| Box::pin(handler(page)));
1227 self.page_events.add_handler(handler);
1228 Ok(())
1229 }
1230
1231 /// Adds a listener for the `download` event: fired when any page in the
1232 /// context starts a download. Forwarded from each page's own `download`
1233 /// event.
1234 ///
1235 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-download>
1236 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1237 pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
1238 where
1239 F: Fn(Download) -> Fut + Send + Sync + 'static,
1240 Fut: Future<Output = Result<()>> + Send + 'static,
1241 {
1242 let handler = Arc::new(move |d: Download| -> CtxHandlerFuture { Box::pin(handler(d)) });
1243 let was_empty = self.download_handlers.lock().unwrap().is_empty();
1244 self.download_handlers.lock().unwrap().push(handler);
1245 if was_empty {
1246 for page in self.pages() {
1247 Self::wire_download(&page, self.download_handlers.clone()).await;
1248 }
1249 }
1250 Ok(())
1251 }
1252
1253 /// Adds a listener for the `frameAttached` event: fired when a frame is
1254 /// attached in any page of the context. Forwarded from each page.
1255 ///
1256 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-attached>
1257 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1258 pub async fn on_frame_attached<F, Fut>(&self, handler: F) -> Result<()>
1259 where
1260 F: Fn(Frame) -> Fut + Send + Sync + 'static,
1261 Fut: Future<Output = Result<()>> + Send + 'static,
1262 {
1263 let handler = Arc::new(move |f: Frame| -> CtxHandlerFuture { Box::pin(handler(f)) });
1264 let was_empty = self.frame_attached_handlers.lock().unwrap().is_empty();
1265 self.frame_attached_handlers.lock().unwrap().push(handler);
1266 if was_empty {
1267 for page in self.pages() {
1268 Self::wire_frame_attached(&page, self.frame_attached_handlers.clone()).await;
1269 }
1270 }
1271 Ok(())
1272 }
1273
1274 /// Adds a listener for the `frameDetached` event: fired when a frame is
1275 /// detached in any page of the context. Forwarded from each page.
1276 ///
1277 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-detached>
1278 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1279 pub async fn on_frame_detached<F, Fut>(&self, handler: F) -> Result<()>
1280 where
1281 F: Fn(Frame) -> Fut + Send + Sync + 'static,
1282 Fut: Future<Output = Result<()>> + Send + 'static,
1283 {
1284 let handler = Arc::new(move |f: Frame| -> CtxHandlerFuture { Box::pin(handler(f)) });
1285 let was_empty = self.frame_detached_handlers.lock().unwrap().is_empty();
1286 self.frame_detached_handlers.lock().unwrap().push(handler);
1287 if was_empty {
1288 for page in self.pages() {
1289 Self::wire_frame_detached(&page, self.frame_detached_handlers.clone()).await;
1290 }
1291 }
1292 Ok(())
1293 }
1294
1295 /// Adds a listener for the `frameNavigated` event: fired when a frame
1296 /// navigates in any page of the context. Forwarded from each page.
1297 ///
1298 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-frame-navigated>
1299 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1300 pub async fn on_frame_navigated<F, Fut>(&self, handler: F) -> Result<()>
1301 where
1302 F: Fn(Frame) -> Fut + Send + Sync + 'static,
1303 Fut: Future<Output = Result<()>> + Send + 'static,
1304 {
1305 let handler = Arc::new(move |f: Frame| -> CtxHandlerFuture { Box::pin(handler(f)) });
1306 let was_empty = self.frame_navigated_handlers.lock().unwrap().is_empty();
1307 self.frame_navigated_handlers.lock().unwrap().push(handler);
1308 if was_empty {
1309 for page in self.pages() {
1310 Self::wire_frame_navigated(&page, self.frame_navigated_handlers.clone()).await;
1311 }
1312 }
1313 Ok(())
1314 }
1315
1316 /// Adds a listener for the `pageLoad` event: fired when any page in the
1317 /// context fires its `load` event. The handler receives that `Page`.
1318 ///
1319 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-load>
1320 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1321 pub async fn on_page_load<F, Fut>(&self, handler: F) -> Result<()>
1322 where
1323 F: Fn(Page) -> Fut + Send + Sync + 'static,
1324 Fut: Future<Output = Result<()>> + Send + 'static,
1325 {
1326 let handler = Arc::new(move |p: Page| -> CtxHandlerFuture { Box::pin(handler(p)) });
1327 let was_empty = self.page_load_handlers.lock().unwrap().is_empty();
1328 self.page_load_handlers.lock().unwrap().push(handler);
1329 if was_empty {
1330 for page in self.pages() {
1331 Self::wire_page_load(&page, self.page_load_handlers.clone()).await;
1332 }
1333 }
1334 Ok(())
1335 }
1336
1337 /// Adds a listener for the `pageClose` event: fired when any page in the
1338 /// context closes. The handler receives that `Page`.
1339 ///
1340 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-close>
1341 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1342 pub async fn on_page_close<F, Fut>(&self, handler: F) -> Result<()>
1343 where
1344 F: Fn(Page) -> Fut + Send + Sync + 'static,
1345 Fut: Future<Output = Result<()>> + Send + 'static,
1346 {
1347 let handler = Arc::new(move |p: Page| -> CtxHandlerFuture { Box::pin(handler(p)) });
1348 let was_empty = self.page_close_handlers.lock().unwrap().is_empty();
1349 self.page_close_handlers.lock().unwrap().push(handler);
1350 if was_empty {
1351 for page in self.pages() {
1352 Self::wire_page_close(&page, self.page_close_handlers.clone()).await;
1353 }
1354 }
1355 Ok(())
1356 }
1357
1358 // --- Forwarders: wire a single page's events to the context handler vecs. ---
1359 // Each (page, event) is wired exactly once: on the first context handler
1360 // (current pages) or at page creation (future pages, see the "page" event
1361 // dispatch). The vec is cloned out under the lock before awaiting handlers.
1362
1363 async fn wire_download(page: &Page, handlers: Arc<Mutex<Vec<DownloadHandler>>>) {
1364 let _ = page
1365 .on_download(move |d: Download| {
1366 let handlers = handlers.clone();
1367 async move {
1368 let hs = handlers.lock().unwrap().clone();
1369 for h in hs {
1370 let _ = h(d.clone()).await;
1371 }
1372 Ok(())
1373 }
1374 })
1375 .await;
1376 }
1377
1378 async fn wire_frame_attached(page: &Page, handlers: Arc<Mutex<Vec<CtxFrameHandler>>>) {
1379 let _ = page
1380 .on_frameattached(move |f: Frame| {
1381 let handlers = handlers.clone();
1382 async move {
1383 let hs = handlers.lock().unwrap().clone();
1384 for h in hs {
1385 let _ = h(f.clone()).await;
1386 }
1387 Ok(())
1388 }
1389 })
1390 .await;
1391 }
1392
1393 async fn wire_frame_detached(page: &Page, handlers: Arc<Mutex<Vec<CtxFrameHandler>>>) {
1394 let _ = page
1395 .on_framedetached(move |f: Frame| {
1396 let handlers = handlers.clone();
1397 async move {
1398 let hs = handlers.lock().unwrap().clone();
1399 for h in hs {
1400 let _ = h(f.clone()).await;
1401 }
1402 Ok(())
1403 }
1404 })
1405 .await;
1406 }
1407
1408 async fn wire_frame_navigated(page: &Page, handlers: Arc<Mutex<Vec<CtxFrameHandler>>>) {
1409 let _ = page
1410 .on_framenavigated(move |f: Frame| {
1411 let handlers = handlers.clone();
1412 async move {
1413 let hs = handlers.lock().unwrap().clone();
1414 for h in hs {
1415 let _ = h(f.clone()).await;
1416 }
1417 Ok(())
1418 }
1419 })
1420 .await;
1421 }
1422
1423 async fn wire_page_load(page: &Page, handlers: Arc<Mutex<Vec<PageEventHandler>>>) {
1424 let p = page.clone();
1425 let _ = page
1426 .on_load(move || {
1427 let handlers = handlers.clone();
1428 let p = p.clone();
1429 async move {
1430 let hs = handlers.lock().unwrap().clone();
1431 for h in hs {
1432 let _ = h(p.clone()).await;
1433 }
1434 Ok(())
1435 }
1436 })
1437 .await;
1438 }
1439
1440 async fn wire_page_close(page: &Page, handlers: Arc<Mutex<Vec<PageEventHandler>>>) {
1441 let p = page.clone();
1442 let _ = page
1443 .on_close(move || {
1444 let handlers = handlers.clone();
1445 let p = p.clone();
1446 async move {
1447 let hs = handlers.lock().unwrap().clone();
1448 for h in hs {
1449 let _ = h(p.clone()).await;
1450 }
1451 Ok(())
1452 }
1453 })
1454 .await;
1455 }
1456
1457 /// Adds a listener for the `close` event.
1458 ///
1459 /// The handler is called when the browser context is closed.
1460 ///
1461 /// # Arguments
1462 ///
1463 /// * `handler` - Async function called with no arguments when the context closes
1464 ///
1465 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-close>
1466 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1467 pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
1468 where
1469 F: Fn() -> Fut + Send + Sync + 'static,
1470 Fut: Future<Output = Result<()>> + Send + 'static,
1471 {
1472 let handler: Handler<()> = Arc::new(move |()| Box::pin(handler()));
1473 self.close_events.add_handler(handler);
1474 Ok(())
1475 }
1476
1477 /// Adds a listener for the `request` event.
1478 ///
1479 /// The handler fires whenever a request is issued from any page in the context.
1480 /// This is equivalent to subscribing to `on_request` on each individual page,
1481 /// but covers all current and future pages of the context.
1482 ///
1483 /// Context-level handlers fire before page-level handlers.
1484 ///
1485 /// # Arguments
1486 ///
1487 /// * `handler` - Async function that receives the `Request`
1488 ///
1489 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request>
1490 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1491 pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
1492 where
1493 F: Fn(Request) -> Fut + Send + Sync + 'static,
1494 Fut: Future<Output = Result<()>> + Send + 'static,
1495 {
1496 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
1497 self.subscribe_if_idle(&self.request).await;
1498 self.request.add_handler(handler);
1499 Ok(())
1500 }
1501
1502 /// Adds a listener for the `requestFinished` event.
1503 ///
1504 /// The handler fires after the request has been successfully received by the server
1505 /// and a response has been fully downloaded for any page in the context.
1506 ///
1507 /// Context-level handlers fire before page-level handlers.
1508 ///
1509 /// # Arguments
1510 ///
1511 /// * `handler` - Async function that receives the completed `Request`
1512 ///
1513 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request-finished>
1514 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1515 pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
1516 where
1517 F: Fn(Request) -> Fut + Send + Sync + 'static,
1518 Fut: Future<Output = Result<()>> + Send + 'static,
1519 {
1520 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
1521 self.subscribe_if_idle(&self.request_finished).await;
1522 self.request_finished.add_handler(handler);
1523 Ok(())
1524 }
1525
1526 /// Adds a listener for the `requestFailed` event.
1527 ///
1528 /// The handler fires when a request from any page in the context fails,
1529 /// for example due to a network error or if the server returned an error response.
1530 ///
1531 /// Context-level handlers fire before page-level handlers.
1532 ///
1533 /// # Arguments
1534 ///
1535 /// * `handler` - Async function that receives the failed `Request`
1536 ///
1537 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-request-failed>
1538 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1539 pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
1540 where
1541 F: Fn(Request) -> Fut + Send + Sync + 'static,
1542 Fut: Future<Output = Result<()>> + Send + 'static,
1543 {
1544 let handler: Handler<Request> = Arc::new(move |request| Box::pin(handler(request)));
1545 self.subscribe_if_idle(&self.request_failed).await;
1546 self.request_failed.add_handler(handler);
1547 Ok(())
1548 }
1549
1550 /// Adds a listener for the `response` event.
1551 ///
1552 /// The handler fires whenever a response is received from any page in the context.
1553 ///
1554 /// Context-level handlers fire before page-level handlers.
1555 ///
1556 /// # Arguments
1557 ///
1558 /// * `handler` - Async function that receives the `ResponseObject`
1559 ///
1560 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-response>
1561 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1562 pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
1563 where
1564 F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
1565 Fut: Future<Output = Result<()>> + Send + 'static,
1566 {
1567 let handler: Handler<ResponseObject> =
1568 Arc::new(move |response| Box::pin(handler(response)));
1569 self.subscribe_if_idle(&self.response).await;
1570 self.response.add_handler(handler);
1571 Ok(())
1572 }
1573
1574 /// Adds a listener for the `dialog` event on this browser context.
1575 ///
1576 /// The handler fires whenever a JavaScript dialog (alert, confirm, prompt,
1577 /// or beforeunload) is triggered from **any** page in the context. Context-level
1578 /// handlers fire before page-level handlers.
1579 ///
1580 /// The dialog must be explicitly accepted or dismissed; otherwise the page
1581 /// will freeze waiting for a response.
1582 ///
1583 /// # Arguments
1584 ///
1585 /// * `handler` - Async function that receives the [`Dialog`](crate::protocol::Dialog) and calls
1586 /// `dialog.accept()` or `dialog.dismiss()`.
1587 ///
1588 /// # Errors
1589 ///
1590 /// Returns error if communication with the browser process fails.
1591 ///
1592 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog>
1593 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1594 pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
1595 where
1596 F: Fn(crate::protocol::Dialog) -> Fut + Send + Sync + 'static,
1597 Fut: Future<Output = Result<()>> + Send + 'static,
1598 {
1599 let handler: Handler<crate::protocol::Dialog> =
1600 Arc::new(move |dialog| Box::pin(handler(dialog)));
1601 self.dialog.add_handler(handler);
1602 Ok(())
1603 }
1604
1605 /// Registers a context-level console event handler.
1606 ///
1607 /// The handler fires for any console message emitted by any page in this context.
1608 /// Context-level handlers fire before page-level handlers.
1609 ///
1610 /// The server only sends console events after the first handler is registered
1611 /// (subscription is managed automatically per context channel).
1612 ///
1613 /// # Arguments
1614 ///
1615 /// * `handler` - Async closure that receives the [`ConsoleMessage`](crate::protocol::ConsoleMessage)
1616 ///
1617 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-console>
1618 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1619 pub async fn on_console<F, Fut>(&self, handler: F) -> Result<()>
1620 where
1621 F: Fn(crate::protocol::ConsoleMessage) -> Fut + Send + Sync + 'static,
1622 Fut: Future<Output = Result<()>> + Send + 'static,
1623 {
1624 let handler: Handler<crate::protocol::ConsoleMessage> =
1625 Arc::new(move |msg| Box::pin(handler(msg)));
1626
1627 self.subscribe_if_idle(&self.console).await;
1628 self.console.add_handler(handler);
1629
1630 Ok(())
1631 }
1632
1633 /// Registers a context-level handler for uncaught JavaScript exceptions.
1634 ///
1635 /// The handler fires whenever a page in this context throws an unhandled
1636 /// JavaScript error (i.e. an exception that propagates to `window.onerror`
1637 /// or an unhandled promise rejection). The [`WebError`](crate::protocol::WebError)
1638 /// passed to the handler contains the error message and an optional back-reference
1639 /// to the originating page.
1640 ///
1641 /// # Arguments
1642 ///
1643 /// * `handler` - Async closure that receives a [`WebError`](crate::protocol::WebError).
1644 ///
1645 /// # Errors
1646 ///
1647 /// Returns error if communication with the browser process fails.
1648 ///
1649 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-web-error>
1650 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1651 pub async fn on_weberror<F, Fut>(&self, handler: F) -> Result<()>
1652 where
1653 F: Fn(crate::protocol::WebError) -> Fut + Send + Sync + 'static,
1654 Fut: Future<Output = Result<()>> + Send + 'static,
1655 {
1656 let handler: Handler<crate::protocol::WebError> =
1657 Arc::new(move |web_error| Box::pin(handler(web_error)));
1658 self.weberror.add_handler(handler);
1659 Ok(())
1660 }
1661
1662 /// Registers a handler for the `serviceWorker` event.
1663 ///
1664 /// The handler is called when a new service worker is registered in the browser context.
1665 ///
1666 /// Note: Service worker testing typically requires HTTPS and a registered service worker.
1667 ///
1668 /// # Arguments
1669 ///
1670 /// * `handler` - Async closure called with the new [`Worker`](crate::protocol::Worker) object
1671 ///
1672 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-service-worker>
1673 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1674 pub async fn on_serviceworker<F, Fut>(&self, handler: F) -> Result<()>
1675 where
1676 F: Fn(crate::protocol::Worker) -> Fut + Send + Sync + 'static,
1677 Fut: Future<Output = Result<()>> + Send + 'static,
1678 {
1679 let handler = Arc::new(
1680 move |worker: crate::protocol::Worker| -> ServiceWorkerHandlerFuture {
1681 Box::pin(handler(worker))
1682 },
1683 );
1684 self.serviceworker_handlers.lock().unwrap().push(handler);
1685 Ok(())
1686 }
1687
1688 /// Exposes a Rust function to every page in this browser context as
1689 /// `window[name]` in JavaScript.
1690 ///
1691 /// When JavaScript code calls `window[name](arg1, arg2, …)` the Playwright
1692 /// server fires a `bindingCall` event that invokes `callback` with the
1693 /// deserialized arguments. The return value of `callback` is serialized back
1694 /// to JavaScript so the `await window[name](…)` expression resolves with it.
1695 ///
1696 /// The binding is injected into every existing page and every new page
1697 /// created in this context.
1698 ///
1699 /// # Arguments
1700 ///
1701 /// * `name` – JavaScript identifier that will be available as `window[name]`.
1702 /// * `callback` – Async closure called with `Vec<serde_json::Value>` (the JS
1703 /// arguments) and returning `serde_json::Value` (the result).
1704 ///
1705 /// # Errors
1706 ///
1707 /// Returns error if:
1708 /// - The context has been closed.
1709 /// - Communication with the browser process fails.
1710 ///
1711 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-expose-function>
1712 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), name = %name))]
1713 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1714 where
1715 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1716 Fut: Future<Output = serde_json::Value> + Send + 'static,
1717 {
1718 self.expose_binding_internal(name, callback).await
1719 }
1720
1721 /// Exposes a Rust function to every page in this browser context as
1722 /// `window[name]` in JavaScript.
1723 ///
1724 /// Currently identical to [`expose_function`](Self::expose_function):
1725 /// arguments arrive as plain serialized values. Upstream Playwright's
1726 /// `exposeBinding` can additionally hand the callback a source
1727 /// (page/frame) descriptor, which this crate does not surface yet.
1728 ///
1729 /// # Arguments
1730 ///
1731 /// * `name` – JavaScript identifier.
1732 /// * `callback` – Async closure with `Vec<serde_json::Value>` → `serde_json::Value`.
1733 ///
1734 /// # Errors
1735 ///
1736 /// Returns error if:
1737 /// - The context has been closed.
1738 /// - Communication with the browser process fails.
1739 ///
1740 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-expose-binding>
1741 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), name = %name))]
1742 pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1743 where
1744 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1745 Fut: Future<Output = serde_json::Value> + Send + 'static,
1746 {
1747 self.expose_binding_internal(name, callback).await
1748 }
1749
1750 /// Internal implementation shared by expose_function and expose_binding.
1751 async fn expose_binding_internal<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1752 where
1753 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1754 Fut: Future<Output = serde_json::Value> + Send + 'static,
1755 {
1756 // Wrap callback with type erasure
1757 let callback: BindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
1758 Box::pin(callback(args)) as BindingCallbackFuture
1759 });
1760
1761 // Store the callback before sending the RPC so that a race-condition
1762 // where a bindingCall arrives before we finish registering is avoided.
1763 self.binding_callbacks
1764 .lock()
1765 .unwrap()
1766 .insert(name.to_string(), callback);
1767
1768 // Tell the Playwright server to inject window[name] into every page.
1769 //
1770 // The protocol also accepts `noGlobal`, which suppresses that
1771 // injection. It is deliberately not exposed: it exists to support
1772 // passing functions as evaluate arguments, where the binding is
1773 // called through the bindings controller rather than off `window`.
1774 self.channel()
1775 .send_no_result("exposeBinding", serde_json::json!({ "name": name }))
1776 .await
1777 }
1778
1779 /// Waits for a new page to be created in this browser context.
1780 ///
1781 /// Creates a one-shot waiter that resolves when the next `page` event fires.
1782 /// The waiter **must** be created before the action that triggers the new page
1783 /// (e.g. `new_page()` or a user action that opens a popup) to avoid a race
1784 /// condition.
1785 ///
1786 /// # Arguments
1787 ///
1788 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1789 ///
1790 /// # Errors
1791 ///
1792 /// Returns [`crate::error::Error::Timeout`] if no page is created within the timeout.
1793 ///
1794 /// # Example
1795 ///
1796 /// ```no_run
1797 /// # use playwright_rs::Playwright;
1798 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1799 /// # let pw = Playwright::launch().await?;
1800 /// # let browser = pw.chromium().launch().await?;
1801 /// # let context = browser.new_context().await?;
1802 /// // Set up the waiter BEFORE the triggering action
1803 /// let waiter = context.expect_page(None).await?;
1804 /// let _page = context.new_page().await?;
1805 /// let new_page = waiter.wait().await?;
1806 /// # Ok(())
1807 /// # }
1808 /// ```
1809 ///
1810 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1811 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1812 pub async fn expect_page(&self, timeout: Option<f64>) -> Result<EventWaiter<Page>> {
1813 let rx = self.page_events.wait();
1814 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1815 }
1816
1817 /// Waits for this browser context to be closed.
1818 ///
1819 /// Creates a one-shot waiter that resolves when the `close` event fires.
1820 /// The waiter **must** be created before the action that closes the context
1821 /// to avoid a race condition.
1822 ///
1823 /// # Arguments
1824 ///
1825 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1826 ///
1827 /// # Errors
1828 ///
1829 /// Returns [`crate::error::Error::Timeout`] if the context is not closed within the timeout.
1830 ///
1831 /// # Example
1832 ///
1833 /// ```no_run
1834 /// # use playwright_rs::Playwright;
1835 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1836 /// # let pw = Playwright::launch().await?;
1837 /// # let browser = pw.chromium().launch().await?;
1838 /// # let context = browser.new_context().await?;
1839 /// // Set up the waiter BEFORE closing
1840 /// let waiter = context.expect_close(None).await?;
1841 /// context.close().await?;
1842 /// waiter.wait().await?;
1843 /// # Ok(())
1844 /// # }
1845 /// ```
1846 ///
1847 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1848 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1849 pub async fn expect_close(&self, timeout: Option<f64>) -> Result<EventWaiter<()>> {
1850 let rx = self.close_events.wait();
1851 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1852 }
1853
1854 /// Waits for a console message from any page in this context.
1855 ///
1856 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-console>
1857 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1858 pub async fn expect_console_message(
1859 &self,
1860 timeout: Option<f64>,
1861 ) -> Result<EventWaiter<crate::protocol::ConsoleMessage>> {
1862 self.subscribe_if_idle(&self.console).await;
1863 let rx = self.console.wait();
1864 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1865 }
1866
1867 /// Waits for the given event to fire and returns a typed `EventValue`.
1868 ///
1869 /// This is the generic version of the specific `expect_*` methods. It matches
1870 /// the playwright-python / playwright-js `context.expect_event(event_name)` API.
1871 ///
1872 /// The waiter **must** be created before the action that triggers the event.
1873 ///
1874 /// # Supported event names
1875 ///
1876 /// `"page"`, `"close"`, `"console"`, `"request"`, `"response"`,
1877 /// `"weberror"`, `"serviceworker"`
1878 ///
1879 /// # Arguments
1880 ///
1881 /// * `event` - Event name (case-sensitive, matches Playwright protocol names).
1882 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1883 ///
1884 /// # Errors
1885 ///
1886 /// Returns [`crate::error::Error::InvalidArgument`] for unknown event names.
1887 /// Returns [`crate::error::Error::Timeout`] if the event does not fire within the timeout.
1888 ///
1889 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1890 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1891 pub async fn expect_event(
1892 &self,
1893 event: &str,
1894 timeout: Option<f64>,
1895 ) -> crate::error::Result<EventWaiter<crate::protocol::EventValue>> {
1896 use crate::protocol::EventValue;
1897 use tokio::sync::oneshot;
1898
1899 let timeout_ms = timeout.or(Some(30_000.0));
1900
1901 match event {
1902 "page" => {
1903 let (mut tx, rx) = oneshot::channel::<EventValue>();
1904 let inner_rx = self.page_events.wait();
1905
1906 // select: drop the registry receiver when the caller times
1907 // out, or a stale FIFO waiter swallows the next event.
1908 tokio::spawn(async move {
1909 tokio::select! {
1910 v = inner_rx => {
1911 if let Ok(v) = v { let _ = tx.send(EventValue::Page(v)); }
1912 }
1913 () = tx.closed() => {}
1914 }
1915 });
1916
1917 Ok(EventWaiter::new(rx, timeout_ms))
1918 }
1919
1920 "close" => {
1921 let (mut tx, rx) = oneshot::channel::<EventValue>();
1922 let inner_rx = self.close_events.wait();
1923
1924 // select: drop the registry receiver when the caller times
1925 // out, or a stale FIFO waiter swallows the next event.
1926 tokio::spawn(async move {
1927 tokio::select! {
1928 v = inner_rx => {
1929 if v.is_ok() { let _ = tx.send(EventValue::Close); }
1930 }
1931 () = tx.closed() => {}
1932 }
1933 });
1934
1935 Ok(EventWaiter::new(rx, timeout_ms))
1936 }
1937
1938 "console" => {
1939 let (mut tx, rx) = oneshot::channel::<EventValue>();
1940
1941 self.subscribe_if_idle(&self.console).await;
1942 let inner_rx = self.console.wait();
1943
1944 // select: drop the registry receiver when the caller times
1945 // out, or a stale FIFO waiter swallows the next event.
1946 tokio::spawn(async move {
1947 tokio::select! {
1948 v = inner_rx => {
1949 if let Ok(v) = v { let _ = tx.send(EventValue::ConsoleMessage(v)); }
1950 }
1951 () = tx.closed() => {}
1952 }
1953 });
1954
1955 Ok(EventWaiter::new(rx, timeout_ms))
1956 }
1957
1958 "request" => {
1959 let (mut tx, rx) = oneshot::channel::<EventValue>();
1960
1961 self.subscribe_if_idle(&self.request).await;
1962 let inner_rx = self.request.wait();
1963
1964 // select: drop the registry receiver when the caller times
1965 // out, or a stale FIFO waiter swallows the next event.
1966 tokio::spawn(async move {
1967 tokio::select! {
1968 v = inner_rx => {
1969 if let Ok(v) = v { let _ = tx.send(EventValue::Request(v)); }
1970 }
1971 () = tx.closed() => {}
1972 }
1973 });
1974
1975 Ok(EventWaiter::new(rx, timeout_ms))
1976 }
1977
1978 "response" => {
1979 let (mut tx, rx) = oneshot::channel::<EventValue>();
1980
1981 self.subscribe_if_idle(&self.response).await;
1982 let inner_rx = self.response.wait();
1983
1984 // select: drop the registry receiver when the caller times
1985 // out, or a stale FIFO waiter swallows the next event.
1986 tokio::spawn(async move {
1987 tokio::select! {
1988 v = inner_rx => {
1989 if let Ok(v) = v { let _ = tx.send(EventValue::Response(v)); }
1990 }
1991 () = tx.closed() => {}
1992 }
1993 });
1994
1995 Ok(EventWaiter::new(rx, timeout_ms))
1996 }
1997
1998 "weberror" => {
1999 let (mut tx, rx) = oneshot::channel::<EventValue>();
2000 let inner_rx = self.weberror.wait();
2001
2002 // select: drop the registry receiver when the caller times
2003 // out, or a stale FIFO waiter swallows the next event.
2004 tokio::spawn(async move {
2005 tokio::select! {
2006 v = inner_rx => {
2007 if let Ok(v) = v { let _ = tx.send(EventValue::WebError(v)); }
2008 }
2009 () = tx.closed() => {}
2010 }
2011 });
2012
2013 Ok(EventWaiter::new(rx, timeout_ms))
2014 }
2015
2016 "serviceworker" => {
2017 let (tx, rx) = oneshot::channel::<EventValue>();
2018 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Worker>();
2019 self.serviceworker_waiters.lock().unwrap().push(inner_tx);
2020
2021 tokio::spawn(async move {
2022 if let Ok(v) = inner_rx.await {
2023 let _ = tx.send(EventValue::Worker(v));
2024 }
2025 });
2026
2027 Ok(EventWaiter::new(rx, timeout_ms))
2028 }
2029
2030 other => Err(crate::error::Error::InvalidArgument(format!(
2031 "Unknown event name '{}'. Supported: page, close, console, request, response, \
2032 weberror, serviceworker",
2033 other
2034 ))),
2035 }
2036 }
2037
2038 /// Intercepts WebSocket connections matching the given URL pattern for all pages in this context.
2039 ///
2040 /// When a WebSocket connection from any page in this context matches `url`,
2041 /// the `handler` is called with a [`WebSocketRoute`](crate::protocol::WebSocketRoute) object.
2042 /// The handler must call [`connect_to_server`](crate::protocol::WebSocketRoute::connect_to_server)
2043 /// to forward the connection to the real server, or
2044 /// [`close`](crate::protocol::WebSocketRoute::close) to terminate it.
2045 ///
2046 /// # Arguments
2047 ///
2048 /// * `url` — URL glob pattern (e.g. `"ws://**"` or `"wss://example.com/ws"`).
2049 /// * `handler` — Async closure receiving a `WebSocketRoute`.
2050 ///
2051 /// # Errors
2052 ///
2053 /// Returns an error if the RPC call to enable interception fails.
2054 ///
2055 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route-web-socket>
2056 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %url))]
2057 pub async fn route_web_socket<F, Fut>(&self, url: &str, handler: F) -> Result<()>
2058 where
2059 F: Fn(crate::protocol::WebSocketRoute) -> Fut + Send + Sync + 'static,
2060 Fut: Future<Output = Result<()>> + Send + 'static,
2061 {
2062 let handler = Arc::new(
2063 move |route: crate::protocol::WebSocketRoute| -> WsRouteHandlerFuture {
2064 Box::pin(handler(route))
2065 },
2066 );
2067
2068 self.ws_route_handlers
2069 .lock()
2070 .unwrap()
2071 .push(ContextWsRouteHandlerEntry {
2072 pattern: url.to_string(),
2073 handler,
2074 });
2075
2076 self.enable_ws_interception().await
2077 }
2078
2079 /// Updates WebSocket interception patterns for this context.
2080 async fn enable_ws_interception(&self) -> Result<()> {
2081 let patterns: Vec<serde_json::Value> = self
2082 .ws_route_handlers
2083 .lock()
2084 .unwrap()
2085 .iter()
2086 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
2087 .collect();
2088
2089 self.channel()
2090 .send_no_result(
2091 "setWebSocketInterceptionPatterns",
2092 serde_json::json!({ "patterns": patterns }),
2093 )
2094 .await
2095 }
2096
2097 /// Updates network interception patterns for this context
2098 async fn enable_network_interception(&self) -> Result<()> {
2099 let patterns: Vec<serde_json::Value> = self
2100 .route_handlers
2101 .lock()
2102 .unwrap()
2103 .iter()
2104 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
2105 .collect();
2106
2107 self.channel()
2108 .send_no_result(
2109 "setNetworkInterceptionPatterns",
2110 serde_json::json!({ "patterns": patterns }),
2111 )
2112 .await
2113 }
2114
2115 /// Deserializes binding call arguments from Playwright's protocol format.
2116 ///
2117 /// The `args` field in the BindingCall initializer is a JSON array where each
2118 /// element is in `serialize_argument` format: `{"value": <tagged>, "handles": []}`.
2119 /// This helper extracts the inner "value" from each entry and parses it.
2120 ///
2121 /// This is `pub` so that `Page::on_event("bindingCall")` can reuse it without
2122 /// duplicating the deserialization logic.
2123 pub fn deserialize_binding_args_pub(raw_args: &Value) -> Vec<Value> {
2124 Self::deserialize_binding_args(raw_args)
2125 }
2126
2127 fn deserialize_binding_args(raw_args: &Value) -> Vec<Value> {
2128 let Some(arr) = raw_args.as_array() else {
2129 return vec![];
2130 };
2131
2132 arr.iter()
2133 .map(|arg| {
2134 // Each arg is a direct Playwright type-tagged value, e.g. {"n": 3} or {"s": "hello"}
2135 // (NOT wrapped in {"value": ..., "handles": []} — that format is only for evaluate args)
2136 crate::protocol::evaluate_conversion::parse_value(arg, None)
2137 })
2138 .collect()
2139 }
2140
2141 /// Handles a route event from the protocol
2142 async fn on_route_event(route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>, route: Route) {
2143 let handlers = route_handlers.lock().unwrap().clone();
2144 let url = route.request().url().to_string();
2145
2146 for entry in handlers.iter().rev() {
2147 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
2148 let handler = entry.handler.clone();
2149 if let Err(e) = handler(route.clone()).await {
2150 tracing::warn!("Context route handler error: {}", e);
2151 break;
2152 }
2153 if !route.was_handled() {
2154 continue;
2155 }
2156 break;
2157 }
2158 }
2159 }
2160
2161 fn dispatch_request_event(&self, method: &str, params: Value) {
2162 if let Some(request_guid) = params
2163 .get("request")
2164 .and_then(|v| v.get("guid"))
2165 .and_then(|v| v.as_str())
2166 {
2167 let connection = self.connection();
2168 let request_guid_owned = request_guid.to_owned();
2169 let page_guid_owned = params
2170 .get("page")
2171 .and_then(|v| v.get("guid"))
2172 .and_then(|v| v.as_str())
2173 .map(|v| v.to_owned());
2174 // Extract failureText for requestFailed events
2175 let failure_text = params
2176 .get("failureText")
2177 .and_then(|v| v.as_str())
2178 .map(|s| s.to_owned());
2179 // Extract response GUID for requestFinished events (to read timing)
2180 let response_guid_owned = params
2181 .get("response")
2182 .and_then(|v| v.get("guid"))
2183 .and_then(|v| v.as_str())
2184 .map(|s| s.to_owned());
2185 // Extract responseEndTiming from requestFinished event params
2186 let response_end_timing = params.get("responseEndTiming").and_then(|v| v.as_f64());
2187 let method = method.to_owned();
2188 // Clone context-level handler vecs for use in spawn
2189 let ctx_request = self.request.clone();
2190 let ctx_request_finished = self.request_finished.clone();
2191 let ctx_request_failed = self.request_failed.clone();
2192 tokio::spawn(async move {
2193 let request: Request =
2194 match connection.get_typed::<Request>(&request_guid_owned).await {
2195 Ok(r) => r,
2196 Err(_) => return,
2197 };
2198
2199 // Set failure text on the request before dispatching to handlers
2200 if let Some(text) = failure_text {
2201 request.set_failure_text(text);
2202 }
2203
2204 // For requestFinished, extract timing from the Response object's initializer
2205 if method == "requestFinished"
2206 && let Some(timing) =
2207 extract_timing(&connection, response_guid_owned, response_end_timing).await
2208 {
2209 request.set_timing(timing);
2210 }
2211
2212 // Dispatch to context-level handlers first (matching playwright-python behavior)
2213 match method.as_str() {
2214 "request" => ctx_request.dispatch(request.clone()).await,
2215 "requestFinished" => ctx_request_finished.dispatch(request.clone()).await,
2216 "requestFailed" => ctx_request_failed.dispatch(request.clone()).await,
2217 _ => {}
2218 }
2219
2220 // Then dispatch to page-level handlers
2221 if let Some(page_guid) = page_guid_owned {
2222 let page: Page = match connection.get_typed::<Page>(&page_guid).await {
2223 Ok(p) => p,
2224 Err(_) => return,
2225 };
2226 match method.as_str() {
2227 "request" => page.trigger_request_event(request).await,
2228 "requestFailed" => page.trigger_request_failed_event(request).await,
2229 "requestFinished" => page.trigger_request_finished_event(request).await,
2230 _ => unreachable!("Unreachable method {}", method),
2231 }
2232 }
2233 });
2234 }
2235 }
2236
2237 fn dispatch_response_event(&self, _method: &str, params: Value) {
2238 if let Some(response_guid) = params
2239 .get("response")
2240 .and_then(|v| v.get("guid"))
2241 .and_then(|v| v.as_str())
2242 {
2243 let connection = self.connection();
2244 let response_guid_owned = response_guid.to_owned();
2245 let page_guid_owned = params
2246 .get("page")
2247 .and_then(|v| v.get("guid"))
2248 .and_then(|v| v.as_str())
2249 .map(|v| v.to_owned());
2250 let ctx_response = self.response.clone();
2251 tokio::spawn(async move {
2252 let response: ResponseObject = match connection
2253 .get_typed::<ResponseObject>(&response_guid_owned)
2254 .await
2255 {
2256 Ok(r) => r,
2257 Err(_) => return,
2258 };
2259
2260 // Dispatch to context-level handlers first (matching playwright-python behavior)
2261 ctx_response.dispatch(response.clone()).await;
2262
2263 // Then dispatch to page-level handlers
2264 if let Some(page_guid) = page_guid_owned {
2265 let page: Page = match connection.get_typed::<Page>(&page_guid).await {
2266 Ok(p) => p,
2267 Err(_) => return,
2268 };
2269 page.trigger_response_event(response).await;
2270 }
2271 });
2272 }
2273 }
2274}
2275
2276impl ChannelOwner for BrowserContext {
2277 fn guid(&self) -> &str {
2278 self.base.guid()
2279 }
2280
2281 fn type_name(&self) -> &str {
2282 self.base.type_name()
2283 }
2284
2285 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2286 self.base.parent()
2287 }
2288
2289 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2290 self.base.connection()
2291 }
2292
2293 fn initializer(&self) -> &Value {
2294 self.base.initializer()
2295 }
2296
2297 fn channel(&self) -> &Channel {
2298 self.base.channel()
2299 }
2300
2301 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2302 self.base.dispose(reason)
2303 }
2304
2305 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2306 self.base.adopt(child)
2307 }
2308
2309 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2310 self.base.add_child(guid, child)
2311 }
2312
2313 fn remove_child(&self, guid: &str) {
2314 self.base.remove_child(guid)
2315 }
2316
2317 fn on_event(&self, method: &str, params: Value) {
2318 match method {
2319 "request" | "requestFailed" | "requestFinished" => {
2320 self.dispatch_request_event(method, params)
2321 }
2322 "response" => self.dispatch_response_event(method, params),
2323 "close" => {
2324 // BrowserContext close event — mark as closed and fire registered close handlers
2325 self.is_closed.store(true, Ordering::Relaxed);
2326 let close_events = self.close_events.clone();
2327 tokio::spawn(async move {
2328 close_events.dispatch_all(()).await;
2329 });
2330 }
2331 "page" => {
2332 // Page events are triggered when pages are created, including:
2333 // - Initial page in persistent context with --app mode
2334 // - Popup pages opened through user interactions
2335 // Event format: {page: {guid: "..."}}
2336 if let Some(page_guid) = params
2337 .get("page")
2338 .and_then(|v| v.get("guid"))
2339 .and_then(|v| v.as_str())
2340 {
2341 let connection = self.connection();
2342 let page_guid_owned = page_guid.to_string();
2343 let pages = self.pages.clone();
2344 let page_events = self.page_events.clone();
2345 let download_handlers = self.download_handlers.clone();
2346 let frame_attached_handlers = self.frame_attached_handlers.clone();
2347 let frame_detached_handlers = self.frame_detached_handlers.clone();
2348 let frame_navigated_handlers = self.frame_navigated_handlers.clone();
2349 let page_load_handlers = self.page_load_handlers.clone();
2350 let page_close_handlers = self.page_close_handlers.clone();
2351
2352 tokio::spawn(async move {
2353 // Get and downcast the Page object
2354 let page: Page = match connection.get_typed::<Page>(&page_guid_owned).await
2355 {
2356 Ok(p) => p,
2357 Err(_) => return,
2358 };
2359
2360 // Track the page
2361 pages.lock().unwrap().push(page.clone());
2362
2363 // Forward this new page's lifecycle events to any
2364 // context-level handlers already registered.
2365 if !download_handlers.lock().unwrap().is_empty() {
2366 Self::wire_download(&page, download_handlers.clone()).await;
2367 }
2368 if !frame_attached_handlers.lock().unwrap().is_empty() {
2369 Self::wire_frame_attached(&page, frame_attached_handlers.clone()).await;
2370 }
2371 if !frame_detached_handlers.lock().unwrap().is_empty() {
2372 Self::wire_frame_detached(&page, frame_detached_handlers.clone()).await;
2373 }
2374 if !frame_navigated_handlers.lock().unwrap().is_empty() {
2375 Self::wire_frame_navigated(&page, frame_navigated_handlers.clone())
2376 .await;
2377 }
2378 if !page_load_handlers.lock().unwrap().is_empty() {
2379 Self::wire_page_load(&page, page_load_handlers.clone()).await;
2380 }
2381 if !page_close_handlers.lock().unwrap().is_empty() {
2382 Self::wire_page_close(&page, page_close_handlers.clone()).await;
2383 }
2384
2385 // If this page has an opener, dispatch popup event to opener's handlers.
2386 // The opener guid is in the page's initializer: {"opener": {"guid": "..."}}
2387 if let Some(opener_guid) = page
2388 .initializer()
2389 .get("opener")
2390 .and_then(|v| v.get("guid"))
2391 .and_then(|v| v.as_str())
2392 && let Ok(opener) = connection.get_typed::<Page>(opener_guid).await
2393 {
2394 opener.trigger_popup_event(page.clone()).await;
2395 }
2396
2397 // Dispatch to context-level page handlers, then the
2398 // longest-waiting expect_page() caller.
2399 page_events.dispatch(page).await;
2400 });
2401 }
2402 }
2403 "pageError" => {
2404 // pageError event: fired when an uncaught JS exception occurs on a page.
2405 // Event format:
2406 // { "error": { "error": { "message": "...", "name": "...", "stack": "..." } },
2407 // "page": { "guid": "page@..." } }
2408 //
2409 // Dispatch path:
2410 // 1. Construct WebError and fire context-level on_weberror handlers.
2411 // 2. Forward the raw message to the page's on_pageerror handlers.
2412 let message = params
2413 .get("error")
2414 .and_then(|e| e.get("error"))
2415 .and_then(|e| e.get("message"))
2416 .and_then(|m| m.as_str())
2417 .unwrap_or("")
2418 .to_string();
2419
2420 let page_guid_owned = params
2421 .get("page")
2422 .and_then(|v| v.get("guid"))
2423 .and_then(|v| v.as_str())
2424 .map(|s| s.to_string());
2425
2426 let location =
2427 params
2428 .get("location")
2429 .map(|loc| crate::protocol::WebErrorLocation {
2430 url: loc
2431 .get("url")
2432 .and_then(|v| v.as_str())
2433 .unwrap_or("")
2434 .to_string(),
2435 line: loc.get("line").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
2436 column: loc.get("column").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
2437 });
2438
2439 let connection = self.connection();
2440 let weberror = self.weberror.clone();
2441
2442 tokio::spawn(async move {
2443 // Resolve page (optional — may be None if page already closed)
2444 let page = if let Some(ref guid) = page_guid_owned {
2445 connection.get_typed::<Page>(guid).await.ok()
2446 } else {
2447 None
2448 };
2449
2450 // 1. Dispatch to context-level weberror handlers
2451 let web_error = crate::protocol::WebError::new(
2452 message.clone(),
2453 page.clone(),
2454 location.clone(),
2455 );
2456 weberror.dispatch(web_error).await;
2457
2458 // 2. Forward to page-level pageerror handlers
2459 if let Some(p) = page {
2460 p.trigger_pageerror_event(message).await;
2461 }
2462 });
2463 }
2464 "dialog" => {
2465 // Dialog events come to BrowserContext.
2466 // Dispatch to context-level handlers first, then forward to the Page.
2467 // Event format: {dialog: {guid: "..."}}
2468 // The Dialog protocol object has the Page as its parent
2469 if let Some(dialog_guid) = params
2470 .get("dialog")
2471 .and_then(|v| v.get("guid"))
2472 .and_then(|v| v.as_str())
2473 {
2474 let connection = self.connection();
2475 let dialog_guid_owned = dialog_guid.to_string();
2476 let ctx_dialog = self.dialog.clone();
2477
2478 tokio::spawn(async move {
2479 // Get and downcast the Dialog object
2480 let dialog: crate::protocol::Dialog = match connection
2481 .get_typed::<crate::protocol::Dialog>(&dialog_guid_owned)
2482 .await
2483 {
2484 Ok(d) => d,
2485 Err(_) => return,
2486 };
2487
2488 // Dispatch to context-level dialog handlers first
2489 ctx_dialog.dispatch(dialog.clone()).await;
2490
2491 // Then forward to the Page's dialog handlers
2492 let page: Page =
2493 match crate::server::connection::downcast_parent::<Page>(&dialog) {
2494 Some(p) => p,
2495 None => return,
2496 };
2497
2498 page.trigger_dialog_event(dialog).await;
2499 });
2500 }
2501 }
2502 "bindingCall" => {
2503 // A JS caller invoked an exposed function. Dispatch to the registered
2504 // callback and send the result back via BindingCall::fulfill.
2505 // Event format: {binding: {guid: "..."}}
2506 if let Some(binding_guid) = params
2507 .get("binding")
2508 .and_then(|v| v.get("guid"))
2509 .and_then(|v| v.as_str())
2510 {
2511 let connection = self.connection();
2512 let binding_guid_owned = binding_guid.to_string();
2513 let binding_callbacks = self.binding_callbacks.clone();
2514
2515 tokio::spawn(async move {
2516 let binding_call: crate::protocol::BindingCall = match connection
2517 .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
2518 .await
2519 {
2520 Ok(bc) => bc,
2521 Err(e) => {
2522 tracing::warn!("Failed to get BindingCall object: {}", e);
2523 return;
2524 }
2525 };
2526
2527 let name = binding_call.name().to_string();
2528
2529 // Look up the registered callback
2530 let callback = {
2531 let callbacks = binding_callbacks.lock().unwrap();
2532 callbacks.get(&name).cloned()
2533 };
2534
2535 let Some(callback) = callback else {
2536 tracing::warn!("No callback registered for binding '{}'", name);
2537 let _ = binding_call
2538 .reject(&format!("No Rust handler for binding '{name}'"))
2539 .await;
2540 return;
2541 };
2542
2543 // Deserialize the args from Playwright protocol format
2544 let raw_args = binding_call.args();
2545 let args = Self::deserialize_binding_args(raw_args);
2546
2547 // Call the callback and serialize the result
2548 let result_value = callback(args).await;
2549 let serialized =
2550 crate::protocol::evaluate_conversion::serialize_argument(&result_value);
2551
2552 if let Err(e) = binding_call.resolve(serialized).await {
2553 tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
2554 }
2555 });
2556 }
2557 }
2558 "route" => {
2559 // Handle context-level network routing event
2560 if let Some(route_guid) = params
2561 .get("route")
2562 .and_then(|v| v.get("guid"))
2563 .and_then(|v| v.as_str())
2564 {
2565 let connection = self.connection();
2566 let route_guid_owned = route_guid.to_string();
2567 let route_handlers = self.route_handlers.clone();
2568 let request_context_guid = self.request_context_guid.clone();
2569
2570 tokio::spawn(async move {
2571 let route: Route =
2572 match connection.get_typed::<Route>(&route_guid_owned).await {
2573 Ok(r) => r,
2574 Err(e) => {
2575 tracing::warn!("Failed to get route object: {}", e);
2576 return;
2577 }
2578 };
2579
2580 // Set APIRequestContext on the route for fetch() support
2581 if let Some(ref guid) = request_context_guid
2582 && let Ok(api_ctx) =
2583 connection.get_typed::<APIRequestContext>(guid).await
2584 {
2585 route.set_api_request_context(api_ctx);
2586 }
2587
2588 BrowserContext::on_route_event(route_handlers, route).await;
2589 });
2590 }
2591 }
2592 "console" => {
2593 // Console events are sent to BrowserContext.
2594 // Construct ConsoleMessage from params, dispatch to context-level handlers,
2595 // then forward to the Page's on_console handlers.
2596 //
2597 // Event params format:
2598 // {
2599 // type: "log"|"error"|"warning"|...,
2600 // text: "rendered text",
2601 // location: { url: "...", lineNumber: N, columnNumber: N },
2602 // page: { guid: "page@..." },
2603 // args: [ { guid: "JSHandle@..." }, ... ] -- resolved to Arc<JSHandle>
2604 // timestamp: <f64 milliseconds since Unix epoch>
2605 // }
2606 let type_ = params
2607 .get("type")
2608 .and_then(|v| v.as_str())
2609 .unwrap_or("log")
2610 .to_string();
2611 let text = params
2612 .get("text")
2613 .and_then(|v| v.as_str())
2614 .unwrap_or("")
2615 .to_string();
2616 let loc_url = params
2617 .get("location")
2618 .and_then(|v| v.get("url"))
2619 .and_then(|v| v.as_str())
2620 .unwrap_or("")
2621 .to_string();
2622 // 1.60 emits `line`/`column`; older drivers used
2623 // `lineNumber`/`columnNumber` (deprecated, may be removed). Prefer
2624 // the new keys, fall back to the legacy ones.
2625 let loc_line = params
2626 .get("location")
2627 .and_then(|v| v.get("line").or_else(|| v.get("lineNumber")))
2628 .and_then(|v| v.as_i64())
2629 .unwrap_or(0) as i32;
2630 let loc_col = params
2631 .get("location")
2632 .and_then(|v| v.get("column").or_else(|| v.get("columnNumber")))
2633 .and_then(|v| v.as_i64())
2634 .unwrap_or(0) as i32;
2635 let page_guid_owned = params
2636 .get("page")
2637 .and_then(|v| v.get("guid"))
2638 .and_then(|v| v.as_str())
2639 .map(|s| s.to_string());
2640 // Collect arg GUIDs before spawning.
2641 let arg_guids: Vec<String> = params
2642 .get("args")
2643 .and_then(|v| v.as_array())
2644 .map(|arr| {
2645 arr.iter()
2646 .filter_map(|v| {
2647 v.get("guid")
2648 .and_then(|g| g.as_str())
2649 .map(|s| s.to_string())
2650 })
2651 .collect()
2652 })
2653 .unwrap_or_default();
2654 let timestamp = params
2655 .get("timestamp")
2656 .and_then(|v| v.as_f64())
2657 .unwrap_or(0.0);
2658
2659 let connection = self.connection();
2660 let ctx_console = self.console.clone();
2661
2662 tokio::spawn(async move {
2663 use crate::protocol::JSHandle;
2664 use crate::protocol::console_message::{
2665 ConsoleMessage, ConsoleMessageLocation,
2666 };
2667
2668 // Optionally resolve the page back-reference
2669 let page = if let Some(ref guid) = page_guid_owned {
2670 connection.get_typed::<Page>(guid).await.ok()
2671 } else {
2672 None
2673 };
2674
2675 // Resolve JSHandle args from the connection registry.
2676 let args: Vec<std::sync::Arc<JSHandle>> = {
2677 let mut resolved = Vec::with_capacity(arg_guids.len());
2678 for guid in &arg_guids {
2679 if let Ok(handle) = connection.get_typed::<JSHandle>(guid).await {
2680 resolved.push(std::sync::Arc::new(handle));
2681 }
2682 }
2683 resolved
2684 };
2685
2686 let location = ConsoleMessageLocation {
2687 url: loc_url,
2688 line_number: loc_line,
2689 column_number: loc_col,
2690 };
2691
2692 let msg =
2693 ConsoleMessage::new(type_, text, location, page.clone(), args, timestamp);
2694
2695 // Handlers first, then the longest-waiting
2696 // expect_console_message() caller — the same order as
2697 // every other event since the registry migration.
2698 ctx_console.dispatch(msg.clone()).await;
2699
2700 // Forward to page-level handlers
2701 if let Some(p) = page {
2702 p.trigger_console_event(msg).await;
2703 }
2704 });
2705 }
2706 "serviceWorker" => {
2707 // A new service worker was registered in this context.
2708 // Event format: {worker: {guid: "Worker@..."}}
2709 if let Some(worker_guid) = params
2710 .get("worker")
2711 .and_then(|v| v.get("guid"))
2712 .and_then(|v| v.as_str())
2713 {
2714 let connection = self.connection();
2715 let worker_guid_owned = worker_guid.to_string();
2716 let serviceworker_handlers = self.serviceworker_handlers.clone();
2717 let serviceworker_waiters = self.serviceworker_waiters.clone();
2718 let service_workers_list = self.service_workers_list.clone();
2719
2720 tokio::spawn(async move {
2721 let worker: crate::protocol::Worker = match connection
2722 .get_typed::<crate::protocol::Worker>(&worker_guid_owned)
2723 .await
2724 {
2725 Ok(w) => w,
2726 Err(e) => {
2727 tracing::warn!(
2728 "Failed to get Worker object for serviceWorker event: {}",
2729 e
2730 );
2731 return;
2732 }
2733 };
2734
2735 // Track for service_workers() accessor
2736 service_workers_list.lock().unwrap().push(worker.clone());
2737
2738 let handlers = serviceworker_handlers.lock().unwrap().clone();
2739 for handler in handlers {
2740 let worker_clone = worker.clone();
2741 tokio::spawn(async move {
2742 if let Err(e) = handler(worker_clone).await {
2743 tracing::error!("Error in serviceworker handler: {}", e);
2744 }
2745 });
2746 }
2747 // Notify expect_event("serviceworker") waiters
2748 if let Some(tx) = serviceworker_waiters.lock().unwrap().pop() {
2749 let _ = tx.send(worker);
2750 }
2751 });
2752 }
2753 }
2754 "webSocketRoute" => {
2755 // A WebSocket matched a route_web_socket pattern on the context.
2756 // Event format: {webSocketRoute: {guid: "WebSocketRoute@..."}}
2757 if let Some(wsr_guid) = params
2758 .get("webSocketRoute")
2759 .and_then(|v| v.get("guid"))
2760 .and_then(|v| v.as_str())
2761 {
2762 let connection = self.connection();
2763 let wsr_guid_owned = wsr_guid.to_string();
2764 let ws_route_handlers = self.ws_route_handlers.clone();
2765
2766 tokio::spawn(async move {
2767 let route: crate::protocol::WebSocketRoute = match connection
2768 .get_typed::<crate::protocol::WebSocketRoute>(&wsr_guid_owned)
2769 .await
2770 {
2771 Ok(r) => r,
2772 Err(e) => {
2773 tracing::warn!("Failed to get WebSocketRoute object: {}", e);
2774 return;
2775 }
2776 };
2777
2778 let url = route.url().to_string();
2779 let handlers = ws_route_handlers.lock().unwrap().clone();
2780 for entry in handlers.iter().rev() {
2781 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
2782 let handler = entry.handler.clone();
2783 let route_clone = route.clone();
2784 tokio::spawn(async move {
2785 if let Err(e) = handler(route_clone).await {
2786 tracing::error!(
2787 "Error in context webSocketRoute handler: {}",
2788 e
2789 );
2790 }
2791 });
2792 break;
2793 }
2794 }
2795 });
2796 }
2797 }
2798 _ => {
2799 // Other events will be handled in future phases
2800 }
2801 }
2802 }
2803
2804 fn was_collected(&self) -> bool {
2805 self.base.was_collected()
2806 }
2807
2808 fn as_any(&self) -> &dyn Any {
2809 self
2810 }
2811}
2812
2813impl std::fmt::Debug for BrowserContext {
2814 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2815 f.debug_struct("BrowserContext")
2816 .field("guid", &self.guid())
2817 .finish()
2818 }
2819}
2820
2821/// Viewport dimensions for browser context.
2822///
2823/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
2824#[derive(Debug, Clone, Serialize, Deserialize)]
2825pub struct Viewport {
2826 /// Page width in pixels
2827 pub width: u32,
2828 /// Page height in pixels
2829 pub height: u32,
2830}
2831
2832/// Geolocation coordinates.
2833///
2834/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
2835#[derive(Debug, Clone, Serialize, Deserialize)]
2836pub struct Geolocation {
2837 /// Latitude between -90 and 90
2838 pub latitude: f64,
2839 /// Longitude between -180 and 180
2840 pub longitude: f64,
2841 /// Optional accuracy in meters (default: 0)
2842 #[serde(skip_serializing_if = "Option::is_none")]
2843 pub accuracy: Option<f64>,
2844}
2845
2846/// Cookie information for storage state.
2847///
2848/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2849#[derive(Debug, Clone, Serialize, Deserialize)]
2850#[serde(rename_all = "camelCase")]
2851#[non_exhaustive]
2852pub struct Cookie {
2853 /// Cookie name
2854 pub name: String,
2855 /// Cookie value
2856 pub value: String,
2857 /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
2858 pub domain: String,
2859 /// Cookie path
2860 pub path: String,
2861 /// Unix timestamp in seconds; -1 for session cookies
2862 pub expires: f64,
2863 /// HTTP-only flag
2864 pub http_only: bool,
2865 /// Secure flag
2866 pub secure: bool,
2867 /// SameSite attribute ("Strict", "Lax", "None")
2868 #[serde(skip_serializing_if = "Option::is_none")]
2869 pub same_site: Option<String>,
2870}
2871
2872impl Cookie {
2873 /// Create a session cookie (no expiry) with the given name and value.
2874 /// Set `domain`+`path` (or serve it for a URL) before adding it.
2875 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
2876 Self {
2877 name: name.into(),
2878 value: value.into(),
2879 domain: String::new(),
2880 path: "/".to_string(),
2881 expires: -1.0,
2882 http_only: false,
2883 secure: false,
2884 same_site: None,
2885 }
2886 }
2887 /// Cookie domain (e.g. "example.com").
2888 pub fn domain(mut self, domain: impl Into<String>) -> Self {
2889 self.domain = domain.into();
2890 self
2891 }
2892 /// Cookie path.
2893 pub fn path(mut self, path: impl Into<String>) -> Self {
2894 self.path = path.into();
2895 self
2896 }
2897 /// Expiry as Unix time in seconds (-1 for a session cookie).
2898 pub fn expires(mut self, expires: f64) -> Self {
2899 self.expires = expires;
2900 self
2901 }
2902 /// Mark the cookie HttpOnly.
2903 pub fn http_only(mut self, http_only: bool) -> Self {
2904 self.http_only = http_only;
2905 self
2906 }
2907 /// Mark the cookie Secure.
2908 pub fn secure(mut self, secure: bool) -> Self {
2909 self.secure = secure;
2910 self
2911 }
2912 /// SameSite attribute ("Strict", "Lax", or "None").
2913 pub fn same_site(mut self, same_site: impl Into<String>) -> Self {
2914 self.same_site = Some(same_site.into());
2915 self
2916 }
2917}
2918
2919/// Local storage item for storage state.
2920///
2921/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2922#[derive(Debug, Clone, Serialize, Deserialize)]
2923#[non_exhaustive]
2924pub struct LocalStorageItem {
2925 /// Storage key
2926 pub name: String,
2927 /// Storage value
2928 pub value: String,
2929}
2930
2931impl LocalStorageItem {
2932 /// A single localStorage key/value pair.
2933 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
2934 Self {
2935 name: name.into(),
2936 value: value.into(),
2937 }
2938 }
2939}
2940
2941/// Origin with local storage items for storage state.
2942///
2943/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2944#[derive(Debug, Clone, Serialize, Deserialize)]
2945#[serde(rename_all = "camelCase")]
2946#[non_exhaustive]
2947pub struct Origin {
2948 /// Origin URL (e.g., `https://example.com`)
2949 pub origin: String,
2950 /// Local storage items for this origin
2951 pub local_storage: Vec<LocalStorageItem>,
2952 /// IndexedDB contents for this origin, as the driver's opaque payload.
2953 ///
2954 /// Populated when the state was captured with
2955 /// [`StorageStateOptions::indexed_db`], and passed back verbatim on
2956 /// restore. Kept as raw JSON rather than modelled: the shape is an
2957 /// implementation detail of the driver's snapshot format, and the only
2958 /// supported operation is carrying it back unchanged.
2959 #[serde(rename = "indexedDB", default, skip_serializing_if = "Option::is_none")]
2960 pub indexed_db: Option<serde_json::Value>,
2961}
2962
2963impl Origin {
2964 /// Storage entries for one origin.
2965 pub fn new(origin: impl Into<String>, local_storage: Vec<LocalStorageItem>) -> Self {
2966 Self {
2967 origin: origin.into(),
2968 local_storage,
2969 indexed_db: None,
2970 }
2971 }
2972}
2973
2974/// Storage state containing cookies and local storage.
2975///
2976/// Used to populate a browser context with saved authentication state,
2977/// enabling session persistence across context instances.
2978///
2979/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
2980#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2981#[non_exhaustive]
2982pub struct StorageState {
2983 /// List of cookies
2984 pub cookies: Vec<Cookie>,
2985 /// List of origins with local storage
2986 pub origins: Vec<Origin>,
2987 /// WebAuthn passkeys held by the context's virtual authenticator.
2988 /// Only populated when the state was captured with
2989 /// [`StorageStateOptions::credentials`]; omitted from the wire when empty
2990 /// so a state captured without them is unchanged.
2991 #[serde(default, skip_serializing_if = "Option::is_none")]
2992 pub credentials: Option<Vec<crate::protocol::VirtualCredential>>,
2993}
2994
2995impl StorageState {
2996 /// Cookies to seed the context with.
2997 pub fn cookies(mut self, cookies: Vec<Cookie>) -> Self {
2998 self.cookies = cookies;
2999 self
3000 }
3001 /// Per-origin storage (localStorage) to seed the context with.
3002 pub fn origins(mut self, origins: Vec<Origin>) -> Self {
3003 self.origins = origins;
3004 self
3005 }
3006 /// WebAuthn passkeys to seed the context's virtual authenticator with.
3007 pub fn credentials(mut self, credentials: Vec<crate::protocol::VirtualCredential>) -> Self {
3008 self.credentials = Some(credentials);
3009 self
3010 }
3011}
3012
3013/// Options for [`BrowserContext::storage_state`].
3014///
3015/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
3016#[derive(Debug, Clone, Default, Serialize)]
3017#[serde(rename_all = "camelCase")]
3018#[non_exhaustive]
3019pub struct StorageStateOptions {
3020 /// Include IndexedDB contents in the captured state.
3021 // camelCase would render this "indexedDb"; the protocol field is
3022 // "indexedDB", and the driver drops unknown parameters silently, so the
3023 // wrong casing is not an error but a no-op.
3024 #[serde(rename = "indexedDB", skip_serializing_if = "Option::is_none")]
3025 pub indexed_db: Option<bool>,
3026 /// Include the virtual authenticator's WebAuthn passkeys.
3027 #[serde(skip_serializing_if = "Option::is_none")]
3028 pub credentials: Option<bool>,
3029}
3030
3031impl StorageStateOptions {
3032 /// Include IndexedDB contents in the captured state.
3033 pub fn indexed_db(mut self, include: bool) -> Self {
3034 self.indexed_db = Some(include);
3035 self
3036 }
3037 /// Include the virtual authenticator's WebAuthn passkeys.
3038 pub fn credentials(mut self, include: bool) -> Self {
3039 self.credentials = Some(include);
3040 self
3041 }
3042}
3043
3044/// Options for filtering which cookies to clear with `BrowserContext::clear_cookies()`.
3045///
3046/// All fields are optional; when provided they act as AND-combined filters.
3047///
3048/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
3049#[derive(Debug, Clone, Default, Serialize)]
3050#[serde(rename_all = "camelCase")]
3051#[non_exhaustive]
3052pub struct ClearCookiesOptions {
3053 /// Filter by cookie name (exact match).
3054 #[serde(skip_serializing_if = "Option::is_none")]
3055 pub name: Option<String>,
3056 /// Filter by cookie domain.
3057 #[serde(skip_serializing_if = "Option::is_none")]
3058 pub domain: Option<String>,
3059 /// Filter by cookie path.
3060 #[serde(skip_serializing_if = "Option::is_none")]
3061 pub path: Option<String>,
3062}
3063
3064impl ClearCookiesOptions {
3065 /// Only clear cookies with this name.
3066 pub fn name(mut self, name: impl Into<String>) -> Self {
3067 self.name = Some(name.into());
3068 self
3069 }
3070 /// Only clear cookies for this domain.
3071 pub fn domain(mut self, domain: impl Into<String>) -> Self {
3072 self.domain = Some(domain.into());
3073 self
3074 }
3075 /// Only clear cookies for this path.
3076 pub fn path(mut self, path: impl Into<String>) -> Self {
3077 self.path = Some(path.into());
3078 self
3079 }
3080}
3081
3082/// Options for `BrowserContext::grant_permissions()`.
3083///
3084/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
3085#[derive(Debug, Clone, Default)]
3086#[non_exhaustive]
3087pub struct GrantPermissionsOptions {
3088 /// Optional origin to restrict the permission grant to.
3089 ///
3090 /// For example `"https://example.com"`.
3091 pub origin: Option<String>,
3092}
3093
3094impl GrantPermissionsOptions {
3095 /// Restrict the grant to the given origin.
3096 pub fn origin(mut self, origin: impl Into<String>) -> Self {
3097 self.origin = Some(origin.into());
3098 self
3099 }
3100}
3101
3102/// Options for recording HAR.
3103///
3104/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
3105#[derive(Debug, Clone, Serialize, Default)]
3106#[serde(rename_all = "camelCase")]
3107#[non_exhaustive]
3108pub struct RecordHar {
3109 /// Path on the filesystem to write the HAR file to.
3110 pub path: String,
3111 /// Optional setting to control whether to omit request content from the HAR.
3112 #[serde(skip_serializing_if = "Option::is_none")]
3113 pub omit_content: Option<bool>,
3114 /// Optional setting to control resource content management.
3115 /// "omit" | "embed" | "attach"
3116 #[serde(skip_serializing_if = "Option::is_none")]
3117 pub content: Option<String>,
3118 /// "full" | "minimal"
3119 #[serde(skip_serializing_if = "Option::is_none")]
3120 pub mode: Option<String>,
3121 /// A glob or regex pattern to filter requests that are stored in the HAR.
3122 #[serde(skip_serializing_if = "Option::is_none")]
3123 pub url_filter: Option<String>,
3124}
3125
3126impl RecordHar {
3127 /// Record a HAR to the given path.
3128 pub fn new(path: impl Into<String>) -> Self {
3129 Self {
3130 path: path.into(),
3131 omit_content: None,
3132 content: None,
3133 mode: None,
3134 url_filter: None,
3135 }
3136 }
3137 /// Omit response bodies from the HAR.
3138 pub fn omit_content(mut self, omit_content: bool) -> Self {
3139 self.omit_content = Some(omit_content);
3140 self
3141 }
3142 /// Content mode ("embed", "attach", or "omit").
3143 pub fn content(mut self, content: impl Into<String>) -> Self {
3144 self.content = Some(content.into());
3145 self
3146 }
3147 /// Recording mode ("full" or "minimal").
3148 pub fn mode(mut self, mode: impl Into<String>) -> Self {
3149 self.mode = Some(mode.into());
3150 self
3151 }
3152 /// Only record requests matching this URL glob.
3153 pub fn url_filter(mut self, url_filter: impl Into<String>) -> Self {
3154 self.url_filter = Some(url_filter.into());
3155 self
3156 }
3157}
3158
3159/// Options for recording video.
3160///
3161/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video>
3162#[derive(Debug, Clone, Serialize, Default)]
3163#[non_exhaustive]
3164pub struct RecordVideo {
3165 /// Path to the directory to put videos into.
3166 pub dir: String,
3167 /// Optional dimensions of the recorded videos.
3168 #[serde(skip_serializing_if = "Option::is_none")]
3169 pub size: Option<Viewport>,
3170}
3171
3172impl RecordVideo {
3173 /// Record videos into the given directory.
3174 pub fn new(dir: impl Into<String>) -> Self {
3175 Self {
3176 dir: dir.into(),
3177 size: None,
3178 }
3179 }
3180 /// Recorded video size.
3181 pub fn size(mut self, size: Viewport) -> Self {
3182 self.size = Some(size);
3183 self
3184 }
3185}
3186
3187/// Options for creating a new browser context.
3188///
3189/// Controls how downloads are handled in a [`BrowserContext`].
3190///
3191/// See the `accept_downloads` field of [`BrowserContextOptions`].
3192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3193#[non_exhaustive]
3194pub enum AcceptDownloads {
3195 /// Allow and capture downloads via the `download` event.
3196 #[serde(rename = "accept")]
3197 Accept,
3198 /// Block downloads.
3199 #[serde(rename = "deny")]
3200 Deny,
3201 /// Let the browser handle downloads natively without routing through Playwright.
3202 #[serde(rename = "internal")]
3203 Internal,
3204}
3205
3206impl From<bool> for AcceptDownloads {
3207 fn from(value: bool) -> Self {
3208 if value { Self::Accept } else { Self::Deny }
3209 }
3210}
3211
3212/// Allows customizing viewport, user agent, locale, timezone, geolocation,
3213/// permissions, and other browser context settings.
3214///
3215/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
3216#[derive(Debug, Clone, Default, Serialize)]
3217#[serde(rename_all = "camelCase")]
3218#[non_exhaustive]
3219pub struct BrowserContextOptions {
3220 /// Sets consistent viewport for all pages in the context.
3221 /// Set to null via `no_viewport(true)` to disable viewport emulation.
3222 #[serde(skip_serializing_if = "Option::is_none")]
3223 pub viewport: Option<Viewport>,
3224
3225 /// Disables viewport emulation when set to true.
3226 /// Note: Playwright's public API calls this `noViewport`, but the protocol
3227 /// expects `noDefaultViewport`. playwright-python applies this transformation
3228 /// in `_prepare_browser_context_params`.
3229 #[serde(skip_serializing_if = "Option::is_none")]
3230 #[serde(rename = "noDefaultViewport")]
3231 pub no_viewport: Option<bool>,
3232
3233 /// Custom user agent string
3234 #[serde(skip_serializing_if = "Option::is_none")]
3235 pub user_agent: Option<String>,
3236
3237 /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
3238 #[serde(skip_serializing_if = "Option::is_none")]
3239 pub locale: Option<String>,
3240
3241 /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
3242 #[serde(skip_serializing_if = "Option::is_none")]
3243 pub timezone_id: Option<String>,
3244
3245 /// Geolocation coordinates
3246 #[serde(skip_serializing_if = "Option::is_none")]
3247 pub geolocation: Option<Geolocation>,
3248
3249 /// List of permissions to grant (e.g., "geolocation", "notifications")
3250 #[serde(skip_serializing_if = "Option::is_none")]
3251 pub permissions: Option<Vec<String>>,
3252
3253 /// Network proxy settings
3254 #[serde(skip_serializing_if = "Option::is_none")]
3255 pub proxy: Option<ProxySettings>,
3256
3257 /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
3258 #[serde(skip_serializing_if = "Option::is_none")]
3259 pub color_scheme: Option<String>,
3260
3261 /// Whether the viewport supports touch events
3262 #[serde(skip_serializing_if = "Option::is_none")]
3263 pub has_touch: Option<bool>,
3264
3265 /// Whether the meta viewport tag is respected
3266 #[serde(skip_serializing_if = "Option::is_none")]
3267 pub is_mobile: Option<bool>,
3268
3269 /// Whether JavaScript is enabled in the context
3270 #[serde(skip_serializing_if = "Option::is_none")]
3271 pub javascript_enabled: Option<bool>,
3272
3273 /// Emulates network being offline
3274 #[serde(skip_serializing_if = "Option::is_none")]
3275 pub offline: Option<bool>,
3276
3277 /// How to handle downloads. See [`AcceptDownloads`] for options.
3278 #[serde(skip_serializing_if = "Option::is_none")]
3279 pub accept_downloads: Option<AcceptDownloads>,
3280
3281 /// Whether to bypass Content-Security-Policy
3282 #[serde(skip_serializing_if = "Option::is_none")]
3283 pub bypass_csp: Option<bool>,
3284
3285 /// Whether to ignore HTTPS errors
3286 #[serde(skip_serializing_if = "Option::is_none")]
3287 pub ignore_https_errors: Option<bool>,
3288
3289 /// Device scale factor (default: 1)
3290 #[serde(skip_serializing_if = "Option::is_none")]
3291 pub device_scale_factor: Option<f64>,
3292
3293 /// Extra HTTP headers to send with every request
3294 #[serde(skip_serializing_if = "Option::is_none")]
3295 pub extra_http_headers: Option<HashMap<String, String>>,
3296
3297 /// Base URL for relative navigation
3298 #[serde(skip_serializing_if = "Option::is_none")]
3299 pub base_url: Option<String>,
3300
3301 /// Storage state to populate the context (cookies, localStorage, sessionStorage).
3302 /// Can be an inline StorageState object or a file path string.
3303 /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
3304 #[serde(skip_serializing_if = "Option::is_none")]
3305 pub storage_state: Option<StorageState>,
3306
3307 /// Storage state file path (alternative to inline storage_state).
3308 /// This is handled by the builder and converted to storage_state during serialization.
3309 #[serde(skip_serializing_if = "Option::is_none")]
3310 pub storage_state_path: Option<String>,
3311
3312 // Launch options (for launch_persistent_context)
3313 /// Additional arguments to pass to browser instance
3314 #[serde(skip_serializing_if = "Option::is_none")]
3315 pub args: Option<Vec<String>>,
3316
3317 /// Browser distribution channel (e.g., "chrome", "msedge")
3318 #[serde(skip_serializing_if = "Option::is_none")]
3319 pub channel: Option<String>,
3320
3321 /// Enable Chromium sandboxing (default: false on Linux)
3322 #[serde(skip_serializing_if = "Option::is_none")]
3323 pub chromium_sandbox: Option<bool>,
3324
3325 /// Auto-open DevTools (deprecated, default: false)
3326 #[serde(skip_serializing_if = "Option::is_none")]
3327 pub devtools: Option<bool>,
3328
3329 /// Directory to save downloads
3330 #[serde(skip_serializing_if = "Option::is_none")]
3331 pub downloads_path: Option<String>,
3332
3333 /// Path to custom browser executable
3334 #[serde(skip_serializing_if = "Option::is_none")]
3335 pub executable_path: Option<String>,
3336
3337 /// Firefox user preferences (Firefox only)
3338 #[serde(skip_serializing_if = "Option::is_none")]
3339 pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
3340
3341 /// Run in headless mode (default: true unless devtools=true)
3342 #[serde(skip_serializing_if = "Option::is_none")]
3343 pub headless: Option<bool>,
3344
3345 /// Filter or disable default browser arguments.
3346 /// When `true`, Playwright does not pass its own default args.
3347 /// When an array, filters out the given default arguments.
3348 ///
3349 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
3350 #[serde(skip_serializing_if = "Option::is_none")]
3351 pub ignore_default_args: Option<IgnoreDefaultArgs>,
3352
3353 /// Slow down operations by N milliseconds
3354 #[serde(skip_serializing_if = "Option::is_none")]
3355 pub slow_mo: Option<f64>,
3356
3357 /// Timeout for browser launch in milliseconds
3358 #[serde(skip_serializing_if = "Option::is_none")]
3359 pub timeout: Option<f64>,
3360
3361 /// Directory to save traces
3362 #[serde(skip_serializing_if = "Option::is_none")]
3363 pub traces_dir: Option<String>,
3364
3365 /// Check if strict selectors mode is enabled
3366 #[serde(skip_serializing_if = "Option::is_none")]
3367 pub strict_selectors: Option<bool>,
3368
3369 /// Emulates 'prefers-reduced-motion' media feature
3370 #[serde(skip_serializing_if = "Option::is_none")]
3371 pub reduced_motion: Option<String>,
3372
3373 /// Emulates 'forced-colors' media feature
3374 #[serde(skip_serializing_if = "Option::is_none")]
3375 pub forced_colors: Option<String>,
3376
3377 /// Whether to allow sites to register Service workers
3378 #[serde(skip_serializing_if = "Option::is_none")]
3379 pub service_workers: Option<String>,
3380
3381 /// Options for recording HAR
3382 #[serde(skip_serializing_if = "Option::is_none")]
3383 pub record_har: Option<RecordHar>,
3384
3385 /// Options for recording video
3386 #[serde(skip_serializing_if = "Option::is_none")]
3387 pub record_video: Option<RecordVideo>,
3388}
3389
3390impl BrowserContextOptions {
3391 /// Creates a new builder for BrowserContextOptions
3392 pub fn builder() -> BrowserContextOptionsBuilder {
3393 BrowserContextOptionsBuilder::default()
3394 }
3395}
3396
3397/// Builder for BrowserContextOptions
3398#[derive(Debug, Clone, Default)]
3399pub struct BrowserContextOptionsBuilder {
3400 viewport: Option<Viewport>,
3401 no_viewport: Option<bool>,
3402 user_agent: Option<String>,
3403 locale: Option<String>,
3404 timezone_id: Option<String>,
3405 geolocation: Option<Geolocation>,
3406 permissions: Option<Vec<String>>,
3407 proxy: Option<ProxySettings>,
3408 color_scheme: Option<String>,
3409 has_touch: Option<bool>,
3410 is_mobile: Option<bool>,
3411 javascript_enabled: Option<bool>,
3412 offline: Option<bool>,
3413 accept_downloads: Option<AcceptDownloads>,
3414 bypass_csp: Option<bool>,
3415 ignore_https_errors: Option<bool>,
3416 device_scale_factor: Option<f64>,
3417 extra_http_headers: Option<HashMap<String, String>>,
3418 base_url: Option<String>,
3419 storage_state: Option<StorageState>,
3420 storage_state_path: Option<String>,
3421 // Launch options
3422 args: Option<Vec<String>>,
3423 channel: Option<String>,
3424 chromium_sandbox: Option<bool>,
3425 devtools: Option<bool>,
3426 downloads_path: Option<String>,
3427 executable_path: Option<String>,
3428 firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
3429 headless: Option<bool>,
3430 ignore_default_args: Option<IgnoreDefaultArgs>,
3431 slow_mo: Option<f64>,
3432 timeout: Option<f64>,
3433 traces_dir: Option<String>,
3434 strict_selectors: Option<bool>,
3435 reduced_motion: Option<String>,
3436 forced_colors: Option<String>,
3437 service_workers: Option<String>,
3438 record_har: Option<RecordHar>,
3439 record_video: Option<RecordVideo>,
3440}
3441
3442impl BrowserContextOptionsBuilder {
3443 /// Sets the viewport dimensions
3444 pub fn viewport(mut self, viewport: Viewport) -> Self {
3445 self.viewport = Some(viewport);
3446 self.no_viewport = None; // Clear no_viewport if setting viewport
3447 self
3448 }
3449
3450 /// Disables viewport emulation
3451 pub fn no_viewport(mut self, no_viewport: bool) -> Self {
3452 self.no_viewport = Some(no_viewport);
3453 if no_viewport {
3454 self.viewport = None; // Clear viewport if setting no_viewport
3455 }
3456 self
3457 }
3458
3459 /// Sets the user agent string
3460 pub fn user_agent(mut self, user_agent: String) -> Self {
3461 self.user_agent = Some(user_agent);
3462 self
3463 }
3464
3465 /// Sets the locale
3466 pub fn locale(mut self, locale: String) -> Self {
3467 self.locale = Some(locale);
3468 self
3469 }
3470
3471 /// Sets the timezone identifier
3472 pub fn timezone_id(mut self, timezone_id: String) -> Self {
3473 self.timezone_id = Some(timezone_id);
3474 self
3475 }
3476
3477 /// Sets the geolocation
3478 pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
3479 self.geolocation = Some(geolocation);
3480 self
3481 }
3482
3483 /// Sets the permissions to grant
3484 pub fn permissions(mut self, permissions: Vec<String>) -> Self {
3485 self.permissions = Some(permissions);
3486 self
3487 }
3488
3489 /// Sets the network proxy settings for this context.
3490 ///
3491 /// This allows routing all network traffic through a proxy server,
3492 /// useful for rotating proxies without creating new browsers.
3493 ///
3494 /// # Example
3495 ///
3496 /// ```no_run
3497 /// use playwright_rs::protocol::{BrowserContextOptions, ProxySettings};
3498 ///
3499 /// let options = BrowserContextOptions::builder()
3500 /// .proxy(
3501 /// ProxySettings::new("http://proxy.example.com:8080")
3502 /// .bypass(".example.com")
3503 /// .username("user")
3504 /// .password("pass"),
3505 /// )
3506 /// .build();
3507 /// ```
3508 ///
3509 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
3510 pub fn proxy(mut self, proxy: ProxySettings) -> Self {
3511 self.proxy = Some(proxy);
3512 self
3513 }
3514
3515 /// Sets the color scheme preference
3516 pub fn color_scheme(mut self, color_scheme: String) -> Self {
3517 self.color_scheme = Some(color_scheme);
3518 self
3519 }
3520
3521 /// Sets whether the viewport supports touch events
3522 pub fn has_touch(mut self, has_touch: bool) -> Self {
3523 self.has_touch = Some(has_touch);
3524 self
3525 }
3526
3527 /// Sets whether this is a mobile viewport
3528 pub fn is_mobile(mut self, is_mobile: bool) -> Self {
3529 self.is_mobile = Some(is_mobile);
3530 self
3531 }
3532
3533 /// Sets whether JavaScript is enabled
3534 pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
3535 self.javascript_enabled = Some(javascript_enabled);
3536 self
3537 }
3538
3539 /// Sets whether to emulate offline network
3540 pub fn offline(mut self, offline: bool) -> Self {
3541 self.offline = Some(offline);
3542 self
3543 }
3544
3545 /// Sets how to handle downloads. Accepts `AcceptDownloads` or `bool`
3546 /// (`true` → `Accept`, `false` → `Deny`).
3547 pub fn accept_downloads(mut self, accept_downloads: impl Into<AcceptDownloads>) -> Self {
3548 self.accept_downloads = Some(accept_downloads.into());
3549 self
3550 }
3551
3552 /// Sets whether to bypass Content-Security-Policy
3553 pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
3554 self.bypass_csp = Some(bypass_csp);
3555 self
3556 }
3557
3558 /// Sets whether to ignore HTTPS errors
3559 pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
3560 self.ignore_https_errors = Some(ignore_https_errors);
3561 self
3562 }
3563
3564 /// Sets the device scale factor
3565 pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
3566 self.device_scale_factor = Some(device_scale_factor);
3567 self
3568 }
3569
3570 /// Sets extra HTTP headers
3571 pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
3572 self.extra_http_headers = Some(extra_http_headers);
3573 self
3574 }
3575
3576 /// Sets the base URL for relative navigation
3577 pub fn base_url(mut self, base_url: String) -> Self {
3578 self.base_url = Some(base_url);
3579 self
3580 }
3581
3582 /// Sets the storage state inline (cookies, localStorage).
3583 ///
3584 /// Populates the browser context with the provided storage state, including
3585 /// cookies and local storage. This is useful for initializing a context with
3586 /// a saved authentication state.
3587 ///
3588 /// Mutually exclusive with `storage_state_path()`.
3589 ///
3590 /// # Example
3591 ///
3592 /// ```rust
3593 /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
3594 ///
3595 /// let storage_state = StorageState::default()
3596 /// .cookies(vec![
3597 /// Cookie::new("session_id", "abc123")
3598 /// .domain(".example.com")
3599 /// .http_only(true)
3600 /// .secure(true)
3601 /// .same_site("Lax"),
3602 /// ])
3603 /// .origins(vec![Origin::new(
3604 /// "https://example.com",
3605 /// vec![LocalStorageItem::new("user_prefs", "{\"theme\":\"dark\"}")],
3606 /// )]);
3607 ///
3608 /// let options = BrowserContextOptions::builder()
3609 /// .storage_state(storage_state)
3610 /// .build();
3611 /// ```
3612 ///
3613 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3614 pub fn storage_state(mut self, storage_state: StorageState) -> Self {
3615 self.storage_state = Some(storage_state);
3616 self.storage_state_path = None; // Clear path if setting inline
3617 self
3618 }
3619
3620 /// Sets the storage state from a file path.
3621 ///
3622 /// The file should contain a JSON representation of StorageState with cookies
3623 /// and origins. This is useful for loading authentication state saved from a
3624 /// previous session.
3625 ///
3626 /// Mutually exclusive with `storage_state()`.
3627 ///
3628 /// # Example
3629 ///
3630 /// ```rust
3631 /// use playwright_rs::protocol::BrowserContextOptions;
3632 ///
3633 /// let options = BrowserContextOptions::builder()
3634 /// .storage_state_path("auth.json".to_string())
3635 /// .build();
3636 /// ```
3637 ///
3638 /// The file should have this format:
3639 /// ```json
3640 /// {
3641 /// "cookies": [{
3642 /// "name": "session_id",
3643 /// "value": "abc123",
3644 /// "domain": ".example.com",
3645 /// "path": "/",
3646 /// "expires": -1,
3647 /// "httpOnly": true,
3648 /// "secure": true,
3649 /// "sameSite": "Lax"
3650 /// }],
3651 /// "origins": [{
3652 /// "origin": "https://example.com",
3653 /// "localStorage": [{
3654 /// "name": "user_prefs",
3655 /// "value": "{\"theme\":\"dark\"}"
3656 /// }]
3657 /// }]
3658 /// }
3659 /// ```
3660 ///
3661 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3662 pub fn storage_state_path(mut self, path: String) -> Self {
3663 self.storage_state_path = Some(path);
3664 self.storage_state = None; // Clear inline if setting path
3665 self
3666 }
3667
3668 /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
3669 pub fn args(mut self, args: Vec<String>) -> Self {
3670 self.args = Some(args);
3671 self
3672 }
3673
3674 /// Sets browser distribution channel (for launch_persistent_context)
3675 pub fn channel(mut self, channel: String) -> Self {
3676 self.channel = Some(channel);
3677 self
3678 }
3679
3680 /// Enables or disables Chromium sandboxing (for launch_persistent_context)
3681 pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
3682 self.chromium_sandbox = Some(enabled);
3683 self
3684 }
3685
3686 /// Auto-open DevTools (for launch_persistent_context)
3687 pub fn devtools(mut self, enabled: bool) -> Self {
3688 self.devtools = Some(enabled);
3689 self
3690 }
3691
3692 /// Sets directory to save downloads (for launch_persistent_context)
3693 pub fn downloads_path(mut self, path: String) -> Self {
3694 self.downloads_path = Some(path);
3695 self
3696 }
3697
3698 /// Sets path to custom browser executable (for launch_persistent_context)
3699 pub fn executable_path(mut self, path: String) -> Self {
3700 self.executable_path = Some(path);
3701 self
3702 }
3703
3704 /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
3705 pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
3706 self.firefox_user_prefs = Some(prefs);
3707 self
3708 }
3709
3710 /// Run in headless mode (for launch_persistent_context)
3711 pub fn headless(mut self, enabled: bool) -> Self {
3712 self.headless = Some(enabled);
3713 self
3714 }
3715
3716 /// Filter or disable default browser arguments (for launch_persistent_context).
3717 ///
3718 /// When `IgnoreDefaultArgs::Bool(true)`, Playwright does not pass its own
3719 /// default arguments and only uses the ones from `args`.
3720 /// When `IgnoreDefaultArgs::Array(vec)`, filters out the given default arguments.
3721 ///
3722 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
3723 pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
3724 self.ignore_default_args = Some(args);
3725 self
3726 }
3727
3728 /// Slow down operations by N milliseconds (for launch_persistent_context)
3729 pub fn slow_mo(mut self, ms: f64) -> Self {
3730 self.slow_mo = Some(ms);
3731 self
3732 }
3733
3734 /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
3735 pub fn timeout(mut self, ms: f64) -> Self {
3736 self.timeout = Some(ms);
3737 self
3738 }
3739
3740 /// Set directory to save traces (for launch_persistent_context)
3741 pub fn traces_dir(mut self, path: String) -> Self {
3742 self.traces_dir = Some(path);
3743 self
3744 }
3745
3746 /// Check if strict selectors mode is enabled
3747 pub fn strict_selectors(mut self, enabled: bool) -> Self {
3748 self.strict_selectors = Some(enabled);
3749 self
3750 }
3751
3752 /// Emulates 'prefers-reduced-motion' media feature
3753 pub fn reduced_motion(mut self, value: String) -> Self {
3754 self.reduced_motion = Some(value);
3755 self
3756 }
3757
3758 /// Emulates 'forced-colors' media feature
3759 pub fn forced_colors(mut self, value: String) -> Self {
3760 self.forced_colors = Some(value);
3761 self
3762 }
3763
3764 /// Whether to allow sites to register Service workers ("allow" | "block")
3765 pub fn service_workers(mut self, value: String) -> Self {
3766 self.service_workers = Some(value);
3767 self
3768 }
3769
3770 /// Sets options for recording HAR
3771 pub fn record_har(mut self, record_har: RecordHar) -> Self {
3772 self.record_har = Some(record_har);
3773 self
3774 }
3775
3776 /// Sets options for recording video
3777 pub fn record_video(mut self, record_video: RecordVideo) -> Self {
3778 self.record_video = Some(record_video);
3779 self
3780 }
3781
3782 /// Builds the BrowserContextOptions
3783 pub fn build(self) -> BrowserContextOptions {
3784 BrowserContextOptions {
3785 viewport: self.viewport,
3786 no_viewport: self.no_viewport,
3787 user_agent: self.user_agent,
3788 locale: self.locale,
3789 timezone_id: self.timezone_id,
3790 geolocation: self.geolocation,
3791 permissions: self.permissions,
3792 proxy: self.proxy,
3793 color_scheme: self.color_scheme,
3794 has_touch: self.has_touch,
3795 is_mobile: self.is_mobile,
3796 javascript_enabled: self.javascript_enabled,
3797 offline: self.offline,
3798 accept_downloads: self.accept_downloads,
3799 bypass_csp: self.bypass_csp,
3800 ignore_https_errors: self.ignore_https_errors,
3801 device_scale_factor: self.device_scale_factor,
3802 extra_http_headers: self.extra_http_headers,
3803 base_url: self.base_url,
3804 storage_state: self.storage_state,
3805 storage_state_path: self.storage_state_path,
3806 // Launch options
3807 args: self.args,
3808 channel: self.channel,
3809 chromium_sandbox: self.chromium_sandbox,
3810 devtools: self.devtools,
3811 downloads_path: self.downloads_path,
3812 executable_path: self.executable_path,
3813 firefox_user_prefs: self.firefox_user_prefs,
3814 headless: self.headless,
3815 ignore_default_args: self.ignore_default_args,
3816 slow_mo: self.slow_mo,
3817 timeout: self.timeout,
3818 traces_dir: self.traces_dir,
3819 strict_selectors: self.strict_selectors,
3820 reduced_motion: self.reduced_motion,
3821 forced_colors: self.forced_colors,
3822 service_workers: self.service_workers,
3823 record_har: self.record_har,
3824 record_video: self.record_video,
3825 }
3826 }
3827}
3828
3829/// Extracts timing data from a Response object's initializer, patching in
3830/// `responseEnd` from the event's `responseEndTiming` if available.
3831async fn extract_timing(
3832 connection: &std::sync::Arc<dyn crate::server::connection::ConnectionLike>,
3833 response_guid: Option<String>,
3834 response_end_timing: Option<f64>,
3835) -> Option<serde_json::Value> {
3836 let resp_guid = response_guid?;
3837 let resp_obj: crate::protocol::ResponseObject = connection
3838 .get_typed::<crate::protocol::ResponseObject>(&resp_guid)
3839 .await
3840 .ok()?;
3841 let mut timing = resp_obj.initializer().get("timing")?.clone();
3842 crate::protocol::ResourceTiming::merge_response_end(&mut timing, response_end_timing);
3843 Some(timing)
3844}
3845
3846#[cfg(test)]
3847mod tests {
3848 use super::*;
3849 use crate::api::launch_options::IgnoreDefaultArgs;
3850
3851 #[test]
3852 fn storage_state_options_serialize_with_protocol_casing() {
3853 // The driver validates "indexedDB" and silently drops unknown keys,
3854 // so serde's camelCase ("indexedDb") would make the flag a no-op
3855 // that reports success.
3856 let opts = StorageStateOptions::default()
3857 .indexed_db(true)
3858 .credentials(true);
3859 let value = serde_json::to_value(&opts).unwrap();
3860 assert_eq!(
3861 value,
3862 serde_json::json!({ "indexedDB": true, "credentials": true })
3863 );
3864 }
3865
3866 #[test]
3867 fn storage_state_round_trips_credentials_content() {
3868 // A captured state's value is in being restorable; this pins that a
3869 // save/load through JSON preserves credential content, not just
3870 // count.
3871 // Exactly the driver's VirtualCredential schema: five required
3872 // string fields.
3873 let json = serde_json::json!({
3874 "cookies": [],
3875 "origins": [],
3876 "credentials": [{
3877 "id": "Y3JlZA",
3878 "rpId": "example.com",
3879 "userHandle": "dXNlcg",
3880 "privateKey": "cGtleQ",
3881 "publicKey": "cHVi"
3882 }]
3883 });
3884 let state: StorageState = serde_json::from_value(json.clone()).unwrap();
3885 let back = serde_json::to_value(&state).unwrap();
3886 assert_eq!(back["credentials"], json["credentials"]);
3887 }
3888
3889 #[test]
3890 fn origin_round_trips_indexed_db_payload_verbatim() {
3891 // The payload is the driver's opaque snapshot format; the contract
3892 // is carrying it back unchanged, under the protocol's exact casing.
3893 let json = serde_json::json!({
3894 "origin": "https://example.com",
3895 "localStorage": [],
3896 "indexedDB": [{"name": "db", "version": 1, "stores": []}]
3897 });
3898 let origin: Origin = serde_json::from_value(json.clone()).unwrap();
3899 let back = serde_json::to_value(&origin).unwrap();
3900 assert_eq!(back["indexedDB"], json["indexedDB"]);
3901 assert!(back.get("indexedDb").is_none());
3902 }
3903
3904 #[test]
3905 fn test_browser_context_options_ignore_default_args_bool_serialization() {
3906 let options = BrowserContextOptions::builder()
3907 .ignore_default_args(IgnoreDefaultArgs::Bool(true))
3908 .build();
3909
3910 let value = serde_json::to_value(&options).unwrap();
3911 assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(true));
3912 }
3913
3914 #[test]
3915 fn test_browser_context_options_ignore_default_args_array_serialization() {
3916 let options = BrowserContextOptions::builder()
3917 .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]))
3918 .build();
3919
3920 let value = serde_json::to_value(&options).unwrap();
3921 assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(["--foo"]));
3922 }
3923
3924 #[test]
3925 fn test_browser_context_options_ignore_default_args_absent() {
3926 let options = BrowserContextOptions::builder().build();
3927
3928 let value = serde_json::to_value(&options).unwrap();
3929 assert!(value.get("ignoreDefaultArgs").is_none());
3930 }
3931
3932 #[test]
3933 fn test_accept_downloads_serializes_as_protocol_string() {
3934 for (variant, expected) in [
3935 (AcceptDownloads::Accept, "accept"),
3936 (AcceptDownloads::Deny, "deny"),
3937 (AcceptDownloads::Internal, "internal"),
3938 ] {
3939 let options = BrowserContextOptions::builder()
3940 .accept_downloads(variant)
3941 .build();
3942 let value = serde_json::to_value(&options).unwrap();
3943 assert_eq!(value["acceptDownloads"], serde_json::json!(expected));
3944 }
3945 }
3946
3947 #[test]
3948 fn test_accept_downloads_bool_compatibility() {
3949 let opts = BrowserContextOptions::builder()
3950 .accept_downloads(true)
3951 .build();
3952 assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Accept));
3953
3954 let opts = BrowserContextOptions::builder()
3955 .accept_downloads(false)
3956 .build();
3957 assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Deny));
3958 }
3959}