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