playwright_cdp/browser_context.rs
1//! `BrowserContext` — an isolated context (incognito-style), owning pages,
2//! init scripts, cookies, and defaults.
3
4use crate::browser::Browser;
5use crate::api_request::APIRequestContext;
6use crate::cdp::session::CdpSession;
7use crate::download::Download;
8use crate::error::{Error, Result};
9use crate::options::NewContextOptions;
10use crate::page::Page;
11use crate::request::Request;
12use crate::response::Response;
13use crate::route::RouteEntry;
14use crate::route::{Route, RouteFulfillOptions, RouteHandler};
15use crate::types::{ConsoleMessage, Headers, Position};
16use crate::clock::{Clock, ClockInstallOptions};
17use crate::har::{routes_from_har, HarRoute, RouteFromHarOptions};
18use parking_lot::Mutex;
19use serde_json::{json, Value};
20use std::future::Future;
21use std::pin::Pin;
22use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
23use std::sync::Arc;
24use std::time::Duration;
25use tokio::sync::oneshot;
26
27type PageHandler = Arc<
28 dyn Fn(Page) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
29>;
30
31/// Erased handler for `Request`/`on_requestfailed` events.
32type RequestHandler =
33 Arc<dyn Fn(Request) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
34
35/// Erased handler for `Response` events.
36type ResponseHandler =
37 Arc<dyn Fn(Response) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
38
39/// Erased handler for `ConsoleMessage` events.
40type ConsoleHandler =
41 Arc<dyn Fn(ConsoleMessage) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
42
43/// Erased handler for `Download` events.
44type DownloadHandler =
45 Arc<dyn Fn(Download) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
46
47/// Erased handler for close events (no payload).
48type CloseHandler =
49 Arc<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
50
51/// Erased, callable exposed-binding: takes the JS args and returns a JSON value.
52type ExposedBindingHandler =
53 Arc<dyn Fn(Vec<Value>) -> Pin<Box<dyn Future<Output = Value> + Send>> + Send + Sync>;
54
55/// A named exposed function registered at the context level, applied to every
56/// page (existing and future). The `Arc` handler is cloned per page.
57#[derive(Clone)]
58struct ExposedFunction {
59 name: String,
60 handler: ExposedBindingHandler,
61}
62
63/// A browser context. The default context has `browser_context_id == None`;
64/// isolated contexts are created via [`Browser::new_context`](crate::Browser::new_context).
65#[derive(Clone)]
66pub struct BrowserContext {
67 inner: Arc<ContextInner>,
68}
69
70struct ContextInner {
71 browser: Browser,
72 browser_context_id: Option<String>,
73 init_scripts: Mutex<Vec<String>>,
74 default_timeout_ms: AtomicU64,
75 /// Per-page default navigation timeout (ms). Applied to existing + future
76 /// pages via `Page::set_default_navigation_timeout`.
77 default_navigation_timeout_ms: AtomicU64,
78 extra_http_headers: Mutex<Option<Headers>>,
79 user_agent: Mutex<Option<String>>,
80 viewport: Mutex<Option<crate::types::Viewport>>,
81 /// Geolocation override applied to existing + future pages.
82 geolocation: Mutex<Option<Position>>,
83 /// Offline-emulation flag applied to existing + future pages.
84 offline: Mutex<bool>,
85 pages: Mutex<Vec<Page>>,
86 on_page_handlers: Mutex<Vec<PageHandler>>,
87 /// Context-level route registry. Each entry is applied to every existing
88 /// page and to pages created after the route is registered.
89 context_routes: Mutex<Vec<RouteEntry>>,
90 /// Context-level exposed functions, applied to existing + future pages.
91 exposed_functions: Mutex<Vec<ExposedFunction>>,
92 utility_page: Mutex<Option<Page>>,
93 /// A single shared Tracing handle (browser-level session). `tracing()`
94 /// returns clones of this so start/stop land on the same state.
95 tracing: crate::Tracing,
96 /// Pending fake-timer install config registered at the context level.
97 /// When set, every page created via `new_page()` runs
98 /// `page.clock().install(...)` so context-level fake timers apply forward.
99 /// See [`BrowserContext::clock`] / [`BrowserContext::set_clock_install`].
100 pending_clock_install: Mutex<Option<ClockInstallOptions>>,
101 closed: Mutex<bool>,
102 /// `true` for the context returned by `launch_persistent_context`: its
103 /// [`close`](BrowserContext::close) owns the browser process and must tear
104 /// the whole browser down (matching Playwright's persistent-context
105 /// semantics). `false` for isolated contexts and the ephemeral default
106 /// handle used by `Browser::new_page`.
107 owns_browser: AtomicBool,
108
109 // --- Context-level event handler lists (fan out to every page) ---
110 on_close_handlers: Mutex<Vec<CloseHandler>>,
111 on_console_handlers: Mutex<Vec<ConsoleHandler>>,
112 on_request_handlers: Mutex<Vec<RequestHandler>>,
113 on_response_handlers: Mutex<Vec<ResponseHandler>>,
114 on_requestfailed_handlers: Mutex<Vec<RequestHandler>>,
115 on_download_handlers: Mutex<Vec<DownloadHandler>>,
116}
117
118/// Build an owned [`CdpSession`] that targets the given page's session.
119///
120/// `CdpSession` is not `Clone` and `Page::session()` returns a borrow, but
121/// [`Clock`] takes a session by value. This reconstructs a target-scoped session
122/// sharing the page's connection and session id — cheap (an `Arc` clone + an
123/// `Option<String>` clone) and pointed at the same CDP session.
124fn page_session_owned(page: &Page) -> CdpSession {
125 let session = page.session();
126 CdpSession::target(
127 Arc::clone(session.connection()),
128 session.session_id().unwrap_or(""),
129 )
130}
131
132impl BrowserContext {
133 /// Create a new isolated context (calls `Target.createBrowserContext`).
134 pub(crate) async fn create(browser: Browser, opts: Option<NewContextOptions>) -> Result<Self> {
135 let v = browser
136 .browser_session()
137 .send(
138 "Target.createBrowserContext",
139 json!({"disposeOnDetach": true}),
140 )
141 .await?;
142 let bcid = v
143 .get("browserContextId")
144 .and_then(|x| x.as_str())
145 .ok_or_else(|| Error::ProtocolError("createBrowserContext missing id".into()))?
146 .to_string();
147
148 let ctx = Self::build(browser.clone(), Some(bcid), opts);
149 browser.track_context(ctx.clone());
150 Ok(ctx)
151 }
152
153 /// Wrap the default browser context (no create call).
154 pub(crate) fn default_for(browser: Browser) -> Self {
155 Self::build(browser, None, None)
156 }
157
158 fn build(browser: Browser, bcid: Option<String>, opts: Option<NewContextOptions>) -> Self {
159 let opts = opts.unwrap_or_default();
160 let tracing = crate::Tracing::new(browser.new_browser_cdp_session());
161 Self {
162 inner: Arc::new(ContextInner {
163 browser,
164 browser_context_id: bcid,
165 init_scripts: Mutex::new(Vec::new()),
166 default_timeout_ms: AtomicU64::new(30_000),
167 default_navigation_timeout_ms: AtomicU64::new(30_000),
168 extra_http_headers: Mutex::new(opts.extra_http_headers),
169 user_agent: Mutex::new(opts.user_agent),
170 viewport: Mutex::new(opts.viewport),
171 geolocation: Mutex::new(None),
172 offline: Mutex::new(false),
173 pages: Mutex::new(Vec::new()),
174 on_page_handlers: Mutex::new(Vec::new()),
175 context_routes: Mutex::new(Vec::new()),
176 exposed_functions: Mutex::new(Vec::new()),
177 utility_page: Mutex::new(None),
178 tracing,
179 pending_clock_install: Mutex::new(None),
180 closed: Mutex::new(false),
181 owns_browser: AtomicBool::new(false),
182 on_close_handlers: Mutex::new(Vec::new()),
183 on_console_handlers: Mutex::new(Vec::new()),
184 on_request_handlers: Mutex::new(Vec::new()),
185 on_response_handlers: Mutex::new(Vec::new()),
186 on_requestfailed_handlers: Mutex::new(Vec::new()),
187 on_download_handlers: Mutex::new(Vec::new()),
188 }),
189 }
190 }
191
192 /// The CDP browser-context id, or `None` for the default context.
193 pub fn browser_context_id(&self) -> Option<&str> {
194 self.inner.browser_context_id.as_deref()
195 }
196
197 /// Open a new page in this context.
198 pub async fn new_page(&self) -> Result<Page> {
199 if *self.inner.closed.lock() {
200 return Err(Error::TargetClosed {
201 target_type: "context".into(),
202 context: "context already closed".into(),
203 });
204 }
205
206 let mut params = json!({"url": "about:blank"});
207 if let Some(id) = &self.inner.browser_context_id {
208 params["browserContextId"] = json!(id);
209 }
210 let resp = self
211 .inner
212 .browser
213 .browser_session()
214 .send("Target.createTarget", params)
215 .await?;
216 let target_id = resp
217 .get("targetId")
218 .and_then(|x| x.as_str())
219 .ok_or_else(|| Error::ProtocolError("createTarget missing targetId".into()))?
220 .to_string();
221
222 let attach = self
223 .inner
224 .browser
225 .browser_session()
226 .send(
227 "Target.attachToTarget",
228 json!({"targetId": target_id, "flatten": true}),
229 )
230 .await?;
231 let session_id = attach
232 .get("sessionId")
233 .and_then(|x| x.as_str())
234 .ok_or_else(|| Error::ProtocolError("attachToTarget missing sessionId".into()))?
235 .to_string();
236
237 let init_scripts = self.inner.init_scripts.lock().clone();
238 let headers = self.inner.extra_http_headers.lock().clone();
239 let ua = self.inner.user_agent.lock().clone();
240 let viewport = self.inner.viewport.lock().as_ref().copied();
241 let timeout_ms = self.inner.default_timeout_ms.load(Ordering::Relaxed);
242
243 let page = Page::attach(
244 self.inner.browser.clone(),
245 session_id,
246 target_id,
247 &init_scripts,
248 timeout_ms,
249 headers.as_ref(),
250 ua.as_deref(),
251 viewport,
252 )
253 .await?;
254
255 self.inner.pages.lock().push(page.clone());
256
257 // Fire on_page handlers.
258 let handlers = self.inner.on_page_handlers.lock().clone();
259 for h in handlers {
260 let p = page.clone();
261 tokio::spawn(async move {
262 (h)(p).await;
263 });
264 }
265
266 // Apply context-level routes to the newly created page so handlers
267 // registered before the page existed still intercept its requests.
268 let routes = self.inner.context_routes.lock().clone();
269 for entry in routes {
270 let handler = Arc::clone(&entry.handler);
271 page.route(&entry.pattern, move |r| {
272 let h = Arc::clone(&handler);
273 async move {
274 (h)(r).await;
275 }
276 })
277 .await?;
278 }
279
280 // Apply stored context defaults/state to the new page.
281 page.set_default_navigation_timeout(
282 self.inner
283 .default_navigation_timeout_ms
284 .load(Ordering::Relaxed),
285 );
286 // If a context-level fake-timer install was registered (via
287 // `clock().install()` against a placeholder, or `set_clock_install`),
288 // apply it to this fresh page so context-level fake timers carry over.
289 if let Some(clock_opts) = self.inner.pending_clock_install.lock().clone() {
290 // Best-effort: a clock install failure on a brand-new page is not
291 // fatal to page creation — log nothing but continue.
292 let _ = Clock::new(page_session_owned(&page))
293 .install(Some(clock_opts))
294 .await;
295 }
296 if let Some(geo) = self.inner.geolocation.lock().as_ref().copied() {
297 page.set_geolocation(Some((geo.x, geo.y))).await?;
298 }
299 if *self.inner.offline.lock() {
300 page.set_offline(true).await?;
301 }
302
303 // Re-expose any context-level functions on the new page.
304 let exposed = self.inner.exposed_functions.lock().clone();
305 for f in exposed {
306 let handler = Arc::clone(&f.handler);
307 page.expose_function(&f.name, move |args| {
308 let h = Arc::clone(&handler);
309 async move { (h)(args).await }
310 })
311 .await?;
312 }
313
314 // Wire context-level event handlers onto this page so each fires for
315 // any page in the context (Playwright semantics).
316 self.register_page_event_fanout(&page);
317
318 Ok(page)
319 }
320
321 /// Register the context's collected event handlers onto one page, so that
322 /// a context-level handler fires whenever that page emits the event.
323 /// Called for every page as it is created (existing pages registered at
324 /// `on_*`-registration time via `apply_event_fanout_to_existing`).
325 fn register_page_event_fanout(&self, page: &Page) {
326 // on_close
327 {
328 let handlers = self.inner.on_close_handlers.lock().clone();
329 if !handlers.is_empty() {
330 page.on_close(move || {
331 let hs = handlers.clone();
332 async move {
333 for h in hs {
334 (h)().await;
335 }
336 }
337 });
338 }
339 }
340 // on_console
341 {
342 let handlers = self.inner.on_console_handlers.lock().clone();
343 if !handlers.is_empty() {
344 page.on_console(move |msg| {
345 let hs = handlers.clone();
346 async move {
347 for h in hs {
348 (h)(msg.clone()).await;
349 }
350 }
351 });
352 }
353 }
354 // on_request
355 {
356 let handlers = self.inner.on_request_handlers.lock().clone();
357 if !handlers.is_empty() {
358 page.on_request(move |req| {
359 let hs = handlers.clone();
360 async move {
361 for h in hs {
362 (h)(req.clone()).await;
363 }
364 }
365 });
366 }
367 }
368 // on_response
369 {
370 let handlers = self.inner.on_response_handlers.lock().clone();
371 if !handlers.is_empty() {
372 page.on_response(move |resp| {
373 let hs = handlers.clone();
374 async move {
375 for h in hs {
376 (h)(resp.clone()).await;
377 }
378 }
379 });
380 }
381 }
382 // on_requestfailed
383 {
384 let handlers = self.inner.on_requestfailed_handlers.lock().clone();
385 if !handlers.is_empty() {
386 page.on_requestfailed(move |req| {
387 let hs = handlers.clone();
388 async move {
389 for h in hs {
390 (h)(req.clone()).await;
391 }
392 }
393 });
394 }
395 }
396 // on_download
397 {
398 let handlers = self.inner.on_download_handlers.lock().clone();
399 if !handlers.is_empty() {
400 page.on_download(move |dl| {
401 let hs = handlers.clone();
402 async move {
403 for h in hs {
404 (h)(dl.clone()).await;
405 }
406 }
407 });
408 }
409 }
410 }
411
412 /// Add a script evaluated before each document load (applied to new pages).
413 pub async fn add_init_script(&self, script: &str) -> Result<()> {
414 self.inner.init_scripts.lock().push(script.to_string());
415 // Retroactively apply to existing pages.
416 let pages = self.inner.pages.lock().clone();
417 for p in pages {
418 p.add_init_script(script).await?;
419 }
420 Ok(())
421 }
422
423 /// Set the default action timeout (ms) for pages in this context.
424 pub fn set_default_timeout(&self, timeout_ms: u64) {
425 self.inner.default_timeout_ms.store(timeout_ms, Ordering::Relaxed);
426 for p in self.inner.pages.lock().iter() {
427 p.set_default_timeout(timeout_ms);
428 }
429 }
430
431 /// Set extra HTTP headers sent on every request (applied to new + existing pages).
432 pub async fn set_extra_http_headers(&self, headers: Headers) -> Result<()> {
433 *self.inner.extra_http_headers.lock() = Some(headers.clone());
434 let pages = self.inner.pages.lock().clone();
435 for p in pages {
436 p.set_extra_http_headers(headers.clone()).await?;
437 }
438 Ok(())
439 }
440
441 /// Return a standalone HTTP client ([`APIRequestContext`]) tied to this
442 /// context. Its default headers are seeded (best-effort) from the
443 /// context's `extra_http_headers`. The client is otherwise independent of
444 /// the browser — it makes direct HTTP requests.
445 pub fn request(&self) -> APIRequestContext {
446 let headers = self
447 .inner
448 .extra_http_headers
449 .lock()
450 .clone()
451 .unwrap_or_default();
452 APIRequestContext::new(headers)
453 }
454
455 /// All pages opened in this context.
456 pub fn pages(&self) -> Vec<Page> {
457 self.inner.pages.lock().clone()
458 }
459
460 /// The owning browser.
461 pub fn browser(&self) -> Browser {
462 self.inner.browser.clone()
463 }
464
465 // --- Cookies (via the Storage domain, scoped by browserContextId) --------
466
467 /// Open (once) and cache a hidden about:blank page in this context, for
468 /// domain commands that need a page session.
469 #[allow(dead_code)]
470 async fn ensure_utility_page(&self) -> Result<Page> {
471 if let Some(p) = self.inner.utility_page.lock().clone() {
472 return Ok(p);
473 }
474 // Open a hidden about:blank page in this context for domain commands.
475 let page = self.new_page().await?;
476 *self.inner.utility_page.lock() = Some(page.clone());
477 Ok(page)
478 }
479
480 /// Add cookies to this context.
481 pub async fn add_cookies(&self, cookies: &[crate::types::Cookie]) -> Result<()> {
482 // Storage.setCookies with browserContextId is only valid on the
483 // browser-level session, so route it there rather than a page session.
484 let mut params = json!({ "cookies": serde_json::to_value(cookies)? });
485 if let Some(id) = &self.inner.browser_context_id {
486 params["browserContextId"] = json!(id);
487 }
488 self.inner
489 .browser
490 .browser_session()
491 .send("Storage.setCookies", params)
492 .await?;
493 Ok(())
494 }
495
496 /// Get cookies for this context.
497 pub async fn cookies(&self) -> Result<Vec<Value>> {
498 let mut params = json!({});
499 if let Some(id) = &self.inner.browser_context_id {
500 params["browserContextId"] = json!(id);
501 }
502 let v = self
503 .inner
504 .browser
505 .browser_session()
506 .send("Storage.getCookies", params)
507 .await?;
508 let cookies = v
509 .get("cookies")
510 .cloned()
511 .unwrap_or_else(|| Value::Array(vec![]));
512 Ok(cookies
513 .as_array()
514 .cloned()
515 .unwrap_or_default())
516 }
517
518 /// Clear cookies for this context.
519 pub async fn clear_cookies(&self) -> Result<()> {
520 let mut params = json!({});
521 if let Some(id) = &self.inner.browser_context_id {
522 params["browserContextId"] = json!(id);
523 }
524 self.inner
525 .browser
526 .browser_session()
527 .send("Storage.clearCookies", params)
528 .await?;
529 Ok(())
530 }
531
532 // --- Events --------------------------------------------------------------
533
534 /// Register a handler called for each new page in this context.
535 pub fn on_page<F, Fut>(&self, handler: F)
536 where
537 F: Fn(Page) -> Fut + Send + Sync + 'static,
538 Fut: Future<Output = ()> + Send + 'static,
539 {
540 let h: PageHandler = Arc::new(move |p| Box::pin(handler(p)));
541 self.inner.on_page_handlers.lock().push(h);
542 }
543
544 /// Close the context and all its pages. Disposes an isolated context.
545 pub async fn close(&self) -> Result<()> {
546 if *self.inner.closed.lock() {
547 return Ok(());
548 }
549 *self.inner.closed.lock() = true;
550
551 // Close pages.
552 let pages = std::mem::take(&mut *self.inner.pages.lock());
553 for p in pages {
554 let _ = p.close().await;
555 }
556 if let Some(_util) = self.inner.utility_page.lock().take() {
557 // already covered above if tracked; utility may not be in pages list
558 }
559
560 if let Some(bcid) = &self.inner.browser_context_id {
561 let _ = self
562 .inner
563 .browser
564 .browser_session()
565 .send("Target.disposeBrowserContext", json!({"browserContextId": bcid}))
566 .await;
567 }
568
569 // A persistent context (launch_persistent_context) owns the browser
570 // process: closing it must tear down the whole browser, like
571 // Playwright's persistent context. Isolated/ephemeral contexts leave
572 // the shared browser alive.
573 if self.inner.owns_browser.load(Ordering::SeqCst) {
574 let _ = self.inner.browser.close().await;
575 }
576 Ok(())
577 }
578
579 /// Mark this context as owning the underlying browser process, so that
580 /// [`close`](Self::close) tears the browser down. Used by
581 /// [`BrowserType::launch_persistent_context`].
582 pub(crate) fn mark_persistent(&self) {
583 self.inner.owns_browser.store(true, Ordering::SeqCst);
584 }
585
586 // ----- Permissions & storage -----
587
588 /// Grant browser permissions (e.g. `["geolocation", "clipboard-read"]`).
589 /// Applies browser-wide (CDP `Browser.grantPermissions` is not context-scoped).
590 pub async fn grant_permissions(&self, permissions: &[&str]) -> Result<()> {
591 self.inner
592 .browser
593 .browser_session()
594 .send(
595 "Browser.grantPermissions",
596 json!({ "permissions": permissions }),
597 )
598 .await
599 .map(|_: Value| ())
600 }
601
602 /// Reset all granted permissions.
603 pub async fn clear_permissions(&self) -> Result<()> {
604 self.inner
605 .browser
606 .browser_session()
607 .send("Browser.resetPermissions", json!({}))
608 .await
609 .map(|_: Value| ())
610 }
611
612 /// Snapshot storage state: cookies plus per-origin localStorage.
613 ///
614 /// For each page in the context, `localStorage` is captured grouped by
615 /// `location.origin`. Pages whose origin is `"null"` (e.g. `about:blank`,
616 /// `data:` URLs) are skipped.
617 pub async fn storage_state(&self) -> Result<crate::types::StorageState> {
618 let cookies = self.cookies().await?;
619
620 // Collect localStorage per unique origin across all pages.
621 let mut origins: Vec<crate::types::OriginStorage> = Vec::new();
622 for page in self.pages() {
623 // Capture { origin, entries } in one round-trip.
624 let v: Value = page
625 .evaluate(
626 "JSON.stringify({ origin: location.origin, entries: Object.entries(localStorage).map(([n,v]) => ({name:n, value:v})) })",
627 )
628 .await?;
629 // `evaluate` deserializes the JSON string into a Value (string). Parse it.
630 let parsed: Value = match &v {
631 Value::String(s) => serde_json::from_str(s).unwrap_or(Value::Null),
632 other => other.clone(),
633 };
634 let origin = parsed
635 .get("origin")
636 .and_then(|o| o.as_str())
637 .unwrap_or("")
638 .to_string();
639 // Skip null-opaque origins (about:blank, data:, etc.).
640 if origin.is_empty() || origin == "null" {
641 continue;
642 }
643 let entries = parsed
644 .get("entries")
645 .and_then(|e| e.as_array())
646 .cloned()
647 .unwrap_or_default();
648 let local_storage: Vec<crate::types::NameValue> = entries
649 .into_iter()
650 .filter_map(|e| {
651 Some(crate::types::NameValue {
652 name: e.get("name")?.as_str()?.to_string(),
653 value: e.get("value")?.as_str()?.to_string(),
654 })
655 })
656 .collect();
657
658 if let Some(existing) = origins.iter_mut().find(|o| o.origin == origin) {
659 // Merge: later pages win on key collisions.
660 for nv in local_storage {
661 if let Some(pos) = existing.localStorage.iter().position(|x| x.name == nv.name) {
662 existing.localStorage[pos].value = nv.value;
663 } else {
664 existing.localStorage.push(nv);
665 }
666 }
667 } else {
668 origins.push(crate::types::OriginStorage {
669 origin,
670 localStorage: local_storage,
671 });
672 }
673 }
674
675 Ok(crate::types::StorageState { cookies, origins })
676 }
677
678 /// Restore storage state previously captured by [`Self::storage_state`]:
679 /// cookies are added via the Storage domain, and each origin's localStorage
680 /// is set by evaluating `localStorage.setItem` on a page at that origin
681 /// (an existing page if one matches, otherwise a freshly created one).
682 pub async fn set_storage_state(
683 &self,
684 state: &crate::types::StorageState,
685 ) -> Result<()> {
686 // --- Cookies ---
687 if !state.cookies.is_empty() {
688 // The stored cookies are the raw Storage cookie objects; re-apply them
689 // through the Storage domain scoped to this context (browser-level
690 // session, since browserContextId is only valid there).
691 let mut params = json!({ "cookies": state.cookies });
692 if let Some(id) = &self.inner.browser_context_id {
693 params["browserContextId"] = json!(id);
694 }
695 self.inner
696 .browser
697 .browser_session()
698 .send("Storage.setCookies", params)
699 .await?;
700 }
701
702 // --- localStorage per origin ---
703 for origin_state in &state.origins {
704 if origin_state.localStorage.is_empty() {
705 continue;
706 }
707 // Find an existing page already at this origin, else create one.
708 let mut target: Option<Page> = None;
709 for p in self.pages() {
710 let p_origin: String = match p.evaluate::<String>("location.origin").await {
711 Ok(o) => o,
712 Err(_) => continue,
713 };
714 if p_origin == origin_state.origin {
715 target = Some(p);
716 break;
717 }
718 }
719 let page = match target {
720 Some(p) => p,
721 None => {
722 let p = self.new_page().await?;
723 let url = format!("{}/", origin_state.origin);
724 p.goto(&url, None).await?;
725 p
726 }
727 };
728
729 // Batch all setItem calls in a single eval with the entries as the arg.
730 let _: Value = page
731 .evaluate_with_arg(
732 "(() => { for (const {name, value} of arg) localStorage.setItem(name, value); return null; })()",
733 &origin_state.localStorage,
734 )
735 .await?;
736 }
737
738 Ok(())
739 }
740
741 // ----- Network interception (context-level routing) -----
742
743 /// Register a route that intercepts matching requests on ALL pages in this
744 /// context — including pages created after this call. The handler must
745 /// continue/fulfill/abort the [`Route`](crate::Route).
746 ///
747 /// A context route is applied to each page by registering the equivalent
748 /// page-level route on it. Context-level and page-level routes are
749 /// otherwise independent.
750 pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
751 where
752 F: Fn(Route) -> Fut + Send + Sync + 'static,
753 Fut: Future<Output = ()> + Send + 'static,
754 {
755 let handler: RouteHandler = Arc::new(move |r| Box::pin(handler(r)));
756 let entry = RouteEntry {
757 pattern: pattern.to_string(),
758 handler: Arc::clone(&handler),
759 };
760 self.inner.context_routes.lock().push(entry);
761
762 // Apply to every existing page, sharing the same handler Arc.
763 let pat = pattern.to_string();
764 for page in self.pages() {
765 let h = Arc::clone(&handler);
766 page.route(&pat, move |r| {
767 let h = Arc::clone(&h);
768 async move {
769 (h)(r).await;
770 }
771 })
772 .await?;
773 }
774 Ok(())
775 }
776
777 /// Remove the first context-level route registered with exactly `pattern`
778 /// and remove the matching page-level route on each page.
779 pub async fn unroute(&self, pattern: &str) -> Result<()> {
780 let mut routes = self.inner.context_routes.lock();
781 if let Some(pos) = routes.iter().position(|e| e.pattern == pattern) {
782 routes.remove(pos);
783 }
784 drop(routes);
785
786 for page in self.pages() {
787 page.unroute(pattern).await?;
788 }
789 Ok(())
790 }
791
792 /// Remove all context-level routes and all page-level routes on each page.
793 pub async fn unroute_all(&self) -> Result<()> {
794 self.inner.context_routes.lock().clear();
795 for page in self.pages() {
796 page.unroute_all().await?;
797 }
798 Ok(())
799 }
800
801 // ----- HAR replay (route_from_har) --------------------------------------
802
803 /// Replay a HAR file: register one route per HAR entry that serves the
804 /// recorded response for matching requests on every page (existing and
805 /// future). Mirrors Playwright's `context.route_from_har`.
806 ///
807 /// Behavior:
808 /// - Each HAR entry becomes a context-level [`route`](Self::route) with the
809 /// entry's recorded URL as the pattern. On a match (HTTP method + URL),
810 /// the handler serves the canned response via
811 /// [`Route::fulfill`](crate::Route::fulfill) and returns; non-matching
812 /// requests fall through to the network (the handler calls
813 /// [`Route::fallback`](crate::Route::fallback)).
814 /// - If `options.url` is set, only entries whose recorded URL matches that
815 /// glob are installed; the rest are skipped (their requests go to network).
816 /// - If `options.update` is `Some(true)`, the file is *not* replayed:
817 /// nothing is fulfilled and all requests fall through to the network.
818 /// (Full HAR recording is deferred — see the module docs of
819 /// [`crate::har`].)
820 pub async fn route_from_har(
821 &self,
822 har_path: impl AsRef<std::path::Path>,
823 options: Option<RouteFromHarOptions>,
824 ) -> Result<()> {
825 let opts = options;
826 let parsed = routes_from_har(har_path.as_ref(), opts.as_ref())?;
827
828 // In update mode we deliberately do not fulfill from the HAR; let every
829 // request hit the network. Recording itself is deferred.
830 let update = opts.as_ref().and_then(|o| o.update).unwrap_or(false);
831
832 for HarRoute {
833 method,
834 pattern,
835 status,
836 headers,
837 body,
838 } in parsed
839 {
840 let method = method;
841 let status = status;
842 let headers = headers.clone();
843 let body = body.clone();
844 let update = update;
845 self.route(&pattern, move |route: Route| {
846 let method = method.clone();
847 let status = status;
848 let headers = headers.clone();
849 let body = body.clone();
850 async move {
851 if update {
852 // Update mode: never fulfill; let the request proceed.
853 let _ = route.fallback().await;
854 return;
855 }
856 // Match the HTTP method (case-insensitive). A mismatched
857 // method means this entry does not own the request — let it
858 // fall through to the next route / the network.
859 if !route.request().method().eq_ignore_ascii_case(&method) {
860 let _ = route.fallback().await;
861 return;
862 }
863 let opts = RouteFulfillOptions {
864 status: Some(status),
865 headers: Some(headers),
866 content_type: None,
867 body: Some(body),
868 };
869 // If fulfill fails for any reason, attempt to continue the
870 // request rather than leaving it stalled.
871 if route.fulfill(opts).await.is_err() {
872 let _ = route.fallback().await;
873 }
874 }
875 })
876 .await?;
877 }
878 Ok(())
879 }
880
881 /// A performance tracing handle bound to the browser-level CDP session.
882 ///
883 /// Mirrors Playwright's `context.tracing()`. The Tracing domain is
884 /// browser-level; `tracing()` returns clones of a single shared handle so
885 /// [`Tracing::start`] and [`Tracing::stop`] operate on the same state.
886 pub fn tracing(&self) -> crate::Tracing {
887 self.inner.tracing.clone()
888 }
889
890 // ----- Fake timers (context clock) --------------------------------------
891 //
892 // Playwright's `context.clock()` controls fake timers for *all* pages in the
893 // context. CDP has no fake-timer primitive, so fake timers are injected
894 // per-page (see [`crate::clock`]); there is no single session that drives
895 // every page. The approach here is:
896 //
897 // 1. `clock()` returns a [`Clock`] bound to the *first* existing page's
898 // session — so calls like `clock().advance(ms)` / `clock().tick(ms)`
899 // drive that page's virtual clock.
900 // 2. To make context-level fake timers apply to pages created *after* the
901 // install, store a pending install config in `ContextInner` and replay
902 // it in `new_page()` via `page.clock().install(...)`.
903 //
904 // Limitation: `clock()` drives only the first page at the moment of the
905 // call; it does not fan out `advance`/`tick` to every existing page. The
906 // install (initial seeding) does apply forward to new pages. See report.
907
908 /// Return a fake-timer handle for this context, bound to the first existing
909 /// page's session. If no page exists yet, returns a [`Clock`] bound to a
910 /// brand-new utility page's session (created lazily); the install options
911 /// are *not* applied here — call [`Clock::install`] explicitly or use
912 /// [`Self::set_clock_install`] to seed future pages.
913 ///
914 /// Because fake timers are page-scoped, calls on the returned [`Clock`] only
915 /// affect the page it is bound to (the first page). To install fake timers
916 /// across all future pages, use [`Self::set_clock_install`].
917 pub async fn clock(&self) -> Result<Clock> {
918 let pages = self.pages();
919 let session = if let Some(first) = pages.into_iter().next() {
920 page_session_owned(&first)
921 } else {
922 // No page yet: open a (cached) utility page and bind to it so the
923 // returned Clock is usable. New pages created later still need
924 // `set_clock_install` to inherit the fakes.
925 let page = self.ensure_utility_page().await?;
926 page_session_owned(&page)
927 };
928 Ok(Clock::new(session))
929 }
930
931 /// Register fake-timer install options to be applied to every page created
932 /// after this call (and the first existing page immediately). This is the
933 /// forward-application mechanism for context-level fake timers.
934 ///
935 /// Returns the result of installing on the first existing page (if any);
936 /// future pages pick up the config in `new_page()`.
937 pub async fn set_clock_install(
938 &self,
939 options: Option<ClockInstallOptions>,
940 ) -> Result<()> {
941 let opts = options.clone();
942 *self.inner.pending_clock_install.lock() = opts;
943 // Apply to the first existing page immediately so the caller does not
944 // have to create a page first to get the install to take effect.
945 if let Some(first) = self.pages().into_iter().next() {
946 Clock::new(page_session_owned(&first))
947 .install(options.clone())
948 .await?;
949 }
950 Ok(())
951 }
952
953 // ----- Context-level event handlers (fan out to every page) -------------
954 //
955 // Each handler fires when *any* page in the context emits the event. The
956 // handler list is stored here and mirrored onto each page at creation and
957 // at registration time. Events Page does not expose (`on_weberror`,
958 // `on_serviceworker`) are not yet wired — see the report notes.
959
960 /// Register a handler invoked when any page in the context closes.
961 pub fn on_close<F, Fut>(&self, handler: F)
962 where
963 F: Fn() -> Fut + Send + Sync + 'static,
964 Fut: Future<Output = ()> + Send + 'static,
965 {
966 let h: CloseHandler = Arc::new(move || Box::pin(handler()));
967 self.inner.on_close_handlers.lock().push(h.clone());
968 for page in self.pages() {
969 let h = h.clone();
970 page.on_close(move || {
971 let hh = h.clone();
972 async move { (hh)().await }
973 });
974 }
975 }
976
977 /// Register a handler invoked for every console message on any page.
978 pub fn on_console<F, Fut>(&self, handler: F)
979 where
980 F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
981 Fut: Future<Output = ()> + Send + 'static,
982 {
983 let h: ConsoleHandler = Arc::new(move |m| Box::pin(handler(m)));
984 self.inner.on_console_handlers.lock().push(h.clone());
985 for page in self.pages() {
986 let h = h.clone();
987 page.on_console(move |m| {
988 let hh = h.clone();
989 async move { (hh)(m).await }
990 });
991 }
992 }
993
994 /// Register a handler invoked for every request sent by any page.
995 pub fn on_request<F, Fut>(&self, handler: F)
996 where
997 F: Fn(Request) -> Fut + Send + Sync + 'static,
998 Fut: Future<Output = ()> + Send + 'static,
999 {
1000 let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1001 self.inner.on_request_handlers.lock().push(h.clone());
1002 for page in self.pages() {
1003 let h = h.clone();
1004 page.on_request(move |r| {
1005 let hh = h.clone();
1006 async move { (hh)(r).await }
1007 });
1008 }
1009 }
1010
1011 /// Register a handler invoked for every response received on any page.
1012 pub fn on_response<F, Fut>(&self, handler: F)
1013 where
1014 F: Fn(Response) -> Fut + Send + Sync + 'static,
1015 Fut: Future<Output = ()> + Send + 'static,
1016 {
1017 let h: ResponseHandler = Arc::new(move |r| Box::pin(handler(r)));
1018 self.inner.on_response_handlers.lock().push(h.clone());
1019 for page in self.pages() {
1020 let h = h.clone();
1021 page.on_response(move |r| {
1022 let hh = h.clone();
1023 async move { (hh)(r).await }
1024 });
1025 }
1026 }
1027
1028 /// Wait for the context to produce a new page (via `new_page` or a
1029 /// `window.open` popup), returning it. Times out after `timeout`
1030 /// (default 30s) with [`Error::Timeout`].
1031 ///
1032 /// A one-shot handler is registered with [`on_page`](Self::on_page); it
1033 /// sends the first page over a `tokio::sync::oneshot` channel and then
1034 /// drains its sender to `None`, so subsequent events are no-ops. The
1035 /// handler is *not* removed from the handler list after it fires — it
1036 /// simply has nothing to send — so it is idempotent and harmless to leave
1037 /// in place (one extra closure in a `Vec`, no side effects).
1038 ///
1039 /// Typical use races the wait against the action that opens the page:
1040 ///
1041 /// ```no_run
1042 /// # use std::time::Duration;
1043 /// # async fn example(ctx: &playwright_cdp::BrowserContext) {
1044 /// let (page, _) = tokio::join!(
1045 /// ctx.expect_page(Some(Duration::from_secs(5))),
1046 /// ctx.new_page(),
1047 /// );
1048 /// page.unwrap().close().await.ok();
1049 /// # }
1050 /// ```
1051 pub async fn expect_page(&self, timeout: Option<Duration>) -> Result<Page> {
1052 let (tx, rx) = oneshot::channel::<Page>();
1053 let tx = Arc::new(Mutex::new(Some(tx)));
1054 self.on_page(move |page| {
1055 let tx = Arc::clone(&tx);
1056 Box::pin(async move {
1057 if let Some(sender) = tx.lock().take() {
1058 let _ = sender.send(page);
1059 }
1060 })
1061 });
1062 match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1063 Ok(Ok(page)) => Ok(page),
1064 Ok(Err(_)) => Err(Error::Timeout(
1065 "expect_page: oneshot sender dropped before a page arrived".into(),
1066 )),
1067 Err(_) => Err(Error::Timeout(
1068 "expect_page: no page opened within the timeout".into(),
1069 )),
1070 }
1071 }
1072
1073 /// Wait for a request whose URL contains `url_predicate` on any page in
1074 /// this context, returning it. Times out after `timeout` (default 30s)
1075 /// with [`Error::Timeout`].
1076 ///
1077 /// The matcher is a plain substring test against the request URL
1078 /// (`str::contains`), not a regex or glob — keep predicates literal.
1079 ///
1080 /// A one-shot handler is registered with [`on_request`](Self::on_request);
1081 /// it sends the first matching event over a `tokio::sync::oneshot` channel
1082 /// and then drains its sender to `None`, so subsequent events are no-ops.
1083 /// The handler is *not* removed from the handler list after it fires — it
1084 /// simply has nothing to send — so it is idempotent and harmless to leave
1085 /// in place (one extra closure in a `Vec`, no side effects).
1086 ///
1087 /// Typical use races the wait against the navigation that triggers it:
1088 ///
1089 /// ```no_run
1090 /// # use std::time::Duration;
1091 /// # async fn example(ctx: &playwright_cdp::BrowserContext, page: &playwright_cdp::Page, url: &str) {
1092 /// let (req, _) = tokio::join!(
1093 /// ctx.expect_request("127.0.0.1", Some(Duration::from_secs(5))),
1094 /// page.goto(url, None),
1095 /// );
1096 /// req.unwrap().url().to_string();
1097 /// # }
1098 /// ```
1099 pub async fn expect_request(
1100 &self,
1101 url_predicate: &str,
1102 timeout: Option<Duration>,
1103 ) -> Result<Request> {
1104 let (tx, rx) = oneshot::channel::<Request>();
1105 let tx = Arc::new(Mutex::new(Some(tx)));
1106 // Own the predicate so the `'static` handler closure can capture it.
1107 let url_predicate = url_predicate.to_string();
1108 self.on_request({
1109 // Move a separate clone into the closure so the outer binding
1110 // remains available for the timeout message below.
1111 let url_predicate = url_predicate.clone();
1112 move |req| {
1113 let tx = Arc::clone(&tx);
1114 // The `Fn` closure may fire many times; re-clone per call so the
1115 // owned predicate is never consumed.
1116 let url_predicate = url_predicate.clone();
1117 Box::pin(async move {
1118 if req.url().contains(&url_predicate) {
1119 if let Some(sender) = tx.lock().take() {
1120 let _ = sender.send(req);
1121 }
1122 }
1123 })
1124 }
1125 });
1126 match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1127 Ok(Ok(req)) => Ok(req),
1128 Ok(Err(_)) => Err(Error::Timeout(
1129 "expect_request: oneshot sender dropped before a match arrived".into(),
1130 )),
1131 Err(_) => Err(Error::Timeout(format!(
1132 "expect_request: no request matched {url_predicate:?} within the timeout"
1133 ))),
1134 }
1135 }
1136
1137 /// Wait for a console message whose text contains `text_predicate` on any
1138 /// page in this context, returning it. Times out after `timeout`
1139 /// (default 30s) with [`Error::Timeout`].
1140 ///
1141 /// The matcher is a plain substring test against the message text
1142 /// (`str::contains`), not a regex or glob — keep predicates literal.
1143 ///
1144 /// A one-shot handler is registered with [`on_console`](Self::on_console);
1145 /// it sends the first matching event over a `tokio::sync::oneshot` channel
1146 /// and then drains its sender to `None`, so subsequent events are no-ops.
1147 /// The handler is *not* removed from the handler list after it fires — it
1148 /// simply has nothing to send — so it is idempotent and harmless to leave
1149 /// in place (one extra closure in a `Vec`, no side effects).
1150 ///
1151 /// Typical use races the wait against the action that logs the message:
1152 ///
1153 /// ```no_run
1154 /// # use std::time::Duration;
1155 /// # async fn example(ctx: &playwright_cdp::BrowserContext, page: &playwright_cdp::Page) {
1156 /// let (msg, _) = tokio::join!(
1157 /// ctx.expect_console("hello", Some(Duration::from_secs(5))),
1158 /// page.evaluate::<()>("console.log('hello world')"),
1159 /// );
1160 /// msg.unwrap().text().to_string();
1161 /// # }
1162 /// ```
1163 pub async fn expect_console(
1164 &self,
1165 text_predicate: &str,
1166 timeout: Option<Duration>,
1167 ) -> Result<ConsoleMessage> {
1168 let (tx, rx) = oneshot::channel::<ConsoleMessage>();
1169 let tx = Arc::new(Mutex::new(Some(tx)));
1170 // Own the predicate so the `'static` handler closure can capture it.
1171 let text_predicate = text_predicate.to_string();
1172 self.on_console({
1173 // Move a separate clone into the closure so the outer binding
1174 // remains available for the timeout message below.
1175 let text_predicate = text_predicate.clone();
1176 move |msg| {
1177 let tx = Arc::clone(&tx);
1178 // The `Fn` closure may fire many times; re-clone per call so the
1179 // owned predicate is never consumed.
1180 let text_predicate = text_predicate.clone();
1181 Box::pin(async move {
1182 if msg.text().contains(&text_predicate) {
1183 if let Some(sender) = tx.lock().take() {
1184 let _ = sender.send(msg);
1185 }
1186 }
1187 })
1188 }
1189 });
1190 match tokio::time::timeout(timeout.unwrap_or_else(|| Duration::from_secs(30)), rx).await {
1191 Ok(Ok(msg)) => Ok(msg),
1192 Ok(Err(_)) => Err(Error::Timeout(
1193 "expect_console: oneshot sender dropped before a match arrived".into(),
1194 )),
1195 Err(_) => Err(Error::Timeout(format!(
1196 "expect_console: no console message matched {text_predicate:?} within the timeout"
1197 ))),
1198 }
1199 }
1200
1201 /// Register a handler invoked when any request from any page fails.
1202 pub fn on_requestfailed<F, Fut>(&self, handler: F)
1203 where
1204 F: Fn(Request) -> Fut + Send + Sync + 'static,
1205 Fut: Future<Output = ()> + Send + 'static,
1206 {
1207 let h: RequestHandler = Arc::new(move |r| Box::pin(handler(r)));
1208 self.inner.on_requestfailed_handlers.lock().push(h.clone());
1209 for page in self.pages() {
1210 let h = h.clone();
1211 page.on_requestfailed(move |r| {
1212 let hh = h.clone();
1213 async move { (hh)(r).await }
1214 });
1215 }
1216 }
1217
1218 /// Register a handler invoked for each file download on any page.
1219 pub fn on_download<F, Fut>(&self, handler: F)
1220 where
1221 F: Fn(Download) -> Fut + Send + Sync + 'static,
1222 Fut: Future<Output = ()> + Send + 'static,
1223 {
1224 let h: DownloadHandler = Arc::new(move |d| Box::pin(handler(d)));
1225 self.inner.on_download_handlers.lock().push(h.clone());
1226 for page in self.pages() {
1227 let h = h.clone();
1228 page.on_download(move |d| {
1229 let hh = h.clone();
1230 async move { (hh)(d).await }
1231 });
1232 }
1233 }
1234
1235 // ----- Emulation / state setters (existing + future pages) --------------
1236
1237 /// Override geolocation for all current and future pages, or clear it with
1238 /// `None`.
1239 ///
1240 /// **Mapping note:** [`Position`] is the crate's 2D point type
1241 /// (`{ x, y }`). For geolocation its components are interpreted as
1242 /// `{ x => latitude, y => longitude }` when forwarded to
1243 /// [`Page::set_geolocation`].
1244 pub async fn set_geolocation(&self, geolocation: Option<Position>) -> Result<()> {
1245 *self.inner.geolocation.lock() = geolocation;
1246 let geo = geolocation.map(|p| (p.x, p.y));
1247 for page in self.pages() {
1248 page.set_geolocation(geo).await?;
1249 }
1250 Ok(())
1251 }
1252
1253 /// Toggle network-offline emulation for all current and future pages.
1254 pub async fn set_offline(&self, offline: bool) -> Result<()> {
1255 *self.inner.offline.lock() = offline;
1256 for page in self.pages() {
1257 page.set_offline(offline).await?;
1258 }
1259 Ok(())
1260 }
1261
1262 /// Set the default navigation timeout (ms) for all current and future
1263 /// pages.
1264 pub fn set_default_navigation_timeout(&self, ms: u64) {
1265 self.inner
1266 .default_navigation_timeout_ms
1267 .store(ms, Ordering::Relaxed);
1268 for page in self.inner.pages.lock().iter() {
1269 page.set_default_navigation_timeout(ms);
1270 }
1271 }
1272
1273 /// Expose a Rust function to every page in the context as
1274 /// `window[name]`, including pages created after this call.
1275 ///
1276 /// The callback signature matches [`Page::expose_function`]: it receives
1277 /// the JS arguments as a `Vec<serde_json::Value>` and returns a
1278 /// `serde_json::Value`.
1279 pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
1280 where
1281 F: Fn(Vec<Value>) -> Fut + Send + Sync + 'static,
1282 Fut: Future<Output = Value> + Send + 'static,
1283 {
1284 let handler: ExposedBindingHandler = Arc::new(move |args| Box::pin(callback(args)));
1285 self.inner.exposed_functions.lock().push(ExposedFunction {
1286 name: name.to_string(),
1287 handler: Arc::clone(&handler),
1288 });
1289 let name = name.to_string();
1290 for page in self.pages() {
1291 let h = Arc::clone(&handler);
1292 page.expose_function(&name, move |args| {
1293 let hh = Arc::clone(&h);
1294 async move { (hh)(args).await }
1295 })
1296 .await?;
1297 }
1298 Ok(())
1299 }
1300
1301 /// Attach a fresh CDP session to the given page's target and return it.
1302 ///
1303 /// Sends `Target.attachToTarget` with `{ targetId, flatten: true }` over
1304 /// the browser-level session, then wraps the returned `sessionId` in a
1305 /// target-scoped [`CdpSession`] bound to the same connection.
1306 pub async fn new_cdp_session(&self, page: &Page) -> Result<CdpSession> {
1307 let browser_session = self.inner.browser.browser_session();
1308 let conn = Arc::clone(browser_session.connection());
1309 let resp: Value = browser_session
1310 .send(
1311 "Target.attachToTarget",
1312 json!({ "targetId": page.target_id(), "flatten": true }),
1313 )
1314 .await?;
1315 let session_id = resp
1316 .get("sessionId")
1317 .and_then(|v| v.as_str())
1318 .ok_or_else(|| {
1319 Error::ProtocolError("attachToTarget missing sessionId".into())
1320 })?
1321 .to_string();
1322 Ok(CdpSession::target(conn, session_id))
1323 }
1324}