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