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