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::route::UnrouteBehavior;
11use crate::protocol::{Browser, Page, ProxySettings, Request, ResponseObject, Route};
12use crate::server::channel::Channel;
13use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::any::Any;
17use std::collections::HashMap;
18use std::future::Future;
19use std::pin::Pin;
20use std::sync::{Arc, Mutex};
21
22/// BrowserContext represents an isolated browser session.
23///
24/// Contexts are isolated environments within a browser instance. Each context
25/// has its own cookies, cache, and local storage, enabling independent sessions
26/// without interference.
27///
28/// # Example
29///
30/// ```ignore
31/// use playwright_rs::protocol::Playwright;
32///
33/// #[tokio::main]
34/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
35/// let playwright = Playwright::launch().await?;
36/// let browser = playwright.chromium().launch().await?;
37///
38/// // Create isolated contexts
39/// let context1 = browser.new_context().await?;
40/// let context2 = browser.new_context().await?;
41///
42/// // Create pages in each context
43/// let page1 = context1.new_page().await?;
44/// let page2 = context2.new_page().await?;
45///
46/// // Access all pages in a context
47/// let pages = context1.pages();
48/// assert_eq!(pages.len(), 1);
49///
50/// // Access the browser from a context
51/// let ctx_browser = context1.browser().unwrap();
52/// assert_eq!(ctx_browser.name(), browser.name());
53///
54/// // App mode: access initial page created automatically
55/// let chromium = playwright.chromium();
56/// let app_context = chromium
57/// .launch_persistent_context_with_options(
58/// "/tmp/app-data",
59/// playwright_rs::protocol::BrowserContextOptions::builder()
60/// .args(vec!["--app=https://example.com".to_string()])
61/// .headless(true)
62/// .build()
63/// )
64/// .await?;
65///
66/// // Get the initial page (don't create a new one!)
67/// let app_pages = app_context.pages();
68/// if !app_pages.is_empty() {
69/// let initial_page = &app_pages[0];
70/// // Use the initial page...
71/// }
72///
73/// // Cleanup
74/// context1.close().await?;
75/// context2.close().await?;
76/// app_context.close().await?;
77/// browser.close().await?;
78/// Ok(())
79/// }
80/// ```
81///
82/// See: <https://playwright.dev/docs/api/class-browsercontext>
83/// Type alias for boxed route handler future
84type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
85
86/// Storage for a single route handler
87#[derive(Clone)]
88struct RouteHandlerEntry {
89 pattern: String,
90 handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
91}
92
93#[derive(Clone)]
94pub struct BrowserContext {
95 base: ChannelOwnerImpl,
96 /// Browser instance that owns this context (None for persistent contexts)
97 browser: Option<Browser>,
98 /// All open pages in this context
99 pages: Arc<Mutex<Vec<Page>>>,
100 /// Route handlers for context-level network interception
101 route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
102 /// APIRequestContext GUID from initializer (resolved lazily)
103 request_context_guid: Option<String>,
104 /// Default action timeout for all pages in this context (milliseconds), stored as f64 bits.
105 default_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
106 /// Default navigation timeout for all pages in this context (milliseconds), stored as f64 bits.
107 default_navigation_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
108}
109
110impl BrowserContext {
111 /// Creates a new BrowserContext from protocol initialization
112 ///
113 /// This is called by the object factory when the server sends a `__create__` message
114 /// for a BrowserContext object.
115 ///
116 /// # Arguments
117 ///
118 /// * `parent` - The parent Browser object
119 /// * `type_name` - The protocol type name ("BrowserContext")
120 /// * `guid` - The unique identifier for this context
121 /// * `initializer` - The initialization data from the server
122 ///
123 /// # Errors
124 ///
125 /// Returns error if initializer is malformed
126 pub fn new(
127 parent: Arc<dyn ChannelOwner>,
128 type_name: String,
129 guid: Arc<str>,
130 initializer: Value,
131 ) -> Result<Self> {
132 // Extract APIRequestContext GUID from initializer before moving it
133 let request_context_guid = initializer
134 .get("requestContext")
135 .and_then(|v| v.get("guid"))
136 .and_then(|v| v.as_str())
137 .map(|s| s.to_string());
138
139 let base = ChannelOwnerImpl::new(
140 ParentOrConnection::Parent(parent.clone()),
141 type_name,
142 guid,
143 initializer,
144 );
145
146 // Store browser reference if parent is a Browser
147 // Returns None only for special contexts (Android, Electron) where parent is not a Browser
148 // For both regular contexts and persistent contexts, parent is a Browser instance
149 let browser = parent.as_any().downcast_ref::<Browser>().cloned();
150
151 let context = Self {
152 base,
153 browser,
154 pages: Arc::new(Mutex::new(Vec::new())),
155 route_handlers: Arc::new(Mutex::new(Vec::new())),
156 request_context_guid,
157 default_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
158 crate::DEFAULT_TIMEOUT_MS.to_bits(),
159 )),
160 default_navigation_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
161 crate::DEFAULT_TIMEOUT_MS.to_bits(),
162 )),
163 };
164
165 // Enable dialog event subscription
166 // Dialog events need to be explicitly subscribed to via updateSubscription command
167 let channel = context.channel().clone();
168 tokio::spawn(async move {
169 _ = channel.update_subscription("dialog", true).await;
170 });
171
172 Ok(context)
173 }
174
175 /// Returns the channel for sending protocol messages
176 ///
177 /// Used internally for sending RPC calls to the context.
178 fn channel(&self) -> &Channel {
179 self.base.channel()
180 }
181
182 /// Adds a script which would be evaluated in one of the following scenarios:
183 ///
184 /// - Whenever a page is created in the browser context or is navigated.
185 /// - Whenever a child frame is attached or navigated in any page in the browser context.
186 ///
187 /// The script is evaluated after the document was created but before any of its scripts
188 /// were run. This is useful to amend the JavaScript environment, e.g. to seed Math.random.
189 ///
190 /// # Arguments
191 ///
192 /// * `script` - Script to be evaluated in all pages in the browser context.
193 ///
194 /// # Errors
195 ///
196 /// Returns error if:
197 /// - Context has been closed
198 /// - Communication with browser process fails
199 ///
200 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script>
201 pub async fn add_init_script(&self, script: &str) -> Result<()> {
202 self.channel()
203 .send_no_result("addInitScript", serde_json::json!({ "source": script }))
204 .await
205 }
206
207 /// Creates a new page in this browser context.
208 ///
209 /// Pages are isolated tabs/windows within a context. Each page starts
210 /// at "about:blank" and can be navigated independently.
211 ///
212 /// # Errors
213 ///
214 /// Returns error if:
215 /// - Context has been closed
216 /// - Communication with browser process fails
217 ///
218 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-new-page>
219 pub async fn new_page(&self) -> Result<Page> {
220 // Response contains the GUID of the created Page
221 #[derive(Deserialize)]
222 struct NewPageResponse {
223 page: GuidRef,
224 }
225
226 #[derive(Deserialize)]
227 struct GuidRef {
228 #[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
229 guid: Arc<str>,
230 }
231
232 // Send newPage RPC to server
233 let response: NewPageResponse = self
234 .channel()
235 .send("newPage", serde_json::json!({}))
236 .await?;
237
238 // Retrieve the Page object from the connection registry
239 let page_arc = self.connection().get_object(&response.page.guid).await?;
240
241 // Downcast to Page
242 let page = page_arc.as_any().downcast_ref::<Page>().ok_or_else(|| {
243 crate::error::Error::ProtocolError(format!(
244 "Expected Page object, got {}",
245 page_arc.type_name()
246 ))
247 })?;
248
249 // Note: Don't track the page here - it will be tracked via the "page" event
250 // that Playwright server sends automatically when a page is created.
251 // Tracking it here would create duplicates.
252
253 let page = page.clone();
254
255 // Propagate context-level timeout defaults to the new page
256 let ctx_timeout = self.default_timeout_ms();
257 let ctx_nav_timeout = self.default_navigation_timeout_ms();
258 if ctx_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
259 page.set_default_timeout(ctx_timeout).await;
260 }
261 if ctx_nav_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
262 page.set_default_navigation_timeout(ctx_nav_timeout).await;
263 }
264
265 Ok(page)
266 }
267
268 /// Returns all open pages in the context.
269 ///
270 /// This method provides a snapshot of all currently active pages that belong
271 /// to this browser context instance. Pages created via `new_page()` and popup
272 /// pages opened through user interactions are included.
273 ///
274 /// In persistent contexts launched with `--app=url`, this will include the
275 /// initial page created automatically by Playwright.
276 ///
277 /// # Errors
278 ///
279 /// This method does not return errors. It provides a snapshot of pages at
280 /// the time of invocation.
281 ///
282 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-pages>
283 pub fn pages(&self) -> Vec<Page> {
284 self.pages.lock().unwrap().clone()
285 }
286
287 /// Returns the browser instance that owns this context.
288 ///
289 /// Returns `None` only for contexts created outside of normal browser
290 /// (e.g., Android or Electron contexts). For both regular contexts and
291 /// persistent contexts, this returns the owning Browser instance.
292 ///
293 /// # Errors
294 ///
295 /// This method does not return errors.
296 ///
297 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-browser>
298 pub fn browser(&self) -> Option<Browser> {
299 self.browser.clone()
300 }
301
302 /// Returns the APIRequestContext associated with this context.
303 ///
304 /// The APIRequestContext is created automatically by the server for each
305 /// BrowserContext. It enables performing HTTP requests and is used internally
306 /// by `Route::fetch()`.
307 ///
308 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-request>
309 pub async fn request(&self) -> Result<APIRequestContext> {
310 let guid = self.request_context_guid.as_ref().ok_or_else(|| {
311 crate::error::Error::ProtocolError(
312 "No APIRequestContext available for this context".to_string(),
313 )
314 })?;
315
316 let obj = self.connection().get_object(guid).await?;
317 obj.as_any()
318 .downcast_ref::<APIRequestContext>()
319 .cloned()
320 .ok_or_else(|| {
321 crate::error::Error::ProtocolError(format!(
322 "Expected APIRequestContext, got {}",
323 obj.type_name()
324 ))
325 })
326 }
327
328 /// Closes the browser context and all its pages.
329 ///
330 /// This is a graceful operation that sends a close command to the context
331 /// and waits for it to shut down properly.
332 ///
333 /// # Errors
334 ///
335 /// Returns error if:
336 /// - Context has already been closed
337 /// - Communication with browser process fails
338 ///
339 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-close>
340 pub async fn close(&self) -> Result<()> {
341 // Send close RPC to server
342 self.channel()
343 .send_no_result("close", serde_json::json!({}))
344 .await
345 }
346
347 /// Sets the default timeout for all operations in this browser context.
348 ///
349 /// This applies to all pages already open in this context as well as pages
350 /// created subsequently. Pass `0` to disable timeouts.
351 ///
352 /// # Arguments
353 ///
354 /// * `timeout` - Timeout in milliseconds
355 ///
356 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout>
357 pub async fn set_default_timeout(&self, timeout: f64) {
358 self.default_timeout_ms
359 .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
360 let pages: Vec<Page> = self.pages.lock().unwrap().clone();
361 for page in pages {
362 page.set_default_timeout(timeout).await;
363 }
364 crate::protocol::page::set_timeout_and_notify(
365 self.channel(),
366 "setDefaultTimeoutNoReply",
367 timeout,
368 )
369 .await;
370 }
371
372 /// Sets the default timeout for navigation operations in this browser context.
373 ///
374 /// This applies to all pages already open in this context as well as pages
375 /// created subsequently. Pass `0` to disable timeouts.
376 ///
377 /// # Arguments
378 ///
379 /// * `timeout` - Timeout in milliseconds
380 ///
381 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout>
382 pub async fn set_default_navigation_timeout(&self, timeout: f64) {
383 self.default_navigation_timeout_ms
384 .store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
385 let pages: Vec<Page> = self.pages.lock().unwrap().clone();
386 for page in pages {
387 page.set_default_navigation_timeout(timeout).await;
388 }
389 crate::protocol::page::set_timeout_and_notify(
390 self.channel(),
391 "setDefaultNavigationTimeoutNoReply",
392 timeout,
393 )
394 .await;
395 }
396
397 /// Returns the context's current default action timeout in milliseconds.
398 fn default_timeout_ms(&self) -> f64 {
399 f64::from_bits(
400 self.default_timeout_ms
401 .load(std::sync::atomic::Ordering::Relaxed),
402 )
403 }
404
405 /// Returns the context's current default navigation timeout in milliseconds.
406 fn default_navigation_timeout_ms(&self) -> f64 {
407 f64::from_bits(
408 self.default_navigation_timeout_ms
409 .load(std::sync::atomic::Ordering::Relaxed),
410 )
411 }
412
413 /// Pauses the browser context.
414 ///
415 /// This pauses the execution of all pages in the context.
416 pub async fn pause(&self) -> Result<()> {
417 self.channel()
418 .send_no_result("pause", serde_json::Value::Null)
419 .await
420 }
421
422 /// Returns storage state for this browser context.
423 ///
424 /// Contains current cookies and local storage snapshots.
425 ///
426 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state>
427 pub async fn storage_state(&self) -> Result<StorageState> {
428 let response: StorageState = self
429 .channel()
430 .send("storageState", serde_json::json!({}))
431 .await?;
432 Ok(response)
433 }
434
435 /// Adds cookies into this browser context.
436 ///
437 /// All pages within this context will have these cookies installed. Cookies can be granularly specified
438 /// with `name`, `value`, `url`, `domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`.
439 ///
440 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies>
441 pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
442 self.channel()
443 .send_no_result(
444 "addCookies",
445 serde_json::json!({
446 "cookies": cookies
447 }),
448 )
449 .await
450 }
451
452 /// Returns cookies for this browser context, optionally filtered by URLs.
453 ///
454 /// If `urls` is `None` or empty, all cookies are returned.
455 ///
456 /// # Arguments
457 ///
458 /// * `urls` - Optional list of URLs to filter cookies by
459 ///
460 /// # Errors
461 ///
462 /// Returns error if:
463 /// - Context has been closed
464 /// - Communication with browser process fails
465 ///
466 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-cookies>
467 pub async fn cookies(&self, urls: Option<&[&str]>) -> Result<Vec<Cookie>> {
468 let url_list: Vec<&str> = urls.unwrap_or(&[]).to_vec();
469 #[derive(serde::Deserialize)]
470 struct CookiesResponse {
471 cookies: Vec<Cookie>,
472 }
473 let response: CookiesResponse = self
474 .channel()
475 .send("cookies", serde_json::json!({ "urls": url_list }))
476 .await?;
477 Ok(response.cookies)
478 }
479
480 /// Clears cookies from this browser context, with optional filters.
481 ///
482 /// When called with no options, all cookies are removed. Use `ClearCookiesOptions`
483 /// to filter which cookies to clear by name, domain, or path.
484 ///
485 /// # Arguments
486 ///
487 /// * `options` - Optional filters for which cookies to clear
488 ///
489 /// # Errors
490 ///
491 /// Returns error if:
492 /// - Context has been closed
493 /// - Communication with browser process fails
494 ///
495 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
496 pub async fn clear_cookies(&self, options: Option<ClearCookiesOptions>) -> Result<()> {
497 let params = match options {
498 None => serde_json::json!({}),
499 Some(opts) => serde_json::to_value(opts).unwrap_or(serde_json::json!({})),
500 };
501 self.channel().send_no_result("clearCookies", params).await
502 }
503
504 /// Sets extra HTTP headers that will be sent with every request from this context.
505 ///
506 /// These headers are merged with per-page extra headers set with `page.set_extra_http_headers()`.
507 /// If the page has specific headers that conflict, page-level headers take precedence.
508 ///
509 /// # Arguments
510 ///
511 /// * `headers` - Map of header names to values. All header names are lowercased.
512 ///
513 /// # Errors
514 ///
515 /// Returns error if:
516 /// - Context has been closed
517 /// - Communication with browser process fails
518 ///
519 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-extra-http-headers>
520 pub async fn set_extra_http_headers(&self, headers: HashMap<String, String>) -> Result<()> {
521 // Playwright protocol expects an array of {name, value} objects
522 let headers_array: Vec<serde_json::Value> = headers
523 .into_iter()
524 .map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
525 .collect();
526 self.channel()
527 .send_no_result(
528 "setExtraHTTPHeaders",
529 serde_json::json!({ "headers": headers_array }),
530 )
531 .await
532 }
533
534 /// Grants browser permissions to the context.
535 ///
536 /// Permissions are granted for all pages in the context. The optional `origin`
537 /// in `GrantPermissionsOptions` restricts the grant to a specific URL origin.
538 ///
539 /// Common permissions: `"geolocation"`, `"notifications"`, `"camera"`,
540 /// `"microphone"`, `"clipboard-read"`, `"clipboard-write"`.
541 ///
542 /// # Arguments
543 ///
544 /// * `permissions` - List of permission strings to grant
545 /// * `options` - Optional options, including `origin` to restrict the grant
546 ///
547 /// # Errors
548 ///
549 /// Returns error if:
550 /// - Permission name is not recognised
551 /// - Context has been closed
552 /// - Communication with browser process fails
553 ///
554 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
555 pub async fn grant_permissions(
556 &self,
557 permissions: &[&str],
558 options: Option<GrantPermissionsOptions>,
559 ) -> Result<()> {
560 let mut params = serde_json::json!({ "permissions": permissions });
561 if let Some(opts) = options {
562 if let Some(origin) = opts.origin {
563 params["origin"] = serde_json::Value::String(origin);
564 }
565 }
566 self.channel()
567 .send_no_result("grantPermissions", params)
568 .await
569 }
570
571 /// Clears all permission overrides for this browser context.
572 ///
573 /// Reverts all permissions previously set with `grant_permissions()` back to
574 /// the browser default state.
575 ///
576 /// # Errors
577 ///
578 /// Returns error if:
579 /// - Context has been closed
580 /// - Communication with browser process fails
581 ///
582 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-permissions>
583 pub async fn clear_permissions(&self) -> Result<()> {
584 self.channel()
585 .send_no_result("clearPermissions", serde_json::json!({}))
586 .await
587 }
588
589 /// Sets or clears the geolocation for all pages in this context.
590 ///
591 /// Pass `Some(Geolocation { ... })` to set a specific location, or `None` to
592 /// clear the override and let the browser handle location requests naturally.
593 ///
594 /// Note: Geolocation access requires the `"geolocation"` permission to be granted
595 /// via `grant_permissions()` for navigator.geolocation to succeed.
596 ///
597 /// # Arguments
598 ///
599 /// * `geolocation` - Location to set, or `None` to clear
600 ///
601 /// # Errors
602 ///
603 /// Returns error if:
604 /// - Latitude or longitude is out of range
605 /// - Context has been closed
606 /// - Communication with browser process fails
607 ///
608 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-geolocation>
609 pub async fn set_geolocation(&self, geolocation: Option<Geolocation>) -> Result<()> {
610 // Playwright protocol: omit the "geolocation" key entirely to clear;
611 // passing null causes a validation error on the server side.
612 let params = match geolocation {
613 Some(geo) => serde_json::json!({ "geolocation": geo }),
614 None => serde_json::json!({}),
615 };
616 self.channel()
617 .send_no_result("setGeolocation", params)
618 .await
619 }
620
621 /// Toggles the offline mode for this browser context.
622 ///
623 /// When `true`, all network requests from pages in this context will fail with
624 /// a network error. Set to `false` to restore network connectivity.
625 ///
626 /// # Arguments
627 ///
628 /// * `offline` - `true` to go offline, `false` to go back online
629 ///
630 /// # Errors
631 ///
632 /// Returns error if:
633 /// - Context has been closed
634 /// - Communication with browser process fails
635 ///
636 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-set-offline>
637 pub async fn set_offline(&self, offline: bool) -> Result<()> {
638 self.channel()
639 .send_no_result("setOffline", serde_json::json!({ "offline": offline }))
640 .await
641 }
642
643 /// Registers a route handler for context-level network interception.
644 ///
645 /// Routes registered on a context apply to all pages within the context.
646 /// Page-level routes take precedence over context-level routes.
647 ///
648 /// # Arguments
649 ///
650 /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
651 /// * `handler` - Async closure that handles the route
652 ///
653 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route>
654 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
655 where
656 F: Fn(Route) -> Fut + Send + Sync + 'static,
657 Fut: Future<Output = Result<()>> + Send + 'static,
658 {
659 let handler =
660 Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
661
662 self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
663 pattern: pattern.to_string(),
664 handler,
665 });
666
667 self.enable_network_interception().await
668 }
669
670 /// Removes route handler(s) matching the given URL pattern.
671 ///
672 /// # Arguments
673 ///
674 /// * `pattern` - URL pattern to remove handlers for
675 ///
676 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute>
677 pub async fn unroute(&self, pattern: &str) -> Result<()> {
678 self.route_handlers
679 .lock()
680 .unwrap()
681 .retain(|entry| entry.pattern != pattern);
682 self.enable_network_interception().await
683 }
684
685 /// Removes all registered route handlers.
686 ///
687 /// # Arguments
688 ///
689 /// * `behavior` - Optional behavior for in-flight handlers
690 ///
691 /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute-all>
692 pub async fn unroute_all(&self, _behavior: Option<UnrouteBehavior>) -> Result<()> {
693 self.route_handlers.lock().unwrap().clear();
694 self.enable_network_interception().await
695 }
696
697 /// Updates network interception patterns for this context
698 async fn enable_network_interception(&self) -> Result<()> {
699 let patterns: Vec<serde_json::Value> = self
700 .route_handlers
701 .lock()
702 .unwrap()
703 .iter()
704 .map(|entry| serde_json::json!({ "glob": entry.pattern }))
705 .collect();
706
707 self.channel()
708 .send_no_result(
709 "setNetworkInterceptionPatterns",
710 serde_json::json!({ "patterns": patterns }),
711 )
712 .await
713 }
714
715 /// Handles a route event from the protocol
716 async fn on_route_event(route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>, route: Route) {
717 let handlers = route_handlers.lock().unwrap().clone();
718 let url = route.request().url().to_string();
719
720 for entry in handlers.iter().rev() {
721 if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
722 let handler = entry.handler.clone();
723 if let Err(e) = handler(route.clone()).await {
724 tracing::warn!("Context route handler error: {}", e);
725 break;
726 }
727 if !route.was_handled() {
728 continue;
729 }
730 break;
731 }
732 }
733 }
734
735 fn dispatch_request_event(&self, method: &str, params: Value) {
736 if let Some(request_guid) = params
737 .get("request")
738 .and_then(|v| v.get("guid"))
739 .and_then(|v| v.as_str())
740 {
741 let connection = self.connection();
742 let request_guid_owned = request_guid.to_owned();
743 let page_guid_owned = params
744 .get("page")
745 .and_then(|v| v.get("guid"))
746 .and_then(|v| v.as_str())
747 .map(|v| v.to_owned());
748 // Extract failureText for requestFailed events
749 let failure_text = params
750 .get("failureText")
751 .and_then(|v| v.as_str())
752 .map(|s| s.to_owned());
753 // Extract response GUID for requestFinished events (to read timing)
754 let response_guid_owned = params
755 .get("response")
756 .and_then(|v| v.get("guid"))
757 .and_then(|v| v.as_str())
758 .map(|s| s.to_owned());
759 // Extract responseEndTiming from requestFinished event params
760 let response_end_timing = params.get("responseEndTiming").and_then(|v| v.as_f64());
761 let method = method.to_owned();
762 tokio::spawn(async move {
763 let request_arc = match connection.get_object(&request_guid_owned).await {
764 Ok(obj) => obj,
765 Err(_err) => return,
766 };
767
768 let request = match request_arc.as_any().downcast_ref::<Request>() {
769 Some(v) => v.clone(),
770 None => return,
771 };
772
773 // Set failure text on the request before dispatching to handlers
774 if let Some(text) = failure_text {
775 request.set_failure_text(text);
776 }
777
778 // For requestFinished, extract timing from the Response object's initializer
779 if method == "requestFinished" {
780 if let Some(timing) =
781 extract_timing(&connection, response_guid_owned, response_end_timing).await
782 {
783 request.set_timing(timing);
784 }
785 }
786
787 if let Some(page_guid) = page_guid_owned {
788 let page_arc = match connection.get_object(&page_guid).await {
789 Ok(v) => v,
790 Err(_) => return,
791 };
792 let page = match page_arc.as_any().downcast_ref::<Page>() {
793 Some(p) => p,
794 None => return,
795 };
796 match method.as_str() {
797 "request" => page.trigger_request_event(request).await,
798 "requestFailed" => page.trigger_request_failed_event(request).await,
799 "requestFinished" => page.trigger_request_finished_event(request).await,
800 _ => unreachable!("Unreachable method {}", method),
801 }
802 }
803 });
804 }
805 }
806
807 fn dispatch_response_event(&self, _method: &str, params: Value) {
808 if let Some(response_guid) = params
809 .get("response")
810 .and_then(|v| v.get("guid"))
811 .and_then(|v| v.as_str())
812 {
813 let connection = self.connection();
814 let response_guid_owned = response_guid.to_owned();
815 let page_guid_owned = params
816 .get("page")
817 .and_then(|v| v.get("guid"))
818 .and_then(|v| v.as_str())
819 .map(|v| v.to_owned());
820 tokio::spawn(async move {
821 let response_arc = match connection.get_object(&response_guid_owned).await {
822 Ok(obj) => obj,
823 Err(_err) => return,
824 };
825
826 let response = match response_arc.as_any().downcast_ref::<ResponseObject>() {
827 Some(v) => v.clone(),
828 None => return,
829 };
830
831 if let Some(page_guid) = page_guid_owned {
832 let page_arc = match connection.get_object(&page_guid).await {
833 Ok(v) => v,
834 Err(_) => return,
835 };
836 let page = match page_arc.as_any().downcast_ref::<Page>() {
837 Some(p) => p,
838 None => return,
839 };
840 page.trigger_response_event(response).await;
841 }
842 });
843 }
844 }
845}
846
847impl ChannelOwner for BrowserContext {
848 fn guid(&self) -> &str {
849 self.base.guid()
850 }
851
852 fn type_name(&self) -> &str {
853 self.base.type_name()
854 }
855
856 fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
857 self.base.parent()
858 }
859
860 fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
861 self.base.connection()
862 }
863
864 fn initializer(&self) -> &Value {
865 self.base.initializer()
866 }
867
868 fn channel(&self) -> &Channel {
869 self.base.channel()
870 }
871
872 fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
873 self.base.dispose(reason)
874 }
875
876 fn adopt(&self, child: Arc<dyn ChannelOwner>) {
877 self.base.adopt(child)
878 }
879
880 fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
881 self.base.add_child(guid, child)
882 }
883
884 fn remove_child(&self, guid: &str) {
885 self.base.remove_child(guid)
886 }
887
888 fn on_event(&self, method: &str, params: Value) {
889 match method {
890 "request" | "requestFailed" | "requestFinished" => {
891 self.dispatch_request_event(method, params)
892 }
893 "response" => self.dispatch_response_event(method, params),
894 "page" => {
895 // Page events are triggered when pages are created, including:
896 // - Initial page in persistent context with --app mode
897 // - Popup pages opened through user interactions
898 // Event format: {page: {guid: "..."}}
899 if let Some(page_guid) = params
900 .get("page")
901 .and_then(|v| v.get("guid"))
902 .and_then(|v| v.as_str())
903 {
904 let connection = self.connection();
905 let page_guid_owned = page_guid.to_string();
906 let pages = self.pages.clone();
907
908 tokio::spawn(async move {
909 // Get the Page object
910 let page_arc = match connection.get_object(&page_guid_owned).await {
911 Ok(obj) => obj,
912 Err(_) => return,
913 };
914
915 // Downcast to Page
916 let page = match page_arc.as_any().downcast_ref::<Page>() {
917 Some(p) => p.clone(),
918 None => return,
919 };
920
921 // Track the page
922 pages.lock().unwrap().push(page);
923 });
924 }
925 }
926 "dialog" => {
927 // Dialog events come to BrowserContext, need to forward to the associated Page
928 // Event format: {dialog: {guid: "..."}}
929 // The Dialog protocol object has the Page as its parent
930 if let Some(dialog_guid) = params
931 .get("dialog")
932 .and_then(|v| v.get("guid"))
933 .and_then(|v| v.as_str())
934 {
935 let connection = self.connection();
936 let dialog_guid_owned = dialog_guid.to_string();
937
938 tokio::spawn(async move {
939 // Get the Dialog object
940 let dialog_arc = match connection.get_object(&dialog_guid_owned).await {
941 Ok(obj) => obj,
942 Err(_) => return,
943 };
944
945 // Downcast to Dialog
946 let dialog = match dialog_arc
947 .as_any()
948 .downcast_ref::<crate::protocol::Dialog>()
949 {
950 Some(d) => d.clone(),
951 None => return,
952 };
953
954 // Get the Page from the Dialog's parent
955 let page_arc = match dialog_arc.parent() {
956 Some(parent) => parent,
957 None => return,
958 };
959
960 // Downcast to Page
961 let page = match page_arc.as_any().downcast_ref::<Page>() {
962 Some(p) => p.clone(),
963 None => return,
964 };
965
966 // Forward to Page's dialog handlers
967 page.trigger_dialog_event(dialog).await;
968 });
969 }
970 }
971 "route" => {
972 // Handle context-level network routing event
973 if let Some(route_guid) = params
974 .get("route")
975 .and_then(|v| v.get("guid"))
976 .and_then(|v| v.as_str())
977 {
978 let connection = self.connection();
979 let route_guid_owned = route_guid.to_string();
980 let route_handlers = self.route_handlers.clone();
981 let request_context_guid = self.request_context_guid.clone();
982
983 tokio::spawn(async move {
984 let route_arc = match connection.get_object(&route_guid_owned).await {
985 Ok(obj) => obj,
986 Err(e) => {
987 tracing::warn!("Failed to get route object: {}", e);
988 return;
989 }
990 };
991
992 let route = match route_arc.as_any().downcast_ref::<Route>() {
993 Some(r) => r.clone(),
994 None => {
995 tracing::warn!("Failed to downcast to Route");
996 return;
997 }
998 };
999
1000 // Set APIRequestContext on the route for fetch() support
1001 if let Some(ref guid) = request_context_guid {
1002 if let Ok(obj) = connection.get_object(guid).await {
1003 if let Some(api_ctx) =
1004 obj.as_any().downcast_ref::<APIRequestContext>()
1005 {
1006 route.set_api_request_context(api_ctx.clone());
1007 }
1008 }
1009 }
1010
1011 BrowserContext::on_route_event(route_handlers, route).await;
1012 });
1013 }
1014 }
1015 _ => {
1016 // Other events will be handled in future phases
1017 }
1018 }
1019 }
1020
1021 fn was_collected(&self) -> bool {
1022 self.base.was_collected()
1023 }
1024
1025 fn as_any(&self) -> &dyn Any {
1026 self
1027 }
1028}
1029
1030impl std::fmt::Debug for BrowserContext {
1031 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1032 f.debug_struct("BrowserContext")
1033 .field("guid", &self.guid())
1034 .finish()
1035 }
1036}
1037
1038/// Viewport dimensions for browser context.
1039///
1040/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1042pub struct Viewport {
1043 /// Page width in pixels
1044 pub width: u32,
1045 /// Page height in pixels
1046 pub height: u32,
1047}
1048
1049/// Geolocation coordinates.
1050///
1051/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1052#[derive(Debug, Clone, Serialize, Deserialize)]
1053pub struct Geolocation {
1054 /// Latitude between -90 and 90
1055 pub latitude: f64,
1056 /// Longitude between -180 and 180
1057 pub longitude: f64,
1058 /// Optional accuracy in meters (default: 0)
1059 #[serde(skip_serializing_if = "Option::is_none")]
1060 pub accuracy: Option<f64>,
1061}
1062
1063/// Cookie information for storage state.
1064///
1065/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1066#[derive(Debug, Clone, Serialize, Deserialize)]
1067#[serde(rename_all = "camelCase")]
1068pub struct Cookie {
1069 /// Cookie name
1070 pub name: String,
1071 /// Cookie value
1072 pub value: String,
1073 /// Cookie domain (use dot prefix for subdomain matching, e.g., ".example.com")
1074 pub domain: String,
1075 /// Cookie path
1076 pub path: String,
1077 /// Unix timestamp in seconds; -1 for session cookies
1078 pub expires: f64,
1079 /// HTTP-only flag
1080 pub http_only: bool,
1081 /// Secure flag
1082 pub secure: bool,
1083 /// SameSite attribute ("Strict", "Lax", "None")
1084 #[serde(skip_serializing_if = "Option::is_none")]
1085 pub same_site: Option<String>,
1086}
1087
1088/// Local storage item for storage state.
1089///
1090/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1091#[derive(Debug, Clone, Serialize, Deserialize)]
1092pub struct LocalStorageItem {
1093 /// Storage key
1094 pub name: String,
1095 /// Storage value
1096 pub value: String,
1097}
1098
1099/// Origin with local storage items for storage state.
1100///
1101/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1102#[derive(Debug, Clone, Serialize, Deserialize)]
1103#[serde(rename_all = "camelCase")]
1104pub struct Origin {
1105 /// Origin URL (e.g., `https://example.com`)
1106 pub origin: String,
1107 /// Local storage items for this origin
1108 pub local_storage: Vec<LocalStorageItem>,
1109}
1110
1111/// Storage state containing cookies and local storage.
1112///
1113/// Used to populate a browser context with saved authentication state,
1114/// enabling session persistence across context instances.
1115///
1116/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1118pub struct StorageState {
1119 /// List of cookies
1120 pub cookies: Vec<Cookie>,
1121 /// List of origins with local storage
1122 pub origins: Vec<Origin>,
1123}
1124
1125/// Options for filtering which cookies to clear with `BrowserContext::clear_cookies()`.
1126///
1127/// All fields are optional; when provided they act as AND-combined filters.
1128///
1129/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-clear-cookies>
1130#[derive(Debug, Clone, Default, Serialize)]
1131#[serde(rename_all = "camelCase")]
1132pub struct ClearCookiesOptions {
1133 /// Filter by cookie name (exact match).
1134 #[serde(skip_serializing_if = "Option::is_none")]
1135 pub name: Option<String>,
1136 /// Filter by cookie domain.
1137 #[serde(skip_serializing_if = "Option::is_none")]
1138 pub domain: Option<String>,
1139 /// Filter by cookie path.
1140 #[serde(skip_serializing_if = "Option::is_none")]
1141 pub path: Option<String>,
1142}
1143
1144/// Options for `BrowserContext::grant_permissions()`.
1145///
1146/// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions>
1147#[derive(Debug, Clone, Default)]
1148pub struct GrantPermissionsOptions {
1149 /// Optional origin to restrict the permission grant to.
1150 ///
1151 /// For example `"https://example.com"`.
1152 pub origin: Option<String>,
1153}
1154
1155/// Options for recording HAR.
1156///
1157/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har>
1158#[derive(Debug, Clone, Serialize, Default)]
1159#[serde(rename_all = "camelCase")]
1160pub struct RecordHar {
1161 /// Path on the filesystem to write the HAR file to.
1162 pub path: String,
1163 /// Optional setting to control whether to omit request content from the HAR.
1164 #[serde(skip_serializing_if = "Option::is_none")]
1165 pub omit_content: Option<bool>,
1166 /// Optional setting to control resource content management.
1167 /// "omit" | "embed" | "attach"
1168 #[serde(skip_serializing_if = "Option::is_none")]
1169 pub content: Option<String>,
1170 /// "full" | "minimal"
1171 #[serde(skip_serializing_if = "Option::is_none")]
1172 pub mode: Option<String>,
1173 /// A glob or regex pattern to filter requests that are stored in the HAR.
1174 #[serde(skip_serializing_if = "Option::is_none")]
1175 pub url_filter: Option<String>,
1176}
1177
1178/// Options for recording video.
1179///
1180/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-video>
1181#[derive(Debug, Clone, Serialize, Default)]
1182pub struct RecordVideo {
1183 /// Path to the directory to put videos into.
1184 pub dir: String,
1185 /// Optional dimensions of the recorded videos.
1186 #[serde(skip_serializing_if = "Option::is_none")]
1187 pub size: Option<Viewport>,
1188}
1189
1190/// Options for creating a new browser context.
1191///
1192/// Allows customizing viewport, user agent, locale, timezone, geolocation,
1193/// permissions, and other browser context settings.
1194///
1195/// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1196#[derive(Debug, Clone, Default, Serialize)]
1197#[serde(rename_all = "camelCase")]
1198pub struct BrowserContextOptions {
1199 /// Sets consistent viewport for all pages in the context.
1200 /// Set to null via `no_viewport(true)` to disable viewport emulation.
1201 #[serde(skip_serializing_if = "Option::is_none")]
1202 pub viewport: Option<Viewport>,
1203
1204 /// Disables viewport emulation when set to true.
1205 /// Note: Playwright's public API calls this `noViewport`, but the protocol
1206 /// expects `noDefaultViewport`. playwright-python applies this transformation
1207 /// in `_prepare_browser_context_params`.
1208 #[serde(skip_serializing_if = "Option::is_none")]
1209 #[serde(rename = "noDefaultViewport")]
1210 pub no_viewport: Option<bool>,
1211
1212 /// Custom user agent string
1213 #[serde(skip_serializing_if = "Option::is_none")]
1214 pub user_agent: Option<String>,
1215
1216 /// Locale for the context (e.g., "en-GB", "de-DE", "fr-FR")
1217 #[serde(skip_serializing_if = "Option::is_none")]
1218 pub locale: Option<String>,
1219
1220 /// Timezone identifier (e.g., "America/New_York", "Europe/Berlin")
1221 #[serde(skip_serializing_if = "Option::is_none")]
1222 pub timezone_id: Option<String>,
1223
1224 /// Geolocation coordinates
1225 #[serde(skip_serializing_if = "Option::is_none")]
1226 pub geolocation: Option<Geolocation>,
1227
1228 /// List of permissions to grant (e.g., "geolocation", "notifications")
1229 #[serde(skip_serializing_if = "Option::is_none")]
1230 pub permissions: Option<Vec<String>>,
1231
1232 /// Network proxy settings
1233 #[serde(skip_serializing_if = "Option::is_none")]
1234 pub proxy: Option<ProxySettings>,
1235
1236 /// Emulates 'prefers-colors-scheme' media feature ("light", "dark", "no-preference")
1237 #[serde(skip_serializing_if = "Option::is_none")]
1238 pub color_scheme: Option<String>,
1239
1240 /// Whether the viewport supports touch events
1241 #[serde(skip_serializing_if = "Option::is_none")]
1242 pub has_touch: Option<bool>,
1243
1244 /// Whether the meta viewport tag is respected
1245 #[serde(skip_serializing_if = "Option::is_none")]
1246 pub is_mobile: Option<bool>,
1247
1248 /// Whether JavaScript is enabled in the context
1249 #[serde(skip_serializing_if = "Option::is_none")]
1250 pub javascript_enabled: Option<bool>,
1251
1252 /// Emulates network being offline
1253 #[serde(skip_serializing_if = "Option::is_none")]
1254 pub offline: Option<bool>,
1255
1256 /// Whether to automatically download attachments
1257 #[serde(skip_serializing_if = "Option::is_none")]
1258 pub accept_downloads: Option<bool>,
1259
1260 /// Whether to bypass Content-Security-Policy
1261 #[serde(skip_serializing_if = "Option::is_none")]
1262 pub bypass_csp: Option<bool>,
1263
1264 /// Whether to ignore HTTPS errors
1265 #[serde(skip_serializing_if = "Option::is_none")]
1266 pub ignore_https_errors: Option<bool>,
1267
1268 /// Device scale factor (default: 1)
1269 #[serde(skip_serializing_if = "Option::is_none")]
1270 pub device_scale_factor: Option<f64>,
1271
1272 /// Extra HTTP headers to send with every request
1273 #[serde(skip_serializing_if = "Option::is_none")]
1274 pub extra_http_headers: Option<HashMap<String, String>>,
1275
1276 /// Base URL for relative navigation
1277 #[serde(skip_serializing_if = "Option::is_none")]
1278 pub base_url: Option<String>,
1279
1280 /// Storage state to populate the context (cookies, localStorage, sessionStorage).
1281 /// Can be an inline StorageState object or a file path string.
1282 /// Use builder methods `storage_state()` for inline or `storage_state_path()` for file path.
1283 #[serde(skip_serializing_if = "Option::is_none")]
1284 pub storage_state: Option<StorageState>,
1285
1286 /// Storage state file path (alternative to inline storage_state).
1287 /// This is handled by the builder and converted to storage_state during serialization.
1288 #[serde(skip_serializing_if = "Option::is_none")]
1289 pub storage_state_path: Option<String>,
1290
1291 // Launch options (for launch_persistent_context)
1292 /// Additional arguments to pass to browser instance
1293 #[serde(skip_serializing_if = "Option::is_none")]
1294 pub args: Option<Vec<String>>,
1295
1296 /// Browser distribution channel (e.g., "chrome", "msedge")
1297 #[serde(skip_serializing_if = "Option::is_none")]
1298 pub channel: Option<String>,
1299
1300 /// Enable Chromium sandboxing (default: false on Linux)
1301 #[serde(skip_serializing_if = "Option::is_none")]
1302 pub chromium_sandbox: Option<bool>,
1303
1304 /// Auto-open DevTools (deprecated, default: false)
1305 #[serde(skip_serializing_if = "Option::is_none")]
1306 pub devtools: Option<bool>,
1307
1308 /// Directory to save downloads
1309 #[serde(skip_serializing_if = "Option::is_none")]
1310 pub downloads_path: Option<String>,
1311
1312 /// Path to custom browser executable
1313 #[serde(skip_serializing_if = "Option::is_none")]
1314 pub executable_path: Option<String>,
1315
1316 /// Firefox user preferences (Firefox only)
1317 #[serde(skip_serializing_if = "Option::is_none")]
1318 pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
1319
1320 /// Run in headless mode (default: true unless devtools=true)
1321 #[serde(skip_serializing_if = "Option::is_none")]
1322 pub headless: Option<bool>,
1323
1324 /// Filter or disable default browser arguments.
1325 /// When `true`, Playwright does not pass its own default args.
1326 /// When an array, filters out the given default arguments.
1327 ///
1328 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
1329 #[serde(skip_serializing_if = "Option::is_none")]
1330 pub ignore_default_args: Option<IgnoreDefaultArgs>,
1331
1332 /// Slow down operations by N milliseconds
1333 #[serde(skip_serializing_if = "Option::is_none")]
1334 pub slow_mo: Option<f64>,
1335
1336 /// Timeout for browser launch in milliseconds
1337 #[serde(skip_serializing_if = "Option::is_none")]
1338 pub timeout: Option<f64>,
1339
1340 /// Directory to save traces
1341 #[serde(skip_serializing_if = "Option::is_none")]
1342 pub traces_dir: Option<String>,
1343
1344 /// Check if strict selectors mode is enabled
1345 #[serde(skip_serializing_if = "Option::is_none")]
1346 pub strict_selectors: Option<bool>,
1347
1348 /// Emulates 'prefers-reduced-motion' media feature
1349 #[serde(skip_serializing_if = "Option::is_none")]
1350 pub reduced_motion: Option<String>,
1351
1352 /// Emulates 'forced-colors' media feature
1353 #[serde(skip_serializing_if = "Option::is_none")]
1354 pub forced_colors: Option<String>,
1355
1356 /// Whether to allow sites to register Service workers
1357 #[serde(skip_serializing_if = "Option::is_none")]
1358 pub service_workers: Option<String>,
1359
1360 /// Options for recording HAR
1361 #[serde(skip_serializing_if = "Option::is_none")]
1362 pub record_har: Option<RecordHar>,
1363
1364 /// Options for recording video
1365 #[serde(skip_serializing_if = "Option::is_none")]
1366 pub record_video: Option<RecordVideo>,
1367}
1368
1369impl BrowserContextOptions {
1370 /// Creates a new builder for BrowserContextOptions
1371 pub fn builder() -> BrowserContextOptionsBuilder {
1372 BrowserContextOptionsBuilder::default()
1373 }
1374}
1375
1376/// Builder for BrowserContextOptions
1377#[derive(Debug, Clone, Default)]
1378pub struct BrowserContextOptionsBuilder {
1379 viewport: Option<Viewport>,
1380 no_viewport: Option<bool>,
1381 user_agent: Option<String>,
1382 locale: Option<String>,
1383 timezone_id: Option<String>,
1384 geolocation: Option<Geolocation>,
1385 permissions: Option<Vec<String>>,
1386 proxy: Option<ProxySettings>,
1387 color_scheme: Option<String>,
1388 has_touch: Option<bool>,
1389 is_mobile: Option<bool>,
1390 javascript_enabled: Option<bool>,
1391 offline: Option<bool>,
1392 accept_downloads: Option<bool>,
1393 bypass_csp: Option<bool>,
1394 ignore_https_errors: Option<bool>,
1395 device_scale_factor: Option<f64>,
1396 extra_http_headers: Option<HashMap<String, String>>,
1397 base_url: Option<String>,
1398 storage_state: Option<StorageState>,
1399 storage_state_path: Option<String>,
1400 // Launch options
1401 args: Option<Vec<String>>,
1402 channel: Option<String>,
1403 chromium_sandbox: Option<bool>,
1404 devtools: Option<bool>,
1405 downloads_path: Option<String>,
1406 executable_path: Option<String>,
1407 firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
1408 headless: Option<bool>,
1409 ignore_default_args: Option<IgnoreDefaultArgs>,
1410 slow_mo: Option<f64>,
1411 timeout: Option<f64>,
1412 traces_dir: Option<String>,
1413 strict_selectors: Option<bool>,
1414 reduced_motion: Option<String>,
1415 forced_colors: Option<String>,
1416 service_workers: Option<String>,
1417 record_har: Option<RecordHar>,
1418 record_video: Option<RecordVideo>,
1419}
1420
1421impl BrowserContextOptionsBuilder {
1422 /// Sets the viewport dimensions
1423 pub fn viewport(mut self, viewport: Viewport) -> Self {
1424 self.viewport = Some(viewport);
1425 self.no_viewport = None; // Clear no_viewport if setting viewport
1426 self
1427 }
1428
1429 /// Disables viewport emulation
1430 pub fn no_viewport(mut self, no_viewport: bool) -> Self {
1431 self.no_viewport = Some(no_viewport);
1432 if no_viewport {
1433 self.viewport = None; // Clear viewport if setting no_viewport
1434 }
1435 self
1436 }
1437
1438 /// Sets the user agent string
1439 pub fn user_agent(mut self, user_agent: String) -> Self {
1440 self.user_agent = Some(user_agent);
1441 self
1442 }
1443
1444 /// Sets the locale
1445 pub fn locale(mut self, locale: String) -> Self {
1446 self.locale = Some(locale);
1447 self
1448 }
1449
1450 /// Sets the timezone identifier
1451 pub fn timezone_id(mut self, timezone_id: String) -> Self {
1452 self.timezone_id = Some(timezone_id);
1453 self
1454 }
1455
1456 /// Sets the geolocation
1457 pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
1458 self.geolocation = Some(geolocation);
1459 self
1460 }
1461
1462 /// Sets the permissions to grant
1463 pub fn permissions(mut self, permissions: Vec<String>) -> Self {
1464 self.permissions = Some(permissions);
1465 self
1466 }
1467
1468 /// Sets the network proxy settings for this context.
1469 ///
1470 /// This allows routing all network traffic through a proxy server,
1471 /// useful for rotating proxies without creating new browsers.
1472 ///
1473 /// # Example
1474 ///
1475 /// ```ignore
1476 /// use playwright_rs::protocol::{BrowserContextOptions, ProxySettings};
1477 ///
1478 /// let options = BrowserContextOptions::builder()
1479 /// .proxy(ProxySettings {
1480 /// server: "http://proxy.example.com:8080".to_string(),
1481 /// bypass: Some(".example.com".to_string()),
1482 /// username: Some("user".to_string()),
1483 /// password: Some("pass".to_string()),
1484 /// })
1485 /// .build();
1486 /// ```
1487 ///
1488 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context>
1489 pub fn proxy(mut self, proxy: ProxySettings) -> Self {
1490 self.proxy = Some(proxy);
1491 self
1492 }
1493
1494 /// Sets the color scheme preference
1495 pub fn color_scheme(mut self, color_scheme: String) -> Self {
1496 self.color_scheme = Some(color_scheme);
1497 self
1498 }
1499
1500 /// Sets whether the viewport supports touch events
1501 pub fn has_touch(mut self, has_touch: bool) -> Self {
1502 self.has_touch = Some(has_touch);
1503 self
1504 }
1505
1506 /// Sets whether this is a mobile viewport
1507 pub fn is_mobile(mut self, is_mobile: bool) -> Self {
1508 self.is_mobile = Some(is_mobile);
1509 self
1510 }
1511
1512 /// Sets whether JavaScript is enabled
1513 pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
1514 self.javascript_enabled = Some(javascript_enabled);
1515 self
1516 }
1517
1518 /// Sets whether to emulate offline network
1519 pub fn offline(mut self, offline: bool) -> Self {
1520 self.offline = Some(offline);
1521 self
1522 }
1523
1524 /// Sets whether to automatically download attachments
1525 pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
1526 self.accept_downloads = Some(accept_downloads);
1527 self
1528 }
1529
1530 /// Sets whether to bypass Content-Security-Policy
1531 pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
1532 self.bypass_csp = Some(bypass_csp);
1533 self
1534 }
1535
1536 /// Sets whether to ignore HTTPS errors
1537 pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
1538 self.ignore_https_errors = Some(ignore_https_errors);
1539 self
1540 }
1541
1542 /// Sets the device scale factor
1543 pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
1544 self.device_scale_factor = Some(device_scale_factor);
1545 self
1546 }
1547
1548 /// Sets extra HTTP headers
1549 pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
1550 self.extra_http_headers = Some(extra_http_headers);
1551 self
1552 }
1553
1554 /// Sets the base URL for relative navigation
1555 pub fn base_url(mut self, base_url: String) -> Self {
1556 self.base_url = Some(base_url);
1557 self
1558 }
1559
1560 /// Sets the storage state inline (cookies, localStorage).
1561 ///
1562 /// Populates the browser context with the provided storage state, including
1563 /// cookies and local storage. This is useful for initializing a context with
1564 /// a saved authentication state.
1565 ///
1566 /// Mutually exclusive with `storage_state_path()`.
1567 ///
1568 /// # Example
1569 ///
1570 /// ```rust
1571 /// use playwright_rs::protocol::{BrowserContextOptions, Cookie, StorageState, Origin, LocalStorageItem};
1572 ///
1573 /// let storage_state = StorageState {
1574 /// cookies: vec![Cookie {
1575 /// name: "session_id".to_string(),
1576 /// value: "abc123".to_string(),
1577 /// domain: ".example.com".to_string(),
1578 /// path: "/".to_string(),
1579 /// expires: -1.0,
1580 /// http_only: true,
1581 /// secure: true,
1582 /// same_site: Some("Lax".to_string()),
1583 /// }],
1584 /// origins: vec![Origin {
1585 /// origin: "https://example.com".to_string(),
1586 /// local_storage: vec![LocalStorageItem {
1587 /// name: "user_prefs".to_string(),
1588 /// value: "{\"theme\":\"dark\"}".to_string(),
1589 /// }],
1590 /// }],
1591 /// };
1592 ///
1593 /// let options = BrowserContextOptions::builder()
1594 /// .storage_state(storage_state)
1595 /// .build();
1596 /// ```
1597 ///
1598 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1599 pub fn storage_state(mut self, storage_state: StorageState) -> Self {
1600 self.storage_state = Some(storage_state);
1601 self.storage_state_path = None; // Clear path if setting inline
1602 self
1603 }
1604
1605 /// Sets the storage state from a file path.
1606 ///
1607 /// The file should contain a JSON representation of StorageState with cookies
1608 /// and origins. This is useful for loading authentication state saved from a
1609 /// previous session.
1610 ///
1611 /// Mutually exclusive with `storage_state()`.
1612 ///
1613 /// # Example
1614 ///
1615 /// ```rust
1616 /// use playwright_rs::protocol::BrowserContextOptions;
1617 ///
1618 /// let options = BrowserContextOptions::builder()
1619 /// .storage_state_path("auth.json".to_string())
1620 /// .build();
1621 /// ```
1622 ///
1623 /// The file should have this format:
1624 /// ```json
1625 /// {
1626 /// "cookies": [{
1627 /// "name": "session_id",
1628 /// "value": "abc123",
1629 /// "domain": ".example.com",
1630 /// "path": "/",
1631 /// "expires": -1,
1632 /// "httpOnly": true,
1633 /// "secure": true,
1634 /// "sameSite": "Lax"
1635 /// }],
1636 /// "origins": [{
1637 /// "origin": "https://example.com",
1638 /// "localStorage": [{
1639 /// "name": "user_prefs",
1640 /// "value": "{\"theme\":\"dark\"}"
1641 /// }]
1642 /// }]
1643 /// }
1644 /// ```
1645 ///
1646 /// See: <https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state>
1647 pub fn storage_state_path(mut self, path: String) -> Self {
1648 self.storage_state_path = Some(path);
1649 self.storage_state = None; // Clear inline if setting path
1650 self
1651 }
1652
1653 /// Sets additional arguments to pass to browser instance (for launch_persistent_context)
1654 pub fn args(mut self, args: Vec<String>) -> Self {
1655 self.args = Some(args);
1656 self
1657 }
1658
1659 /// Sets browser distribution channel (for launch_persistent_context)
1660 pub fn channel(mut self, channel: String) -> Self {
1661 self.channel = Some(channel);
1662 self
1663 }
1664
1665 /// Enables or disables Chromium sandboxing (for launch_persistent_context)
1666 pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
1667 self.chromium_sandbox = Some(enabled);
1668 self
1669 }
1670
1671 /// Auto-open DevTools (for launch_persistent_context)
1672 pub fn devtools(mut self, enabled: bool) -> Self {
1673 self.devtools = Some(enabled);
1674 self
1675 }
1676
1677 /// Sets directory to save downloads (for launch_persistent_context)
1678 pub fn downloads_path(mut self, path: String) -> Self {
1679 self.downloads_path = Some(path);
1680 self
1681 }
1682
1683 /// Sets path to custom browser executable (for launch_persistent_context)
1684 pub fn executable_path(mut self, path: String) -> Self {
1685 self.executable_path = Some(path);
1686 self
1687 }
1688
1689 /// Sets Firefox user preferences (for launch_persistent_context, Firefox only)
1690 pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
1691 self.firefox_user_prefs = Some(prefs);
1692 self
1693 }
1694
1695 /// Run in headless mode (for launch_persistent_context)
1696 pub fn headless(mut self, enabled: bool) -> Self {
1697 self.headless = Some(enabled);
1698 self
1699 }
1700
1701 /// Filter or disable default browser arguments (for launch_persistent_context).
1702 ///
1703 /// When `IgnoreDefaultArgs::Bool(true)`, Playwright does not pass its own
1704 /// default arguments and only uses the ones from `args`.
1705 /// When `IgnoreDefaultArgs::Array(vec)`, filters out the given default arguments.
1706 ///
1707 /// See: <https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context>
1708 pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
1709 self.ignore_default_args = Some(args);
1710 self
1711 }
1712
1713 /// Slow down operations by N milliseconds (for launch_persistent_context)
1714 pub fn slow_mo(mut self, ms: f64) -> Self {
1715 self.slow_mo = Some(ms);
1716 self
1717 }
1718
1719 /// Set timeout for browser launch in milliseconds (for launch_persistent_context)
1720 pub fn timeout(mut self, ms: f64) -> Self {
1721 self.timeout = Some(ms);
1722 self
1723 }
1724
1725 /// Set directory to save traces (for launch_persistent_context)
1726 pub fn traces_dir(mut self, path: String) -> Self {
1727 self.traces_dir = Some(path);
1728 self
1729 }
1730
1731 /// Check if strict selectors mode is enabled
1732 pub fn strict_selectors(mut self, enabled: bool) -> Self {
1733 self.strict_selectors = Some(enabled);
1734 self
1735 }
1736
1737 /// Emulates 'prefers-reduced-motion' media feature
1738 pub fn reduced_motion(mut self, value: String) -> Self {
1739 self.reduced_motion = Some(value);
1740 self
1741 }
1742
1743 /// Emulates 'forced-colors' media feature
1744 pub fn forced_colors(mut self, value: String) -> Self {
1745 self.forced_colors = Some(value);
1746 self
1747 }
1748
1749 /// Whether to allow sites to register Service workers ("allow" | "block")
1750 pub fn service_workers(mut self, value: String) -> Self {
1751 self.service_workers = Some(value);
1752 self
1753 }
1754
1755 /// Sets options for recording HAR
1756 pub fn record_har(mut self, record_har: RecordHar) -> Self {
1757 self.record_har = Some(record_har);
1758 self
1759 }
1760
1761 /// Sets options for recording video
1762 pub fn record_video(mut self, record_video: RecordVideo) -> Self {
1763 self.record_video = Some(record_video);
1764 self
1765 }
1766
1767 /// Builds the BrowserContextOptions
1768 pub fn build(self) -> BrowserContextOptions {
1769 BrowserContextOptions {
1770 viewport: self.viewport,
1771 no_viewport: self.no_viewport,
1772 user_agent: self.user_agent,
1773 locale: self.locale,
1774 timezone_id: self.timezone_id,
1775 geolocation: self.geolocation,
1776 permissions: self.permissions,
1777 proxy: self.proxy,
1778 color_scheme: self.color_scheme,
1779 has_touch: self.has_touch,
1780 is_mobile: self.is_mobile,
1781 javascript_enabled: self.javascript_enabled,
1782 offline: self.offline,
1783 accept_downloads: self.accept_downloads,
1784 bypass_csp: self.bypass_csp,
1785 ignore_https_errors: self.ignore_https_errors,
1786 device_scale_factor: self.device_scale_factor,
1787 extra_http_headers: self.extra_http_headers,
1788 base_url: self.base_url,
1789 storage_state: self.storage_state,
1790 storage_state_path: self.storage_state_path,
1791 // Launch options
1792 args: self.args,
1793 channel: self.channel,
1794 chromium_sandbox: self.chromium_sandbox,
1795 devtools: self.devtools,
1796 downloads_path: self.downloads_path,
1797 executable_path: self.executable_path,
1798 firefox_user_prefs: self.firefox_user_prefs,
1799 headless: self.headless,
1800 ignore_default_args: self.ignore_default_args,
1801 slow_mo: self.slow_mo,
1802 timeout: self.timeout,
1803 traces_dir: self.traces_dir,
1804 strict_selectors: self.strict_selectors,
1805 reduced_motion: self.reduced_motion,
1806 forced_colors: self.forced_colors,
1807 service_workers: self.service_workers,
1808 record_har: self.record_har,
1809 record_video: self.record_video,
1810 }
1811 }
1812}
1813
1814/// Extracts timing data from a Response object's initializer, patching in
1815/// `responseEnd` from the event's `responseEndTiming` if available.
1816async fn extract_timing(
1817 connection: &std::sync::Arc<dyn crate::server::connection::ConnectionLike>,
1818 response_guid: Option<String>,
1819 response_end_timing: Option<f64>,
1820) -> Option<serde_json::Value> {
1821 let resp_guid = response_guid?;
1822 let resp_arc = connection.get_object(&resp_guid).await.ok()?;
1823 let resp_obj = resp_arc
1824 .as_any()
1825 .downcast_ref::<crate::protocol::ResponseObject>()?;
1826 let mut timing = resp_obj.initializer().get("timing")?.clone();
1827 if let (Some(end), Some(obj)) = (response_end_timing, timing.as_object_mut()) {
1828 if let Some(n) = serde_json::Number::from_f64(end) {
1829 obj.insert("responseEnd".to_string(), serde_json::Value::Number(n));
1830 }
1831 }
1832 Some(timing)
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837 use super::*;
1838 use crate::api::launch_options::IgnoreDefaultArgs;
1839
1840 #[test]
1841 fn test_browser_context_options_ignore_default_args_bool_serialization() {
1842 let options = BrowserContextOptions::builder()
1843 .ignore_default_args(IgnoreDefaultArgs::Bool(true))
1844 .build();
1845
1846 let value = serde_json::to_value(&options).unwrap();
1847 assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(true));
1848 }
1849
1850 #[test]
1851 fn test_browser_context_options_ignore_default_args_array_serialization() {
1852 let options = BrowserContextOptions::builder()
1853 .ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]))
1854 .build();
1855
1856 let value = serde_json::to_value(&options).unwrap();
1857 assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(["--foo"]));
1858 }
1859
1860 #[test]
1861 fn test_browser_context_options_ignore_default_args_absent() {
1862 let options = BrowserContextOptions::builder().build();
1863
1864 let value = serde_json::to_value(&options).unwrap();
1865 assert!(value.get("ignoreDefaultArgs").is_none());
1866 }
1867}