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