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