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(&self) -> Result<StorageState> {
736 let response: StorageState = self
737 .channel()
738 .send("storageState", serde_json::json!({}))
739 .await?;
740 Ok(response)
741 }
742
743 /// Sets storage state (cookies and local storage) for this browser context in-place.
744 ///
745 /// Clears all existing cookies, then adds cookies from `state.cookies`. For each
746 /// origin in `state.origins`, a temporary page is opened to that origin and its
747 /// `localStorage` is restored via JS evaluation, then the page is closed.
748 ///
749 /// This mirrors `browserContext.setStorageState()` from the JS/Python Playwright
750 /// APIs. It is useful for restoring authentication state without recreating the
751 /// context.
752 ///
753 /// # Example
754 ///
755 /// ```no_run
756 /// # use playwright_rs::Playwright;
757 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
758 /// # let pw = Playwright::launch().await?;
759 /// # let browser = pw.chromium().launch().await?;
760 /// # let context = browser.new_context().await?;
761 /// use playwright_rs::protocol::{Cookie, StorageState};
762 ///
763 /// // Restore session cookie
764 /// let state = StorageState::default().cookies(vec![
765 /// Cookie::new("session", "token123")
766 /// .domain("example.com")
767 /// .path("/")
768 /// .http_only(true)
769 /// .secure(true)
770 /// .same_site("Lax"),
771 /// ]);
772 /// context.set_storage_state(state).await?;
773 /// # Ok(())
774 /// # }
775 /// ```
776 ///
777 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-storage-state>
778 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
779 pub async fn set_storage_state(&self, state: StorageState) -> Result<()> {
780 // Step 1: Clear all existing cookies
781 self.clear_cookies(None).await?;
782
783 // Step 2: Add cookies from the new state
784 if !state.cookies.is_empty() {
785 self.add_cookies(&state.cookies).await?;
786 }
787
788 // Step 3: Restore localStorage for each origin via a temporary page
789 if !state.origins.is_empty() {
790 let page = self.new_page().await?;
791 let result: Result<()> = async {
792 for origin in &state.origins {
793 // Navigate the page to the origin so localStorage is in scope
794 let _ = page.goto(&origin.origin, None).await;
795
796 // Restore localStorage entries using JS evaluation
797 if !origin.local_storage.is_empty() {
798 let items_json = serde_json::to_string(&origin.local_storage)
799 .map_err(|e| Error::ProtocolError(format!("Failed to serialize localStorage items: {}", e)))?;
800 let items_value: serde_json::Value = serde_json::from_str(&items_json)
801 .map_err(|e| Error::ProtocolError(format!("Failed to parse localStorage items: {}", e)))?;
802 let script = "items => { localStorage.clear(); for (const {name, value} of items) localStorage.setItem(name, value); }";
803 page.evaluate::<serde_json::Value, ()>(script, Some(&items_value)).await?;
804 }
805 }
806 Ok(())
807 }
808 .await;
809 page.close().await?;
810 result?;
811 }
812
813 Ok(())
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, false, callback).await
1805 }
1806
1807 /// Exposes a Rust function to every page in this browser context as
1808 /// `window[name]` in JavaScript, with `needsHandle: true`.
1809 ///
1810 /// Identical to [`expose_function`](Self::expose_function) but the Playwright
1811 /// server passes the first argument as a `JSHandle` object rather than a plain
1812 /// value. Use this when the JS caller passes complex objects that you want to
1813 /// inspect on the Rust side.
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, true, callback).await
1834 }
1835
1836 /// Internal implementation shared by expose_function and expose_binding.
1837 ///
1838 /// Both `expose_function` and `expose_binding` use `needsHandle: false` because
1839 /// the current implementation does not support JSHandle objects. Using
1840 /// `needsHandle: true` would cause the Playwright server to wrap the first
1841 /// argument as a `JSHandle`, which requires a JSHandle protocol object that
1842 /// is not yet implemented.
1843 async fn expose_binding_internal<F, Fut>(
1844 &self,
1845 name: &str,
1846 _needs_handle: bool,
1847 callback: F,
1848 ) -> Result<()>
1849 where
1850 F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
1851 Fut: Future<Output = serde_json::Value> + Send + 'static,
1852 {
1853 // Wrap callback with type erasure
1854 let callback: BindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
1855 Box::pin(callback(args)) as BindingCallbackFuture
1856 });
1857
1858 // Store the callback before sending the RPC so that a race-condition
1859 // where a bindingCall arrives before we finish registering is avoided.
1860 self.binding_callbacks
1861 .lock()
1862 .unwrap()
1863 .insert(name.to_string(), callback);
1864
1865 // Tell the Playwright server to inject window[name] into every page.
1866 // Always use needsHandle: false — see note above.
1867 self.channel()
1868 .send_no_result(
1869 "exposeBinding",
1870 serde_json::json!({ "name": name, "needsHandle": false }),
1871 )
1872 .await
1873 }
1874
1875 /// Waits for a new page to be created in this browser context.
1876 ///
1877 /// Creates a one-shot waiter that resolves when the next `page` event fires.
1878 /// The waiter **must** be created before the action that triggers the new page
1879 /// (e.g. `new_page()` or a user action that opens a popup) to avoid a race
1880 /// condition.
1881 ///
1882 /// # Arguments
1883 ///
1884 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1885 ///
1886 /// # Errors
1887 ///
1888 /// Returns [`crate::error::Error::Timeout`] if no page is created within the timeout.
1889 ///
1890 /// # Example
1891 ///
1892 /// ```no_run
1893 /// # use playwright_rs::Playwright;
1894 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1895 /// # let pw = Playwright::launch().await?;
1896 /// # let browser = pw.chromium().launch().await?;
1897 /// # let context = browser.new_context().await?;
1898 /// // Set up the waiter BEFORE the triggering action
1899 /// let waiter = context.expect_page(None).await?;
1900 /// let _page = context.new_page().await?;
1901 /// let new_page = waiter.wait().await?;
1902 /// # Ok(())
1903 /// # }
1904 /// ```
1905 ///
1906 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1907 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1908 pub async fn expect_page(&self, timeout: Option<f64>) -> Result<EventWaiter<Page>> {
1909 let (tx, rx) = oneshot::channel();
1910 self.page_waiters.lock().unwrap().push(tx);
1911 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1912 }
1913
1914 /// Waits for this browser context to be closed.
1915 ///
1916 /// Creates a one-shot waiter that resolves when the `close` event fires.
1917 /// The waiter **must** be created before the action that closes the context
1918 /// to avoid a race condition.
1919 ///
1920 /// # Arguments
1921 ///
1922 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1923 ///
1924 /// # Errors
1925 ///
1926 /// Returns [`crate::error::Error::Timeout`] if the context is not closed within the timeout.
1927 ///
1928 /// # Example
1929 ///
1930 /// ```no_run
1931 /// # use playwright_rs::Playwright;
1932 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1933 /// # let pw = Playwright::launch().await?;
1934 /// # let browser = pw.chromium().launch().await?;
1935 /// # let context = browser.new_context().await?;
1936 /// // Set up the waiter BEFORE closing
1937 /// let waiter = context.expect_close(None).await?;
1938 /// context.close().await?;
1939 /// waiter.wait().await?;
1940 /// # Ok(())
1941 /// # }
1942 /// ```
1943 ///
1944 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1945 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1946 pub async fn expect_close(&self, timeout: Option<f64>) -> Result<EventWaiter<()>> {
1947 let (tx, rx) = oneshot::channel();
1948 self.close_waiters.lock().unwrap().push(tx);
1949 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1950 }
1951
1952 /// Waits for a console message from any page in this context.
1953 ///
1954 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-event-console>
1955 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1956 pub async fn expect_console_message(
1957 &self,
1958 timeout: Option<f64>,
1959 ) -> Result<EventWaiter<crate::protocol::ConsoleMessage>> {
1960 let needs_subscription = self.console_handlers.lock().unwrap().is_empty()
1961 && self.console_waiters.lock().unwrap().is_empty();
1962 if needs_subscription {
1963 _ = self.channel().update_subscription("console", true).await;
1964 }
1965 let (tx, rx) = oneshot::channel();
1966 self.console_waiters.lock().unwrap().push(tx);
1967 Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
1968 }
1969
1970 /// Waits for the given event to fire and returns a typed `EventValue`.
1971 ///
1972 /// This is the generic version of the specific `expect_*` methods. It matches
1973 /// the playwright-python / playwright-js `context.expect_event(event_name)` API.
1974 ///
1975 /// The waiter **must** be created before the action that triggers the event.
1976 ///
1977 /// # Supported event names
1978 ///
1979 /// `"page"`, `"close"`, `"console"`, `"request"`, `"response"`,
1980 /// `"weberror"`, `"serviceworker"`
1981 ///
1982 /// # Arguments
1983 ///
1984 /// * `event` - Event name (case-sensitive, matches Playwright protocol names).
1985 /// * `timeout` - Timeout in milliseconds. Defaults to 30 000 ms if `None`.
1986 ///
1987 /// # Errors
1988 ///
1989 /// Returns [`crate::error::Error::InvalidArgument`] for unknown event names.
1990 /// Returns [`crate::error::Error::Timeout`] if the event does not fire within the timeout.
1991 ///
1992 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-wait-for-event>
1993 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
1994 pub async fn expect_event(
1995 &self,
1996 event: &str,
1997 timeout: Option<f64>,
1998 ) -> crate::error::Result<EventWaiter<crate::protocol::EventValue>> {
1999 use crate::protocol::EventValue;
2000 use tokio::sync::oneshot;
2001
2002 let timeout_ms = timeout.or(Some(30_000.0));
2003
2004 match event {
2005 "page" => {
2006 let (tx, rx) = oneshot::channel::<EventValue>();
2007 let (inner_tx, inner_rx) = oneshot::channel::<Page>();
2008 self.page_waiters.lock().unwrap().push(inner_tx);
2009
2010 tokio::spawn(async move {
2011 if let Ok(v) = inner_rx.await {
2012 let _ = tx.send(EventValue::Page(v));
2013 }
2014 });
2015
2016 Ok(EventWaiter::new(rx, timeout_ms))
2017 }
2018
2019 "close" => {
2020 let (tx, rx) = oneshot::channel::<EventValue>();
2021 let (inner_tx, inner_rx) = oneshot::channel::<()>();
2022 self.close_waiters.lock().unwrap().push(inner_tx);
2023
2024 tokio::spawn(async move {
2025 if inner_rx.await.is_ok() {
2026 let _ = tx.send(EventValue::Close);
2027 }
2028 });
2029
2030 Ok(EventWaiter::new(rx, timeout_ms))
2031 }
2032
2033 "console" => {
2034 let (tx, rx) = oneshot::channel::<EventValue>();
2035 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::ConsoleMessage>();
2036
2037 let needs_subscription = self.console_handlers.lock().unwrap().is_empty()
2038 && self.console_waiters.lock().unwrap().is_empty();
2039 if needs_subscription {
2040 _ = self.channel().update_subscription("console", true).await;
2041 }
2042 self.console_waiters.lock().unwrap().push(inner_tx);
2043
2044 tokio::spawn(async move {
2045 if let Ok(v) = inner_rx.await {
2046 let _ = tx.send(EventValue::ConsoleMessage(v));
2047 }
2048 });
2049
2050 Ok(EventWaiter::new(rx, timeout_ms))
2051 }
2052
2053 "request" => {
2054 let (tx, rx) = oneshot::channel::<EventValue>();
2055 let (inner_tx, inner_rx) = oneshot::channel::<Request>();
2056
2057 let needs_subscription = {
2058 let handlers = self.request_handlers.lock().unwrap();
2059 let waiters = self.request_waiters.lock().unwrap();
2060 handlers.is_empty() && waiters.is_empty()
2061 };
2062 if needs_subscription {
2063 _ = self.channel().update_subscription("request", true).await;
2064 }
2065 self.request_waiters.lock().unwrap().push(inner_tx);
2066
2067 tokio::spawn(async move {
2068 if let Ok(v) = inner_rx.await {
2069 let _ = tx.send(EventValue::Request(v));
2070 }
2071 });
2072
2073 Ok(EventWaiter::new(rx, timeout_ms))
2074 }
2075
2076 "response" => {
2077 let (tx, rx) = oneshot::channel::<EventValue>();
2078 let (inner_tx, inner_rx) = oneshot::channel::<ResponseObject>();
2079
2080 let needs_subscription = {
2081 let handlers = self.response_handlers.lock().unwrap();
2082 let waiters = self.response_waiters.lock().unwrap();
2083 handlers.is_empty() && waiters.is_empty()
2084 };
2085 if needs_subscription {
2086 _ = self.channel().update_subscription("response", true).await;
2087 }
2088 self.response_waiters.lock().unwrap().push(inner_tx);
2089
2090 tokio::spawn(async move {
2091 if let Ok(v) = inner_rx.await {
2092 let _ = tx.send(EventValue::Response(v));
2093 }
2094 });
2095
2096 Ok(EventWaiter::new(rx, timeout_ms))
2097 }
2098
2099 "weberror" => {
2100 let (tx, rx) = oneshot::channel::<EventValue>();
2101 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::WebError>();
2102 self.weberror_waiters.lock().unwrap().push(inner_tx);
2103
2104 tokio::spawn(async move {
2105 if let Ok(v) = inner_rx.await {
2106 let _ = tx.send(EventValue::WebError(v));
2107 }
2108 });
2109
2110 Ok(EventWaiter::new(rx, timeout_ms))
2111 }
2112
2113 "serviceworker" => {
2114 let (tx, rx) = oneshot::channel::<EventValue>();
2115 let (inner_tx, inner_rx) = oneshot::channel::<crate::protocol::Worker>();
2116 self.serviceworker_waiters.lock().unwrap().push(inner_tx);
2117
2118 tokio::spawn(async move {
2119 if let Ok(v) = inner_rx.await {
2120 let _ = tx.send(EventValue::Worker(v));
2121 }
2122 });
2123
2124 Ok(EventWaiter::new(rx, timeout_ms))
2125 }
2126
2127 other => Err(crate::error::Error::InvalidArgument(format!(
2128 "Unknown event name '{}'. Supported: page, close, console, request, response, \
2129 weberror, serviceworker",
2130 other
2131 ))),
2132 }
2133 }
2134
2135 /// Intercepts WebSocket connections matching the given URL pattern for all pages in this context.
2136 ///
2137 /// When a WebSocket connection from any page in this context matches `url`,
2138 /// the `handler` is called with a [`WebSocketRoute`](crate::protocol::WebSocketRoute) object.
2139 /// The handler must call [`connect_to_server`](crate::protocol::WebSocketRoute::connect_to_server)
2140 /// to forward the connection to the real server, or
2141 /// [`close`](crate::protocol::WebSocketRoute::close) to terminate it.
2142 ///
2143 /// # Arguments
2144 ///
2145 /// * `url` — URL glob pattern (e.g. `"ws://**"` or `"wss://example.com/ws"`).
2146 /// * `handler` — Async closure receiving a `WebSocketRoute`.
2147 ///
2148 /// # Errors
2149 ///
2150 /// Returns an error if the RPC call to enable interception fails.
2151 ///
2152 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route-web-socket>
2153 #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %url))]
2154 pub async fn route_web_socket<F, Fut>(&self, url: &str, handler: F) -> Result<()>
2155 where
2156 F: Fn(crate::protocol::WebSocketRoute) -> Fut + Send + Sync + 'static,
2157 Fut: Future<Output = Result<()>> + Send + 'static,
2158 {
2159 let handler = Arc::new(
2160 move |route: crate::protocol::WebSocketRoute| -> WsRouteHandlerFuture {
2161 Box::pin(handler(route))
2162 },
2163 );
2164
2165 self.ws_route_handlers
2166 .lock()
2167 .unwrap()
2168 .push(ContextWsRouteHandlerEntry {
2169 pattern: url.to_string(),
2170 handler,
2171 });
2172
2173 self.enable_ws_interception().await
2174 }
2175
2176 /// Updates WebSocket interception patterns for this context.
2177 async fn enable_ws_interception(&self) -> Result<()> {
2178 let patterns: Vec<serde_json::Value> = self
2179 .ws_route_handlers
2180 .lock()
2181 .unwrap()
2182 .iter()
2183 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
2184 .collect();
2185
2186 self.channel()
2187 .send_no_result(
2188 "setWebSocketInterceptionPatterns",
2189 serde_json::json!({ "patterns": patterns }),
2190 )
2191 .await
2192 }
2193
2194 /// Updates network interception patterns for this context
2195 async fn enable_network_interception(&self) -> Result<()> {
2196 let patterns: Vec<serde_json::Value> = self
2197 .route_handlers
2198 .lock()
2199 .unwrap()
2200 .iter()
2201 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
2202 .collect();
2203
2204 self.channel()
2205 .send_no_result(
2206 "setNetworkInterceptionPatterns",
2207 serde_json::json!({ "patterns": patterns }),
2208 )
2209 .await
2210 }
2211
2212 /// Deserializes binding call arguments from Playwright's protocol format.
2213 ///
2214 /// The `args` field in the BindingCall initializer is a JSON array where each
2215 /// element is in `serialize_argument` format: `{"value": <tagged>, "handles": []}`.
2216 /// This helper extracts the inner "value" from each entry and parses it.
2217 ///
2218 /// This is `pub` so that `Page::on_event("bindingCall")` can reuse it without
2219 /// duplicating the deserialization logic.
2220 pub fn deserialize_binding_args_pub(raw_args: &Value) -> Vec<Value> {
2221 Self::deserialize_binding_args(raw_args)
2222 }
2223
2224 fn deserialize_binding_args(raw_args: &Value) -> Vec<Value> {
2225 let Some(arr) = raw_args.as_array() else {
2226 return vec![];
2227 };
2228
2229 arr.iter()
2230 .map(|arg| {
2231 // Each arg is a direct Playwright type-tagged value, e.g. {"n": 3} or {"s": "hello"}
2232 // (NOT wrapped in {"value": ..., "handles": []} — that format is only for evaluate args)
2233 crate::protocol::evaluate_conversion::parse_value(arg, None)
2234 })
2235 .collect()
2236 }
2237
2238 /// Handles a route event from the protocol
2239 async fn on_route_event(route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>, route: Route) {
2240 let handlers = route_handlers.lock().unwrap().clone();
2241 let url = route.request().url().to_string();
2242
2243 for entry in handlers.iter().rev() {
2244 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
2245 let handler = entry.handler.clone();
2246 if let Err(e) = handler(route.clone()).await {
2247 tracing::warn!("Context route handler error: {}", e);
2248 break;
2249 }
2250 if !route.was_handled() {
2251 continue;
2252 }
2253 break;
2254 }
2255 }
2256 }
2257
2258 fn dispatch_request_event(&self, method: &str, params: Value) {
2259 if let Some(request_guid) = params
2260 .get("request")
2261 .and_then(|v| v.get("guid"))
2262 .and_then(|v| v.as_str())
2263 {
2264 let connection = self.connection();
2265 let request_guid_owned = request_guid.to_owned();
2266 let page_guid_owned = params
2267 .get("page")
2268 .and_then(|v| v.get("guid"))
2269 .and_then(|v| v.as_str())
2270 .map(|v| v.to_owned());
2271 // Extract failureText for requestFailed events
2272 let failure_text = params
2273 .get("failureText")
2274 .and_then(|v| v.as_str())
2275 .map(|s| s.to_owned());
2276 // Extract response GUID for requestFinished events (to read timing)
2277 let response_guid_owned = params
2278 .get("response")
2279 .and_then(|v| v.get("guid"))
2280 .and_then(|v| v.as_str())
2281 .map(|s| s.to_owned());
2282 // Extract responseEndTiming from requestFinished event params
2283 let response_end_timing = params.get("responseEndTiming").and_then(|v| v.as_f64());
2284 let method = method.to_owned();
2285 // Clone context-level handler vecs for use in spawn
2286 let ctx_request_handlers = self.request_handlers.clone();
2287 let ctx_request_finished_handlers = self.request_finished_handlers.clone();
2288 let ctx_request_failed_handlers = self.request_failed_handlers.clone();
2289 let ctx_request_waiters = self.request_waiters.clone();
2290 tokio::spawn(async move {
2291 let request: Request =
2292 match connection.get_typed::<Request>(&request_guid_owned).await {
2293 Ok(r) => r,
2294 Err(_) => return,
2295 };
2296
2297 // Set failure text on the request before dispatching to handlers
2298 if let Some(text) = failure_text {
2299 request.set_failure_text(text);
2300 }
2301
2302 // For requestFinished, extract timing from the Response object's initializer
2303 if method == "requestFinished"
2304 && let Some(timing) =
2305 extract_timing(&connection, response_guid_owned, response_end_timing).await
2306 {
2307 request.set_timing(timing);
2308 }
2309
2310 // Dispatch to context-level handlers first (matching playwright-python behavior)
2311 let ctx_handlers = match method.as_str() {
2312 "request" => ctx_request_handlers.lock().unwrap().clone(),
2313 "requestFinished" => ctx_request_finished_handlers.lock().unwrap().clone(),
2314 "requestFailed" => ctx_request_failed_handlers.lock().unwrap().clone(),
2315 _ => vec![],
2316 };
2317 for handler in ctx_handlers {
2318 if let Err(e) = handler(request.clone()).await {
2319 tracing::warn!("Context {} handler error: {}", method, e);
2320 }
2321 }
2322
2323 // Notify expect_event("request") waiters (only for "request" events)
2324 if method == "request"
2325 && let Some(tx) = ctx_request_waiters.lock().unwrap().pop()
2326 {
2327 let _ = tx.send(request.clone());
2328 }
2329
2330 // Then dispatch to page-level handlers
2331 if let Some(page_guid) = page_guid_owned {
2332 let page: Page = match connection.get_typed::<Page>(&page_guid).await {
2333 Ok(p) => p,
2334 Err(_) => return,
2335 };
2336 match method.as_str() {
2337 "request" => page.trigger_request_event(request).await,
2338 "requestFailed" => page.trigger_request_failed_event(request).await,
2339 "requestFinished" => page.trigger_request_finished_event(request).await,
2340 _ => unreachable!("Unreachable method {}", method),
2341 }
2342 }
2343 });
2344 }
2345 }
2346
2347 fn dispatch_response_event(&self, _method: &str, params: Value) {
2348 if let Some(response_guid) = params
2349 .get("response")
2350 .and_then(|v| v.get("guid"))
2351 .and_then(|v| v.as_str())
2352 {
2353 let connection = self.connection();
2354 let response_guid_owned = response_guid.to_owned();
2355 let page_guid_owned = params
2356 .get("page")
2357 .and_then(|v| v.get("guid"))
2358 .and_then(|v| v.as_str())
2359 .map(|v| v.to_owned());
2360 let ctx_response_handlers = self.response_handlers.clone();
2361 let ctx_response_waiters = self.response_waiters.clone();
2362 tokio::spawn(async move {
2363 let response: ResponseObject = match connection
2364 .get_typed::<ResponseObject>(&response_guid_owned)
2365 .await
2366 {
2367 Ok(r) => r,
2368 Err(_) => return,
2369 };
2370
2371 // Dispatch to context-level handlers first (matching playwright-python behavior)
2372 let ctx_handlers = ctx_response_handlers.lock().unwrap().clone();
2373 for handler in ctx_handlers {
2374 if let Err(e) = handler(response.clone()).await {
2375 tracing::warn!("Context response handler error: {}", e);
2376 }
2377 }
2378
2379 // Notify expect_event("response") waiters
2380 if let Some(tx) = ctx_response_waiters.lock().unwrap().pop() {
2381 let _ = tx.send(response.clone());
2382 }
2383
2384 // Then dispatch to page-level handlers
2385 if let Some(page_guid) = page_guid_owned {
2386 let page: Page = match connection.get_typed::<Page>(&page_guid).await {
2387 Ok(p) => p,
2388 Err(_) => return,
2389 };
2390 page.trigger_response_event(response).await;
2391 }
2392 });
2393 }
2394 }
2395}
2396
2397impl ChannelOwner for BrowserContext {
2398 fn guid(&self) -> &str {
2399 self.base.guid()
2400 }
2401
2402 fn type_name(&self) -> &str {
2403 self.base.type_name()
2404 }
2405
2406 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
2407 self.base.parent()
2408 }
2409
2410 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
2411 self.base.connection()
2412 }
2413
2414 fn initializer(&self) -> &Value {
2415 self.base.initializer()
2416 }
2417
2418 fn channel(&self) -> &Channel {
2419 self.base.channel()
2420 }
2421
2422 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
2423 self.base.dispose(reason)
2424 }
2425
2426 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
2427 self.base.adopt(child)
2428 }
2429
2430 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
2431 self.base.add_child(guid, child)
2432 }
2433
2434 fn remove_child(&self, guid: &str) {
2435 self.base.remove_child(guid)
2436 }
2437
2438 fn on_event(&self, method: &str, params: Value) {
2439 match method {
2440 "request" | "requestFailed" | "requestFinished" => {
2441 self.dispatch_request_event(method, params)
2442 }
2443 "response" => self.dispatch_response_event(method, params),
2444 "close" => {
2445 // BrowserContext close event — mark as closed and fire registered close handlers
2446 self.is_closed.store(true, Ordering::Relaxed);
2447 let close_handlers = self.close_handlers.clone();
2448 let close_waiters = self.close_waiters.clone();
2449 tokio::spawn(async move {
2450 let handlers = close_handlers.lock().unwrap().clone();
2451 for handler in handlers {
2452 if let Err(e) = handler().await {
2453 tracing::warn!("Context close handler error: {}", e);
2454 }
2455 }
2456
2457 // Notify all expect_close() waiters
2458 let waiters: Vec<_> = close_waiters.lock().unwrap().drain(..).collect();
2459 for tx in waiters {
2460 let _ = tx.send(());
2461 }
2462 });
2463 }
2464 "page" => {
2465 // Page events are triggered when pages are created, including:
2466 // - Initial page in persistent context with --app mode
2467 // - Popup pages opened through user interactions
2468 // Event format: {page: {guid: "..."}}
2469 if let Some(page_guid) = params
2470 .get("page")
2471 .and_then(|v| v.get("guid"))
2472 .and_then(|v| v.as_str())
2473 {
2474 let connection = self.connection();
2475 let page_guid_owned = page_guid.to_string();
2476 let pages = self.pages.clone();
2477 let page_handlers = self.page_handlers.clone();
2478 let page_waiters = self.page_waiters.clone();
2479 let download_handlers = self.download_handlers.clone();
2480 let frame_attached_handlers = self.frame_attached_handlers.clone();
2481 let frame_detached_handlers = self.frame_detached_handlers.clone();
2482 let frame_navigated_handlers = self.frame_navigated_handlers.clone();
2483 let page_load_handlers = self.page_load_handlers.clone();
2484 let page_close_handlers = self.page_close_handlers.clone();
2485
2486 tokio::spawn(async move {
2487 // Get and downcast the Page object
2488 let page: Page = match connection.get_typed::<Page>(&page_guid_owned).await
2489 {
2490 Ok(p) => p,
2491 Err(_) => return,
2492 };
2493
2494 // Track the page
2495 pages.lock().unwrap().push(page.clone());
2496
2497 // Forward this new page's lifecycle events to any
2498 // context-level handlers already registered.
2499 if !download_handlers.lock().unwrap().is_empty() {
2500 Self::wire_download(&page, download_handlers.clone()).await;
2501 }
2502 if !frame_attached_handlers.lock().unwrap().is_empty() {
2503 Self::wire_frame_attached(&page, frame_attached_handlers.clone()).await;
2504 }
2505 if !frame_detached_handlers.lock().unwrap().is_empty() {
2506 Self::wire_frame_detached(&page, frame_detached_handlers.clone()).await;
2507 }
2508 if !frame_navigated_handlers.lock().unwrap().is_empty() {
2509 Self::wire_frame_navigated(&page, frame_navigated_handlers.clone())
2510 .await;
2511 }
2512 if !page_load_handlers.lock().unwrap().is_empty() {
2513 Self::wire_page_load(&page, page_load_handlers.clone()).await;
2514 }
2515 if !page_close_handlers.lock().unwrap().is_empty() {
2516 Self::wire_page_close(&page, page_close_handlers.clone()).await;
2517 }
2518
2519 // If this page has an opener, dispatch popup event to opener's handlers.
2520 // The opener guid is in the page's initializer: {"opener": {"guid": "..."}}
2521 if let Some(opener_guid) = page
2522 .initializer()
2523 .get("opener")
2524 .and_then(|v| v.get("guid"))
2525 .and_then(|v| v.as_str())
2526 && let Ok(opener) = connection.get_typed::<Page>(opener_guid).await
2527 {
2528 opener.trigger_popup_event(page.clone()).await;
2529 }
2530
2531 // Dispatch to context-level page handlers
2532 let handlers = page_handlers.lock().unwrap().clone();
2533 for handler in handlers {
2534 if let Err(e) = handler(page.clone()).await {
2535 tracing::warn!("Context page handler error: {}", e);
2536 }
2537 }
2538
2539 // Notify the first expect_page() waiter (FIFO order)
2540 if let Some(tx) = page_waiters.lock().unwrap().pop() {
2541 let _ = tx.send(page);
2542 }
2543 });
2544 }
2545 }
2546 "pageError" => {
2547 // pageError event: fired when an uncaught JS exception occurs on a page.
2548 // Event format:
2549 // { "error": { "error": { "message": "...", "name": "...", "stack": "..." } },
2550 // "page": { "guid": "page@..." } }
2551 //
2552 // Dispatch path:
2553 // 1. Construct WebError and fire context-level on_weberror handlers.
2554 // 2. Forward the raw message to the page's on_pageerror handlers.
2555 let message = params
2556 .get("error")
2557 .and_then(|e| e.get("error"))
2558 .and_then(|e| e.get("message"))
2559 .and_then(|m| m.as_str())
2560 .unwrap_or("")
2561 .to_string();
2562
2563 let page_guid_owned = params
2564 .get("page")
2565 .and_then(|v| v.get("guid"))
2566 .and_then(|v| v.as_str())
2567 .map(|s| s.to_string());
2568
2569 let location =
2570 params
2571 .get("location")
2572 .map(|loc| crate::protocol::WebErrorLocation {
2573 url: loc
2574 .get("url")
2575 .and_then(|v| v.as_str())
2576 .unwrap_or("")
2577 .to_string(),
2578 line: loc.get("line").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
2579 column: loc.get("column").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
2580 });
2581
2582 let connection = self.connection();
2583 let weberror_handlers = self.weberror_handlers.clone();
2584 let weberror_waiters = self.weberror_waiters.clone();
2585
2586 tokio::spawn(async move {
2587 // Resolve page (optional — may be None if page already closed)
2588 let page = if let Some(ref guid) = page_guid_owned {
2589 connection.get_typed::<Page>(guid).await.ok()
2590 } else {
2591 None
2592 };
2593
2594 // 1. Dispatch to context-level weberror handlers
2595 let web_error = crate::protocol::WebError::new(
2596 message.clone(),
2597 page.clone(),
2598 location.clone(),
2599 );
2600 let handlers = weberror_handlers.lock().unwrap().clone();
2601 for handler in handlers {
2602 if let Err(e) = handler(web_error.clone()).await {
2603 tracing::warn!("Context weberror handler error: {}", e);
2604 }
2605 }
2606
2607 // Notify expect_event("weberror") waiters
2608 if let Some(tx) = weberror_waiters.lock().unwrap().pop() {
2609 let _ = tx.send(web_error);
2610 }
2611
2612 // 2. Forward to page-level pageerror handlers
2613 if let Some(p) = page {
2614 p.trigger_pageerror_event(message).await;
2615 }
2616 });
2617 }
2618 "dialog" => {
2619 // Dialog events come to BrowserContext.
2620 // Dispatch to context-level handlers first, then forward to the Page.
2621 // Event format: {dialog: {guid: "..."}}
2622 // The Dialog protocol object has the Page as its parent
2623 if let Some(dialog_guid) = params
2624 .get("dialog")
2625 .and_then(|v| v.get("guid"))
2626 .and_then(|v| v.as_str())
2627 {
2628 let connection = self.connection();
2629 let dialog_guid_owned = dialog_guid.to_string();
2630 let dialog_handlers = self.dialog_handlers.clone();
2631
2632 tokio::spawn(async move {
2633 // Get and downcast the Dialog object
2634 let dialog: crate::protocol::Dialog = match connection
2635 .get_typed::<crate::protocol::Dialog>(&dialog_guid_owned)
2636 .await
2637 {
2638 Ok(d) => d,
2639 Err(_) => return,
2640 };
2641
2642 // Dispatch to context-level dialog handlers first
2643 let ctx_handlers = dialog_handlers.lock().unwrap().clone();
2644 for handler in ctx_handlers {
2645 if let Err(e) = handler(dialog.clone()).await {
2646 tracing::warn!("Context dialog handler error: {}", e);
2647 }
2648 }
2649
2650 // Then forward to the Page's dialog handlers
2651 let page: Page =
2652 match crate::server::connection::downcast_parent::<Page>(&dialog) {
2653 Some(p) => p,
2654 None => return,
2655 };
2656
2657 page.trigger_dialog_event(dialog).await;
2658 });
2659 }
2660 }
2661 "bindingCall" => {
2662 // A JS caller invoked an exposed function. Dispatch to the registered
2663 // callback and send the result back via BindingCall::fulfill.
2664 // Event format: {binding: {guid: "..."}}
2665 if let Some(binding_guid) = params
2666 .get("binding")
2667 .and_then(|v| v.get("guid"))
2668 .and_then(|v| v.as_str())
2669 {
2670 let connection = self.connection();
2671 let binding_guid_owned = binding_guid.to_string();
2672 let binding_callbacks = self.binding_callbacks.clone();
2673
2674 tokio::spawn(async move {
2675 let binding_call: crate::protocol::BindingCall = match connection
2676 .get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
2677 .await
2678 {
2679 Ok(bc) => bc,
2680 Err(e) => {
2681 tracing::warn!("Failed to get BindingCall object: {}", e);
2682 return;
2683 }
2684 };
2685
2686 let name = binding_call.name().to_string();
2687
2688 // Look up the registered callback
2689 let callback = {
2690 let callbacks = binding_callbacks.lock().unwrap();
2691 callbacks.get(&name).cloned()
2692 };
2693
2694 let Some(callback) = callback else {
2695 tracing::warn!("No callback registered for binding '{}'", name);
2696 let _ = binding_call
2697 .reject(&format!("No Rust handler for binding '{name}'"))
2698 .await;
2699 return;
2700 };
2701
2702 // Deserialize the args from Playwright protocol format
2703 let raw_args = binding_call.args();
2704 let args = Self::deserialize_binding_args(raw_args);
2705
2706 // Call the callback and serialize the result
2707 let result_value = callback(args).await;
2708 let serialized =
2709 crate::protocol::evaluate_conversion::serialize_argument(&result_value);
2710
2711 if let Err(e) = binding_call.resolve(serialized).await {
2712 tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
2713 }
2714 });
2715 }
2716 }
2717 "route" => {
2718 // Handle context-level network routing event
2719 if let Some(route_guid) = params
2720 .get("route")
2721 .and_then(|v| v.get("guid"))
2722 .and_then(|v| v.as_str())
2723 {
2724 let connection = self.connection();
2725 let route_guid_owned = route_guid.to_string();
2726 let route_handlers = self.route_handlers.clone();
2727 let request_context_guid = self.request_context_guid.clone();
2728
2729 tokio::spawn(async move {
2730 let route: Route =
2731 match connection.get_typed::<Route>(&route_guid_owned).await {
2732 Ok(r) => r,
2733 Err(e) => {
2734 tracing::warn!("Failed to get route object: {}", e);
2735 return;
2736 }
2737 };
2738
2739 // Set APIRequestContext on the route for fetch() support
2740 if let Some(ref guid) = request_context_guid
2741 && let Ok(api_ctx) =
2742 connection.get_typed::<APIRequestContext>(guid).await
2743 {
2744 route.set_api_request_context(api_ctx);
2745 }
2746
2747 BrowserContext::on_route_event(route_handlers, route).await;
2748 });
2749 }
2750 }
2751 "console" => {
2752 // Console events are sent to BrowserContext.
2753 // Construct ConsoleMessage from params, dispatch to context-level handlers,
2754 // then forward to the Page's on_console handlers.
2755 //
2756 // Event params format:
2757 // {
2758 // type: "log"|"error"|"warning"|...,
2759 // text: "rendered text",
2760 // location: { url: "...", lineNumber: N, columnNumber: N },
2761 // page: { guid: "page@..." },
2762 // args: [ { guid: "JSHandle@..." }, ... ] -- resolved to Arc<JSHandle>
2763 // timestamp: <f64 milliseconds since Unix epoch>
2764 // }
2765 let type_ = params
2766 .get("type")
2767 .and_then(|v| v.as_str())
2768 .unwrap_or("log")
2769 .to_string();
2770 let text = params
2771 .get("text")
2772 .and_then(|v| v.as_str())
2773 .unwrap_or("")
2774 .to_string();
2775 let loc_url = params
2776 .get("location")
2777 .and_then(|v| v.get("url"))
2778 .and_then(|v| v.as_str())
2779 .unwrap_or("")
2780 .to_string();
2781 // 1.60 emits `line`/`column`; older drivers used
2782 // `lineNumber`/`columnNumber` (deprecated, may be removed). Prefer
2783 // the new keys, fall back to the legacy ones.
2784 let loc_line = params
2785 .get("location")
2786 .and_then(|v| v.get("line").or_else(|| v.get("lineNumber")))
2787 .and_then(|v| v.as_i64())
2788 .unwrap_or(0) as i32;
2789 let loc_col = params
2790 .get("location")
2791 .and_then(|v| v.get("column").or_else(|| v.get("columnNumber")))
2792 .and_then(|v| v.as_i64())
2793 .unwrap_or(0) as i32;
2794 let page_guid_owned = params
2795 .get("page")
2796 .and_then(|v| v.get("guid"))
2797 .and_then(|v| v.as_str())
2798 .map(|s| s.to_string());
2799 // Collect arg GUIDs before spawning.
2800 let arg_guids: Vec<String> = params
2801 .get("args")
2802 .and_then(|v| v.as_array())
2803 .map(|arr| {
2804 arr.iter()
2805 .filter_map(|v| {
2806 v.get("guid")
2807 .and_then(|g| g.as_str())
2808 .map(|s| s.to_string())
2809 })
2810 .collect()
2811 })
2812 .unwrap_or_default();
2813 let timestamp = params
2814 .get("timestamp")
2815 .and_then(|v| v.as_f64())
2816 .unwrap_or(0.0);
2817
2818 let connection = self.connection();
2819 let ctx_console_handlers = self.console_handlers.clone();
2820 let ctx_console_waiters = self.console_waiters.clone();
2821
2822 tokio::spawn(async move {
2823 use crate::protocol::JSHandle;
2824 use crate::protocol::console_message::{
2825 ConsoleMessage, ConsoleMessageLocation,
2826 };
2827
2828 // Optionally resolve the page back-reference
2829 let page = if let Some(ref guid) = page_guid_owned {
2830 connection.get_typed::<Page>(guid).await.ok()
2831 } else {
2832 None
2833 };
2834
2835 // Resolve JSHandle args from the connection registry.
2836 let args: Vec<std::sync::Arc<JSHandle>> = {
2837 let mut resolved = Vec::with_capacity(arg_guids.len());
2838 for guid in &arg_guids {
2839 if let Ok(handle) = connection.get_typed::<JSHandle>(guid).await {
2840 resolved.push(std::sync::Arc::new(handle));
2841 }
2842 }
2843 resolved
2844 };
2845
2846 let location = ConsoleMessageLocation {
2847 url: loc_url,
2848 line_number: loc_line,
2849 column_number: loc_col,
2850 };
2851
2852 let msg =
2853 ConsoleMessage::new(type_, text, location, page.clone(), args, timestamp);
2854
2855 // Satisfy the first pending waiter (expect_console_message)
2856 if let Some(tx) = ctx_console_waiters.lock().unwrap().pop() {
2857 let _ = tx.send(msg.clone());
2858 }
2859
2860 // Dispatch to context-level handlers
2861 let ctx_handlers = ctx_console_handlers.lock().unwrap().clone();
2862 for handler in ctx_handlers {
2863 if let Err(e) = handler(msg.clone()).await {
2864 tracing::warn!("Context console handler error: {}", e);
2865 }
2866 }
2867
2868 // Forward to page-level handlers
2869 if let Some(p) = page {
2870 p.trigger_console_event(msg).await;
2871 }
2872 });
2873 }
2874 "serviceWorker" => {
2875 // A new service worker was registered in this context.
2876 // Event format: {worker: {guid: "Worker@..."}}
2877 if let Some(worker_guid) = params
2878 .get("worker")
2879 .and_then(|v| v.get("guid"))
2880 .and_then(|v| v.as_str())
2881 {
2882 let connection = self.connection();
2883 let worker_guid_owned = worker_guid.to_string();
2884 let serviceworker_handlers = self.serviceworker_handlers.clone();
2885 let serviceworker_waiters = self.serviceworker_waiters.clone();
2886 let service_workers_list = self.service_workers_list.clone();
2887
2888 tokio::spawn(async move {
2889 let worker: crate::protocol::Worker = match connection
2890 .get_typed::<crate::protocol::Worker>(&worker_guid_owned)
2891 .await
2892 {
2893 Ok(w) => w,
2894 Err(e) => {
2895 tracing::warn!(
2896 "Failed to get Worker object for serviceWorker event: {}",
2897 e
2898 );
2899 return;
2900 }
2901 };
2902
2903 // Track for service_workers() accessor
2904 service_workers_list.lock().unwrap().push(worker.clone());
2905
2906 let handlers = serviceworker_handlers.lock().unwrap().clone();
2907 for handler in handlers {
2908 let worker_clone = worker.clone();
2909 tokio::spawn(async move {
2910 if let Err(e) = handler(worker_clone).await {
2911 tracing::error!("Error in serviceworker handler: {}", e);
2912 }
2913 });
2914 }
2915 // Notify expect_event("serviceworker") waiters
2916 if let Some(tx) = serviceworker_waiters.lock().unwrap().pop() {
2917 let _ = tx.send(worker);
2918 }
2919 });
2920 }
2921 }
2922 "webSocketRoute" => {
2923 // A WebSocket matched a route_web_socket pattern on the context.
2924 // Event format: {webSocketRoute: {guid: "WebSocketRoute@..."}}
2925 if let Some(wsr_guid) = params
2926 .get("webSocketRoute")
2927 .and_then(|v| v.get("guid"))
2928 .and_then(|v| v.as_str())
2929 {
2930 let connection = self.connection();
2931 let wsr_guid_owned = wsr_guid.to_string();
2932 let ws_route_handlers = self.ws_route_handlers.clone();
2933
2934 tokio::spawn(async move {
2935 let route: crate::protocol::WebSocketRoute = match connection
2936 .get_typed::<crate::protocol::WebSocketRoute>(&wsr_guid_owned)
2937 .await
2938 {
2939 Ok(r) => r,
2940 Err(e) => {
2941 tracing::warn!("Failed to get WebSocketRoute object: {}", e);
2942 return;
2943 }
2944 };
2945
2946 let url = route.url().to_string();
2947 let handlers = ws_route_handlers.lock().unwrap().clone();
2948 for entry in handlers.iter().rev() {
2949 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
2950 let handler = entry.handler.clone();
2951 let route_clone = route.clone();
2952 tokio::spawn(async move {
2953 if let Err(e) = handler(route_clone).await {
2954 tracing::error!(
2955 "Error in context webSocketRoute handler: {}",
2956 e
2957 );
2958 }
2959 });
2960 break;
2961 }
2962 }
2963 });
2964 }
2965 }
2966 _ => {
2967 // Other events will be handled in future phases
2968 }
2969 }
2970 }
2971
2972 fn was_collected(&self) -> bool {
2973 self.base.was_collected()
2974 }
2975
2976 fn as_any(&self) -> &dyn Any {
2977 self
2978 }
2979}
2980
2981impl std::fmt::Debug for BrowserContext {
2982 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2983 f.debug_struct("BrowserContext")
2984 .field("guid", &self.guid())
2985 .finish()
2986 }
2987}
2988
2989/// Viewport dimensions for browser context.
2990///
2991/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
2992#[derive(Debug, Clone, Serialize, Deserialize)]
2993pub struct Viewport {
2994 /// Page width in pixels
2995 pub width: u32,
2996 /// Page height in pixels
2997 pub height: u32,
2998}
2999
3000/// Geolocation coordinates.
3001///
3002/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
3003#[derive(Debug, Clone, Serialize, Deserialize)]
3004pub struct Geolocation {
3005 /// Latitude between -90 and 90
3006 pub latitude: f64,
3007 /// Longitude between -180 and 180
3008 pub longitude: f64,
3009 /// Optional accuracy in meters (default: 0)
3010 #[serde(skip_serializing_if = "Option::is_none")]
3011 pub accuracy: Option<f64>,
3012}
3013
3014/// Cookie information for storage state.
3015///
3016/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3017#[derive(Debug, Clone, Serialize, Deserialize)]
3018#[serde(rename_all = "camelCase")]
3019#[non_exhaustive]
3020pub struct Cookie {
3021 /// Cookie name
3022 pub name: String,
3023 /// Cookie value
3024 pub value: String,
3025 /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
3026 pub domain: String,
3027 /// Cookie path
3028 pub path: String,
3029 /// Unix timestamp in seconds; -1 for session cookies
3030 pub expires: f64,
3031 /// HTTP-only flag
3032 pub http_only: bool,
3033 /// Secure flag
3034 pub secure: bool,
3035 /// SameSite attribute ("Strict", "Lax", "None")
3036 #[serde(skip_serializing_if = "Option::is_none")]
3037 pub same_site: Option<String>,
3038}
3039
3040impl Cookie {
3041 /// Create a session cookie (no expiry) with the given name and value.
3042 /// Set `domain`+`path` (or serve it for a URL) before adding it.
3043 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
3044 Self {
3045 name: name.into(),
3046 value: value.into(),
3047 domain: String::new(),
3048 path: "/".to_string(),
3049 expires: -1.0,
3050 http_only: false,
3051 secure: false,
3052 same_site: None,
3053 }
3054 }
3055 /// Cookie domain (e.g. "example.com").
3056 pub fn domain(mut self, domain: impl Into<String>) -> Self {
3057 self.domain = domain.into();
3058 self
3059 }
3060 /// Cookie path.
3061 pub fn path(mut self, path: impl Into<String>) -> Self {
3062 self.path = path.into();
3063 self
3064 }
3065 /// Expiry as Unix time in seconds (-1 for a session cookie).
3066 pub fn expires(mut self, expires: f64) -> Self {
3067 self.expires = expires;
3068 self
3069 }
3070 /// Mark the cookie HttpOnly.
3071 pub fn http_only(mut self, http_only: bool) -> Self {
3072 self.http_only = http_only;
3073 self
3074 }
3075 /// Mark the cookie Secure.
3076 pub fn secure(mut self, secure: bool) -> Self {
3077 self.secure = secure;
3078 self
3079 }
3080 /// SameSite attribute ("Strict", "Lax", or "None").
3081 pub fn same_site(mut self, same_site: impl Into<String>) -> Self {
3082 self.same_site = Some(same_site.into());
3083 self
3084 }
3085}
3086
3087/// Local storage item for storage state.
3088///
3089/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3090#[derive(Debug, Clone, Serialize, Deserialize)]
3091#[non_exhaustive]
3092pub struct LocalStorageItem {
3093 /// Storage key
3094 pub name: String,
3095 /// Storage value
3096 pub value: String,
3097}
3098
3099impl LocalStorageItem {
3100 /// A single localStorage key/value pair.
3101 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
3102 Self {
3103 name: name.into(),
3104 value: value.into(),
3105 }
3106 }
3107}
3108
3109/// Origin with local storage items for storage state.
3110///
3111/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3112#[derive(Debug, Clone, Serialize, Deserialize)]
3113#[serde(rename_all = "camelCase")]
3114#[non_exhaustive]
3115pub struct Origin {
3116 /// Origin URL (e.g., `https://example.com`)
3117 pub origin: String,
3118 /// Local storage items for this origin
3119 pub local_storage: Vec<LocalStorageItem>,
3120}
3121
3122impl Origin {
3123 /// Storage entries for one origin.
3124 pub fn new(origin: impl Into<String>, local_storage: Vec<LocalStorageItem>) -> Self {
3125 Self {
3126 origin: origin.into(),
3127 local_storage,
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}
3146
3147impl StorageState {
3148 /// Cookies to seed the context with.
3149 pub fn cookies(mut self, cookies: Vec<Cookie>) -> Self {
3150 self.cookies = cookies;
3151 self
3152 }
3153 /// Per-origin storage (localStorage) to seed the context with.
3154 pub fn origins(mut self, origins: Vec<Origin>) -> Self {
3155 self.origins = origins;
3156 self
3157 }
3158}
3159
3160/// Options for filtering which cookies to clear with `BrowserContext::clear_cookies()`.
3161///
3162/// All fields are optional; when provided they act as AND-combined filters.
3163///
3164/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
3165#[derive(Debug, Clone, Default, Serialize)]
3166#[serde(rename_all = "camelCase")]
3167#[non_exhaustive]
3168pub struct ClearCookiesOptions {
3169 /// Filter by cookie name (exact match).
3170 #[serde(skip_serializing_if = "Option::is_none")]
3171 pub name: Option<String>,
3172 /// Filter by cookie domain.
3173 #[serde(skip_serializing_if = "Option::is_none")]
3174 pub domain: Option<String>,
3175 /// Filter by cookie path.
3176 #[serde(skip_serializing_if = "Option::is_none")]
3177 pub path: Option<String>,
3178}
3179
3180impl ClearCookiesOptions {
3181 /// Only clear cookies with this name.
3182 pub fn name(mut self, name: impl Into<String>) -> Self {
3183 self.name = Some(name.into());
3184 self
3185 }
3186 /// Only clear cookies for this domain.
3187 pub fn domain(mut self, domain: impl Into<String>) -> Self {
3188 self.domain = Some(domain.into());
3189 self
3190 }
3191 /// Only clear cookies for this path.
3192 pub fn path(mut self, path: impl Into<String>) -> Self {
3193 self.path = Some(path.into());
3194 self
3195 }
3196}
3197
3198/// Options for `BrowserContext::grant_permissions()`.
3199///
3200/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
3201#[derive(Debug, Clone, Default)]
3202#[non_exhaustive]
3203pub struct GrantPermissionsOptions {
3204 /// Optional origin to restrict the permission grant to.
3205 ///
3206 /// For example `"https://example.com"`.
3207 pub origin: Option<String>,
3208}
3209
3210impl GrantPermissionsOptions {
3211 /// Restrict the grant to the given origin.
3212 pub fn origin(mut self, origin: impl Into<String>) -> Self {
3213 self.origin = Some(origin.into());
3214 self
3215 }
3216}
3217
3218/// Options for recording HAR.
3219///
3220/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
3221#[derive(Debug, Clone, Serialize, Default)]
3222#[serde(rename_all = "camelCase")]
3223#[non_exhaustive]
3224pub struct RecordHar {
3225 /// Path on the filesystem to write the HAR file to.
3226 pub path: String,
3227 /// Optional setting to control whether to omit request content from the HAR.
3228 #[serde(skip_serializing_if = "Option::is_none")]
3229 pub omit_content: Option<bool>,
3230 /// Optional setting to control resource content management.
3231 /// "omit" | "embed" | "attach"
3232 #[serde(skip_serializing_if = "Option::is_none")]
3233 pub content: Option<String>,
3234 /// "full" | "minimal"
3235 #[serde(skip_serializing_if = "Option::is_none")]
3236 pub mode: Option<String>,
3237 /// A glob or regex pattern to filter requests that are stored in the HAR.
3238 #[serde(skip_serializing_if = "Option::is_none")]
3239 pub url_filter: Option<String>,
3240}
3241
3242impl RecordHar {
3243 /// Record a HAR to the given path.
3244 pub fn new(path: impl Into<String>) -> Self {
3245 Self {
3246 path: path.into(),
3247 omit_content: None,
3248 content: None,
3249 mode: None,
3250 url_filter: None,
3251 }
3252 }
3253 /// Omit response bodies from the HAR.
3254 pub fn omit_content(mut self, omit_content: bool) -> Self {
3255 self.omit_content = Some(omit_content);
3256 self
3257 }
3258 /// Content mode ("embed", "attach", or "omit").
3259 pub fn content(mut self, content: impl Into<String>) -> Self {
3260 self.content = Some(content.into());
3261 self
3262 }
3263 /// Recording mode ("full" or "minimal").
3264 pub fn mode(mut self, mode: impl Into<String>) -> Self {
3265 self.mode = Some(mode.into());
3266 self
3267 }
3268 /// Only record requests matching this URL glob.
3269 pub fn url_filter(mut self, url_filter: impl Into<String>) -> Self {
3270 self.url_filter = Some(url_filter.into());
3271 self
3272 }
3273}
3274
3275/// Options for recording video.
3276///
3277/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video>
3278#[derive(Debug, Clone, Serialize, Default)]
3279#[non_exhaustive]
3280pub struct RecordVideo {
3281 /// Path to the directory to put videos into.
3282 pub dir: String,
3283 /// Optional dimensions of the recorded videos.
3284 #[serde(skip_serializing_if = "Option::is_none")]
3285 pub size: Option<Viewport>,
3286}
3287
3288impl RecordVideo {
3289 /// Record videos into the given directory.
3290 pub fn new(dir: impl Into<String>) -> Self {
3291 Self {
3292 dir: dir.into(),
3293 size: None,
3294 }
3295 }
3296 /// Recorded video size.
3297 pub fn size(mut self, size: Viewport) -> Self {
3298 self.size = Some(size);
3299 self
3300 }
3301}
3302
3303/// Options for creating a new browser context.
3304///
3305/// Controls how downloads are handled in a [`BrowserContext`].
3306///
3307/// See the `accept_downloads` field of [`BrowserContextOptions`].
3308#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3309#[non_exhaustive]
3310pub enum AcceptDownloads {
3311 /// Allow and capture downloads via the `download` event.
3312 #[serde(rename = "accept")]
3313 Accept,
3314 /// Block downloads.
3315 #[serde(rename = "deny")]
3316 Deny,
3317 /// Let the browser handle downloads natively without routing through Playwright.
3318 #[serde(rename = "internal")]
3319 Internal,
3320}
3321
3322impl From<bool> for AcceptDownloads {
3323 fn from(value: bool) -> Self {
3324 if value { Self::Accept } else { Self::Deny }
3325 }
3326}
3327
3328/// Allows customizing viewport, user agent, locale, timezone, geolocation,
3329/// permissions, and other browser context settings.
3330///
3331/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
3332#[derive(Debug, Clone, Default, Serialize)]
3333#[serde(rename_all = "camelCase")]
3334#[non_exhaustive]
3335pub struct BrowserContextOptions {
3336 /// Sets consistent viewport for all pages in the context.
3337 /// Set to null via `no_viewport(true)` to disable viewport emulation.
3338 #[serde(skip_serializing_if = "Option::is_none")]
3339 pub viewport: Option<Viewport>,
3340
3341 /// Disables viewport emulation when set to true.
3342 /// Note: Playwright's public API calls this `noViewport`, but the protocol
3343 /// expects `noDefaultViewport`. playwright-python applies this transformation
3344 /// in `_prepare_browser_context_params`.
3345 #[serde(skip_serializing_if = "Option::is_none")]
3346 #[serde(rename = "noDefaultViewport")]
3347 pub no_viewport: Option<bool>,
3348
3349 /// Custom user agent string
3350 #[serde(skip_serializing_if = "Option::is_none")]
3351 pub user_agent: Option<String>,
3352
3353 /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
3354 #[serde(skip_serializing_if = "Option::is_none")]
3355 pub locale: Option<String>,
3356
3357 /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
3358 #[serde(skip_serializing_if = "Option::is_none")]
3359 pub timezone_id: Option<String>,
3360
3361 /// Geolocation coordinates
3362 #[serde(skip_serializing_if = "Option::is_none")]
3363 pub geolocation: Option<Geolocation>,
3364
3365 /// List of permissions to grant (e.g., "geolocation", "notifications")
3366 #[serde(skip_serializing_if = "Option::is_none")]
3367 pub permissions: Option<Vec<String>>,
3368
3369 /// Network proxy settings
3370 #[serde(skip_serializing_if = "Option::is_none")]
3371 pub proxy: Option<ProxySettings>,
3372
3373 /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
3374 #[serde(skip_serializing_if = "Option::is_none")]
3375 pub color_scheme: Option<String>,
3376
3377 /// Whether the viewport supports touch events
3378 #[serde(skip_serializing_if = "Option::is_none")]
3379 pub has_touch: Option<bool>,
3380
3381 /// Whether the meta viewport tag is respected
3382 #[serde(skip_serializing_if = "Option::is_none")]
3383 pub is_mobile: Option<bool>,
3384
3385 /// Whether JavaScript is enabled in the context
3386 #[serde(skip_serializing_if = "Option::is_none")]
3387 pub javascript_enabled: Option<bool>,
3388
3389 /// Emulates network being offline
3390 #[serde(skip_serializing_if = "Option::is_none")]
3391 pub offline: Option<bool>,
3392
3393 /// How to handle downloads. See [`AcceptDownloads`] for options.
3394 #[serde(skip_serializing_if = "Option::is_none")]
3395 pub accept_downloads: Option<AcceptDownloads>,
3396
3397 /// Whether to bypass Content-Security-Policy
3398 #[serde(skip_serializing_if = "Option::is_none")]
3399 pub bypass_csp: Option<bool>,
3400
3401 /// Whether to ignore HTTPS errors
3402 #[serde(skip_serializing_if = "Option::is_none")]
3403 pub ignore_https_errors: Option<bool>,
3404
3405 /// Device scale factor (default: 1)
3406 #[serde(skip_serializing_if = "Option::is_none")]
3407 pub device_scale_factor: Option<f64>,
3408
3409 /// Extra HTTP headers to send with every request
3410 #[serde(skip_serializing_if = "Option::is_none")]
3411 pub extra_http_headers: Option<HashMap<String, String>>,
3412
3413 /// Base URL for relative navigation
3414 #[serde(skip_serializing_if = "Option::is_none")]
3415 pub base_url: Option<String>,
3416
3417 /// Storage state to populate the context (cookies, localStorage, sessionStorage).
3418 /// Can be an inline StorageState object or a file path string.
3419 /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
3420 #[serde(skip_serializing_if = "Option::is_none")]
3421 pub storage_state: Option<StorageState>,
3422
3423 /// Storage state file path (alternative to inline storage_state).
3424 /// This is handled by the builder and converted to storage_state during serialization.
3425 #[serde(skip_serializing_if = "Option::is_none")]
3426 pub storage_state_path: Option<String>,
3427
3428 // Launch options (for launch_persistent_context)
3429 /// Additional arguments to pass to browser instance
3430 #[serde(skip_serializing_if = "Option::is_none")]
3431 pub args: Option<Vec<String>>,
3432
3433 /// Browser distribution channel (e.g., "chrome", "msedge")
3434 #[serde(skip_serializing_if = "Option::is_none")]
3435 pub channel: Option<String>,
3436
3437 /// Enable Chromium sandboxing (default: false on Linux)
3438 #[serde(skip_serializing_if = "Option::is_none")]
3439 pub chromium_sandbox: Option<bool>,
3440
3441 /// Auto-open DevTools (deprecated, default: false)
3442 #[serde(skip_serializing_if = "Option::is_none")]
3443 pub devtools: Option<bool>,
3444
3445 /// Directory to save downloads
3446 #[serde(skip_serializing_if = "Option::is_none")]
3447 pub downloads_path: Option<String>,
3448
3449 /// Path to custom browser executable
3450 #[serde(skip_serializing_if = "Option::is_none")]
3451 pub executable_path: Option<String>,
3452
3453 /// Firefox user preferences (Firefox only)
3454 #[serde(skip_serializing_if = "Option::is_none")]
3455 pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
3456
3457 /// Run in headless mode (default: true unless devtools=true)
3458 #[serde(skip_serializing_if = "Option::is_none")]
3459 pub headless: Option<bool>,
3460
3461 /// Filter or disable default browser arguments.
3462 /// When `true`, Playwright does not pass its own default args.
3463 /// When an array, filters out the given default arguments.
3464 ///
3465 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
3466 #[serde(skip_serializing_if = "Option::is_none")]
3467 pub ignore_default_args: Option<IgnoreDefaultArgs>,
3468
3469 /// Slow down operations by N milliseconds
3470 #[serde(skip_serializing_if = "Option::is_none")]
3471 pub slow_mo: Option<f64>,
3472
3473 /// Timeout for browser launch in milliseconds
3474 #[serde(skip_serializing_if = "Option::is_none")]
3475 pub timeout: Option<f64>,
3476
3477 /// Directory to save traces
3478 #[serde(skip_serializing_if = "Option::is_none")]
3479 pub traces_dir: Option<String>,
3480
3481 /// Check if strict selectors mode is enabled
3482 #[serde(skip_serializing_if = "Option::is_none")]
3483 pub strict_selectors: Option<bool>,
3484
3485 /// Emulates 'prefers-reduced-motion' media feature
3486 #[serde(skip_serializing_if = "Option::is_none")]
3487 pub reduced_motion: Option<String>,
3488
3489 /// Emulates 'forced-colors' media feature
3490 #[serde(skip_serializing_if = "Option::is_none")]
3491 pub forced_colors: Option<String>,
3492
3493 /// Whether to allow sites to register Service workers
3494 #[serde(skip_serializing_if = "Option::is_none")]
3495 pub service_workers: Option<String>,
3496
3497 /// Options for recording HAR
3498 #[serde(skip_serializing_if = "Option::is_none")]
3499 pub record_har: Option<RecordHar>,
3500
3501 /// Options for recording video
3502 #[serde(skip_serializing_if = "Option::is_none")]
3503 pub record_video: Option<RecordVideo>,
3504}
3505
3506impl BrowserContextOptions {
3507 /// Creates a new builder for BrowserContextOptions
3508 pub fn builder() -> BrowserContextOptionsBuilder {
3509 BrowserContextOptionsBuilder::default()
3510 }
3511}
3512
3513/// Builder for BrowserContextOptions
3514#[derive(Debug, Clone, Default)]
3515pub struct BrowserContextOptionsBuilder {
3516 viewport: Option<Viewport>,
3517 no_viewport: Option<bool>,
3518 user_agent: Option<String>,
3519 locale: Option<String>,
3520 timezone_id: Option<String>,
3521 geolocation: Option<Geolocation>,
3522 permissions: Option<Vec<String>>,
3523 proxy: Option<ProxySettings>,
3524 color_scheme: Option<String>,
3525 has_touch: Option<bool>,
3526 is_mobile: Option<bool>,
3527 javascript_enabled: Option<bool>,
3528 offline: Option<bool>,
3529 accept_downloads: Option<AcceptDownloads>,
3530 bypass_csp: Option<bool>,
3531 ignore_https_errors: Option<bool>,
3532 device_scale_factor: Option<f64>,
3533 extra_http_headers: Option<HashMap<String, String>>,
3534 base_url: Option<String>,
3535 storage_state: Option<StorageState>,
3536 storage_state_path: Option<String>,
3537 // Launch options
3538 args: Option<Vec<String>>,
3539 channel: Option<String>,
3540 chromium_sandbox: Option<bool>,
3541 devtools: Option<bool>,
3542 downloads_path: Option<String>,
3543 executable_path: Option<String>,
3544 firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
3545 headless: Option<bool>,
3546 ignore_default_args: Option<IgnoreDefaultArgs>,
3547 slow_mo: Option<f64>,
3548 timeout: Option<f64>,
3549 traces_dir: Option<String>,
3550 strict_selectors: Option<bool>,
3551 reduced_motion: Option<String>,
3552 forced_colors: Option<String>,
3553 service_workers: Option<String>,
3554 record_har: Option<RecordHar>,
3555 record_video: Option<RecordVideo>,
3556}
3557
3558impl BrowserContextOptionsBuilder {
3559 /// Sets the viewport dimensions
3560 pub fn viewport(mut self, viewport: Viewport) -> Self {
3561 self.viewport = Some(viewport);
3562 self.no_viewport = None; // Clear no_viewport if setting viewport
3563 self
3564 }
3565
3566 /// Disables viewport emulation
3567 pub fn no_viewport(mut self, no_viewport: bool) -> Self {
3568 self.no_viewport = Some(no_viewport);
3569 if no_viewport {
3570 self.viewport = None; // Clear viewport if setting no_viewport
3571 }
3572 self
3573 }
3574
3575 /// Sets the user agent string
3576 pub fn user_agent(mut self, user_agent: String) -> Self {
3577 self.user_agent = Some(user_agent);
3578 self
3579 }
3580
3581 /// Sets the locale
3582 pub fn locale(mut self, locale: String) -> Self {
3583 self.locale = Some(locale);
3584 self
3585 }
3586
3587 /// Sets the timezone identifier
3588 pub fn timezone_id(mut self, timezone_id: String) -> Self {
3589 self.timezone_id = Some(timezone_id);
3590 self
3591 }
3592
3593 /// Sets the geolocation
3594 pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
3595 self.geolocation = Some(geolocation);
3596 self
3597 }
3598
3599 /// Sets the permissions to grant
3600 pub fn permissions(mut self, permissions: Vec<String>) -> Self {
3601 self.permissions = Some(permissions);
3602 self
3603 }
3604
3605 /// Sets the network proxy settings for this context.
3606 ///
3607 /// This allows routing all network traffic through a proxy server,
3608 /// useful for rotating proxies without creating new browsers.
3609 ///
3610 /// # Example
3611 ///
3612 /// ```no_run
3613 /// use playwright_rs::protocol::{BrowserContextOptions, ProxySettings};
3614 ///
3615 /// let options = BrowserContextOptions::builder()
3616 /// .proxy(
3617 /// ProxySettings::new("http://proxy.example.com:8080")
3618 /// .bypass(".example.com")
3619 /// .username("user")
3620 /// .password("pass"),
3621 /// )
3622 /// .build();
3623 /// ```
3624 ///
3625 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
3626 pub fn proxy(mut self, proxy: ProxySettings) -> Self {
3627 self.proxy = Some(proxy);
3628 self
3629 }
3630
3631 /// Sets the color scheme preference
3632 pub fn color_scheme(mut self, color_scheme: String) -> Self {
3633 self.color_scheme = Some(color_scheme);
3634 self
3635 }
3636
3637 /// Sets whether the viewport supports touch events
3638 pub fn has_touch(mut self, has_touch: bool) -> Self {
3639 self.has_touch = Some(has_touch);
3640 self
3641 }
3642
3643 /// Sets whether this is a mobile viewport
3644 pub fn is_mobile(mut self, is_mobile: bool) -> Self {
3645 self.is_mobile = Some(is_mobile);
3646 self
3647 }
3648
3649 /// Sets whether JavaScript is enabled
3650 pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
3651 self.javascript_enabled = Some(javascript_enabled);
3652 self
3653 }
3654
3655 /// Sets whether to emulate offline network
3656 pub fn offline(mut self, offline: bool) -> Self {
3657 self.offline = Some(offline);
3658 self
3659 }
3660
3661 /// Sets how to handle downloads. Accepts `AcceptDownloads` or `bool`
3662 /// (`true` → `Accept`, `false` → `Deny`).
3663 pub fn accept_downloads(mut self, accept_downloads: impl Into<AcceptDownloads>) -> Self {
3664 self.accept_downloads = Some(accept_downloads.into());
3665 self
3666 }
3667
3668 /// Sets whether to bypass Content-Security-Policy
3669 pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
3670 self.bypass_csp = Some(bypass_csp);
3671 self
3672 }
3673
3674 /// Sets whether to ignore HTTPS errors
3675 pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
3676 self.ignore_https_errors = Some(ignore_https_errors);
3677 self
3678 }
3679
3680 /// Sets the device scale factor
3681 pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
3682 self.device_scale_factor = Some(device_scale_factor);
3683 self
3684 }
3685
3686 /// Sets extra HTTP headers
3687 pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
3688 self.extra_http_headers = Some(extra_http_headers);
3689 self
3690 }
3691
3692 /// Sets the base URL for relative navigation
3693 pub fn base_url(mut self, base_url: String) -> Self {
3694 self.base_url = Some(base_url);
3695 self
3696 }
3697
3698 /// Sets the storage state inline (cookies, localStorage).
3699 ///
3700 /// Populates the browser context with the provided storage state, including
3701 /// cookies and local storage. This is useful for initializing a context with
3702 /// a saved authentication state.
3703 ///
3704 /// Mutually exclusive with `storage_state_path()`.
3705 ///
3706 /// # Example
3707 ///
3708 /// ```rust
3709 /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
3710 ///
3711 /// let storage_state = StorageState::default()
3712 /// .cookies(vec![
3713 /// Cookie::new("session_id", "abc123")
3714 /// .domain(".example.com")
3715 /// .http_only(true)
3716 /// .secure(true)
3717 /// .same_site("Lax"),
3718 /// ])
3719 /// .origins(vec![Origin::new(
3720 /// "https://example.com",
3721 /// vec![LocalStorageItem::new("user_prefs", "{\"theme\":\"dark\"}")],
3722 /// )]);
3723 ///
3724 /// let options = BrowserContextOptions::builder()
3725 /// .storage_state(storage_state)
3726 /// .build();
3727 /// ```
3728 ///
3729 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3730 pub fn storage_state(mut self, storage_state: StorageState) -> Self {
3731 self.storage_state = Some(storage_state);
3732 self.storage_state_path = None; // Clear path if setting inline
3733 self
3734 }
3735
3736 /// Sets the storage state from a file path.
3737 ///
3738 /// The file should contain a JSON representation of StorageState with cookies
3739 /// and origins. This is useful for loading authentication state saved from a
3740 /// previous session.
3741 ///
3742 /// Mutually exclusive with `storage_state()`.
3743 ///
3744 /// # Example
3745 ///
3746 /// ```rust
3747 /// use playwright_rs::protocol::BrowserContextOptions;
3748 ///
3749 /// let options = BrowserContextOptions::builder()
3750 /// .storage_state_path("auth.json".to_string())
3751 /// .build();
3752 /// ```
3753 ///
3754 /// The file should have this format:
3755 /// ```json
3756 /// {
3757 /// "cookies": [{
3758 /// "name": "session_id",
3759 /// "value": "abc123",
3760 /// "domain": ".example.com",
3761 /// "path": "/",
3762 /// "expires": -1,
3763 /// "httpOnly": true,
3764 /// "secure": true,
3765 /// "sameSite": "Lax"
3766 /// }],
3767 /// "origins": [{
3768 /// "origin": "https://example.com",
3769 /// "localStorage": [{
3770 /// "name": "user_prefs",
3771 /// "value": "{\"theme\":\"dark\"}"
3772 /// }]
3773 /// }]
3774 /// }
3775 /// ```
3776 ///
3777 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
3778 pub fn storage_state_path(mut self, path: String) -> Self {
3779 self.storage_state_path = Some(path);
3780 self.storage_state = None; // Clear inline if setting path
3781 self
3782 }
3783
3784 /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
3785 pub fn args(mut self, args: Vec<String>) -> Self {
3786 self.args = Some(args);
3787 self
3788 }
3789
3790 /// Sets browser distribution channel (for launch_persistent_context)
3791 pub fn channel(mut self, channel: String) -> Self {
3792 self.channel = Some(channel);
3793 self
3794 }
3795
3796 /// Enables or disables Chromium sandboxing (for launch_persistent_context)
3797 pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
3798 self.chromium_sandbox = Some(enabled);
3799 self
3800 }
3801
3802 /// Auto-open DevTools (for launch_persistent_context)
3803 pub fn devtools(mut self, enabled: bool) -> Self {
3804 self.devtools = Some(enabled);
3805 self
3806 }
3807
3808 /// Sets directory to save downloads (for launch_persistent_context)
3809 pub fn downloads_path(mut self, path: String) -> Self {
3810 self.downloads_path = Some(path);
3811 self
3812 }
3813
3814 /// Sets path to custom browser executable (for launch_persistent_context)
3815 pub fn executable_path(mut self, path: String) -> Self {
3816 self.executable_path = Some(path);
3817 self
3818 }
3819
3820 /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
3821 pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
3822 self.firefox_user_prefs = Some(prefs);
3823 self
3824 }
3825
3826 /// Run in headless mode (for launch_persistent_context)
3827 pub fn headless(mut self, enabled: bool) -> Self {
3828 self.headless = Some(enabled);
3829 self
3830 }
3831
3832 /// Filter or disable default browser arguments (for launch_persistent_context).
3833 ///
3834 /// When `IgnoreDefaultArgs::Bool(true)`, Playwright does not pass its own
3835 /// default arguments and only uses the ones from `args`.
3836 /// When `IgnoreDefaultArgs::Array(vec)`, filters out the given default arguments.
3837 ///
3838 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
3839 pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
3840 self.ignore_default_args = Some(args);
3841 self
3842 }
3843
3844 /// Slow down operations by N milliseconds (for launch_persistent_context)
3845 pub fn slow_mo(mut self, ms: f64) -> Self {
3846 self.slow_mo = Some(ms);
3847 self
3848 }
3849
3850 /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
3851 pub fn timeout(mut self, ms: f64) -> Self {
3852 self.timeout = Some(ms);
3853 self
3854 }
3855
3856 /// Set directory to save traces (for launch_persistent_context)
3857 pub fn traces_dir(mut self, path: String) -> Self {
3858 self.traces_dir = Some(path);
3859 self
3860 }
3861
3862 /// Check if strict selectors mode is enabled
3863 pub fn strict_selectors(mut self, enabled: bool) -> Self {
3864 self.strict_selectors = Some(enabled);
3865 self
3866 }
3867
3868 /// Emulates 'prefers-reduced-motion' media feature
3869 pub fn reduced_motion(mut self, value: String) -> Self {
3870 self.reduced_motion = Some(value);
3871 self
3872 }
3873
3874 /// Emulates 'forced-colors' media feature
3875 pub fn forced_colors(mut self, value: String) -> Self {
3876 self.forced_colors = Some(value);
3877 self
3878 }
3879
3880 /// Whether to allow sites to register Service workers ("allow" | "block")
3881 pub fn service_workers(mut self, value: String) -> Self {
3882 self.service_workers = Some(value);
3883 self
3884 }
3885
3886 /// Sets options for recording HAR
3887 pub fn record_har(mut self, record_har: RecordHar) -> Self {
3888 self.record_har = Some(record_har);
3889 self
3890 }
3891
3892 /// Sets options for recording video
3893 pub fn record_video(mut self, record_video: RecordVideo) -> Self {
3894 self.record_video = Some(record_video);
3895 self
3896 }
3897
3898 /// Builds the BrowserContextOptions
3899 pub fn build(self) -> BrowserContextOptions {
3900 BrowserContextOptions {
3901 viewport: self.viewport,
3902 no_viewport: self.no_viewport,
3903 user_agent: self.user_agent,
3904 locale: self.locale,
3905 timezone_id: self.timezone_id,
3906 geolocation: self.geolocation,
3907 permissions: self.permissions,
3908 proxy: self.proxy,
3909 color_scheme: self.color_scheme,
3910 has_touch: self.has_touch,
3911 is_mobile: self.is_mobile,
3912 javascript_enabled: self.javascript_enabled,
3913 offline: self.offline,
3914 accept_downloads: self.accept_downloads,
3915 bypass_csp: self.bypass_csp,
3916 ignore_https_errors: self.ignore_https_errors,
3917 device_scale_factor: self.device_scale_factor,
3918 extra_http_headers: self.extra_http_headers,
3919 base_url: self.base_url,
3920 storage_state: self.storage_state,
3921 storage_state_path: self.storage_state_path,
3922 // Launch options
3923 args: self.args,
3924 channel: self.channel,
3925 chromium_sandbox: self.chromium_sandbox,
3926 devtools: self.devtools,
3927 downloads_path: self.downloads_path,
3928 executable_path: self.executable_path,
3929 firefox_user_prefs: self.firefox_user_prefs,
3930 headless: self.headless,
3931 ignore_default_args: self.ignore_default_args,
3932 slow_mo: self.slow_mo,
3933 timeout: self.timeout,
3934 traces_dir: self.traces_dir,
3935 strict_selectors: self.strict_selectors,
3936 reduced_motion: self.reduced_motion,
3937 forced_colors: self.forced_colors,
3938 service_workers: self.service_workers,
3939 record_har: self.record_har,
3940 record_video: self.record_video,
3941 }
3942 }
3943}
3944
3945/// Extracts timing data from a Response object's initializer, patching in
3946/// `responseEnd` from the event's `responseEndTiming` if available.
3947async fn extract_timing(
3948 connection: &std::sync::Arc<dyn crate::server::connection::ConnectionLike>,
3949 response_guid: Option<String>,
3950 response_end_timing: Option<f64>,
3951) -> Option<serde_json::Value> {
3952 let resp_guid = response_guid?;
3953 let resp_obj: crate::protocol::ResponseObject = connection
3954 .get_typed::<crate::protocol::ResponseObject>(&resp_guid)
3955 .await
3956 .ok()?;
3957 let mut timing = resp_obj.initializer().get("timing")?.clone();
3958 if let (Some(end), Some(obj)) = (response_end_timing, timing.as_object_mut())
3959 && let Some(n) = serde_json::Number::from_f64(end)
3960 {
3961 obj.insert("responseEnd".to_string(), serde_json::Value::Number(n));
3962 }
3963 Some(timing)
3964}
3965
3966#[cfg(test)]
3967mod tests {
3968 use super::*;
3969 use crate::api::launch_options::IgnoreDefaultArgs;
3970
3971 #[test]
3972 fn test_browser_context_options_ignore_default_args_bool_serialization() {
3973 let options = BrowserContextOptions::builder()
3974 .ignore_default_args(IgnoreDefaultArgs::Bool(true))
3975 .build();
3976
3977 let value = serde_json::to_value(&options).unwrap();
3978 assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(true));
3979 }
3980
3981 #[test]
3982 fn test_browser_context_options_ignore_default_args_array_serialization() {
3983 let options = BrowserContextOptions::builder()
3984 .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]))
3985 .build();
3986
3987 let value = serde_json::to_value(&options).unwrap();
3988 assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(["--foo"]));
3989 }
3990
3991 #[test]
3992 fn test_browser_context_options_ignore_default_args_absent() {
3993 let options = BrowserContextOptions::builder().build();
3994
3995 let value = serde_json::to_value(&options).unwrap();
3996 assert!(value.get("ignoreDefaultArgs").is_none());
3997 }
3998
3999 #[test]
4000 fn test_accept_downloads_serializes_as_protocol_string() {
4001 for (variant, expected) in [
4002 (AcceptDownloads::Accept, "accept"),
4003 (AcceptDownloads::Deny, "deny"),
4004 (AcceptDownloads::Internal, "internal"),
4005 ] {
4006 let options = BrowserContextOptions::builder()
4007 .accept_downloads(variant)
4008 .build();
4009 let value = serde_json::to_value(&options).unwrap();
4010 assert_eq!(value["acceptDownloads"], serde_json::json!(expected));
4011 }
4012 }
4013
4014 #[test]
4015 fn test_accept_downloads_bool_compatibility() {
4016 let opts = BrowserContextOptions::builder()
4017 .accept_downloads(true)
4018 .build();
4019 assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Accept));
4020
4021 let opts = BrowserContextOptions::builder()
4022 .accept_downloads(false)
4023 .build();
4024 assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Deny));
4025 }
4026}