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