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