Skip to main content

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