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