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::error::{Error, Result};
7use crate::options::NewContextOptions;
8use crate::page::Page;
9use crate::route::RouteEntry;
10use crate::route::{Route, RouteHandler};
11use crate::types::Headers;
12use parking_lot::Mutex;
13use serde_json::{json, Value};
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::Arc;
18
19type PageHandler = Arc<
20    dyn Fn(Page) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
21>;
22
23/// A browser context. The default context has `browser_context_id == None`;
24/// isolated contexts are created via [`Browser::new_context`](crate::Browser::new_context).
25#[derive(Clone)]
26pub struct BrowserContext {
27    inner: Arc<ContextInner>,
28}
29
30struct ContextInner {
31    browser: Browser,
32    browser_context_id: Option<String>,
33    init_scripts: Mutex<Vec<String>>,
34    default_timeout_ms: AtomicU64,
35    extra_http_headers: Mutex<Option<Headers>>,
36    user_agent: Mutex<Option<String>>,
37    viewport: Mutex<Option<crate::types::Viewport>>,
38    pages: Mutex<Vec<Page>>,
39    on_page_handlers: Mutex<Vec<PageHandler>>,
40    /// Context-level route registry. Each entry is applied to every existing
41    /// page and to pages created after the route is registered.
42    context_routes: Mutex<Vec<RouteEntry>>,
43    utility_page: Mutex<Option<Page>>,
44    /// A single shared Tracing handle (browser-level session). `tracing()`
45    /// returns clones of this so start/stop land on the same state.
46    tracing: crate::Tracing,
47    closed: Mutex<bool>,
48}
49
50impl BrowserContext {
51    /// Create a new isolated context (calls `Target.createBrowserContext`).
52    pub(crate) async fn create(browser: Browser, opts: Option<NewContextOptions>) -> Result<Self> {
53        let v = browser
54            .browser_session()
55            .send(
56                "Target.createBrowserContext",
57                json!({"disposeOnDetach": true}),
58            )
59            .await?;
60        let bcid = v
61            .get("browserContextId")
62            .and_then(|x| x.as_str())
63            .ok_or_else(|| Error::ProtocolError("createBrowserContext missing id".into()))?
64            .to_string();
65
66        let ctx = Self::build(browser.clone(), Some(bcid), opts);
67        browser.track_context(ctx.clone());
68        Ok(ctx)
69    }
70
71    /// Wrap the default browser context (no create call).
72    pub(crate) fn default_for(browser: Browser) -> Self {
73        Self::build(browser, None, None)
74    }
75
76    fn build(browser: Browser, bcid: Option<String>, opts: Option<NewContextOptions>) -> Self {
77        let opts = opts.unwrap_or_default();
78        let tracing = crate::Tracing::new(browser.new_browser_cdp_session());
79        Self {
80            inner: Arc::new(ContextInner {
81                browser,
82                browser_context_id: bcid,
83                init_scripts: Mutex::new(Vec::new()),
84                default_timeout_ms: AtomicU64::new(30_000),
85                extra_http_headers: Mutex::new(opts.extra_http_headers),
86                user_agent: Mutex::new(opts.user_agent),
87                viewport: Mutex::new(opts.viewport),
88                pages: Mutex::new(Vec::new()),
89                on_page_handlers: Mutex::new(Vec::new()),
90                context_routes: Mutex::new(Vec::new()),
91                utility_page: Mutex::new(None),
92                tracing,
93                closed: Mutex::new(false),
94            }),
95        }
96    }
97
98    /// The CDP browser-context id, or `None` for the default context.
99    pub fn browser_context_id(&self) -> Option<&str> {
100        self.inner.browser_context_id.as_deref()
101    }
102
103    /// Open a new page in this context.
104    pub async fn new_page(&self) -> Result<Page> {
105        if *self.inner.closed.lock() {
106            return Err(Error::TargetClosed {
107                target_type: "context".into(),
108                context: "context already closed".into(),
109            });
110        }
111
112        let mut params = json!({"url": "about:blank"});
113        if let Some(id) = &self.inner.browser_context_id {
114            params["browserContextId"] = json!(id);
115        }
116        let resp = self
117            .inner
118            .browser
119            .browser_session()
120            .send("Target.createTarget", params)
121            .await?;
122        let target_id = resp
123            .get("targetId")
124            .and_then(|x| x.as_str())
125            .ok_or_else(|| Error::ProtocolError("createTarget missing targetId".into()))?
126            .to_string();
127
128        let attach = self
129            .inner
130            .browser
131            .browser_session()
132            .send(
133                "Target.attachToTarget",
134                json!({"targetId": target_id, "flatten": true}),
135            )
136            .await?;
137        let session_id = attach
138            .get("sessionId")
139            .and_then(|x| x.as_str())
140            .ok_or_else(|| Error::ProtocolError("attachToTarget missing sessionId".into()))?
141            .to_string();
142
143        let init_scripts = self.inner.init_scripts.lock().clone();
144        let headers = self.inner.extra_http_headers.lock().clone();
145        let ua = self.inner.user_agent.lock().clone();
146        let viewport = self.inner.viewport.lock().as_ref().copied();
147        let timeout_ms = self.inner.default_timeout_ms.load(Ordering::Relaxed);
148
149        let page = Page::attach(
150            self.inner.browser.clone(),
151            session_id,
152            target_id,
153            &init_scripts,
154            timeout_ms,
155            headers.as_ref(),
156            ua.as_deref(),
157            viewport,
158        )
159        .await?;
160
161        self.inner.pages.lock().push(page.clone());
162
163        // Fire on_page handlers.
164        let handlers = self.inner.on_page_handlers.lock().clone();
165        for h in handlers {
166            let p = page.clone();
167            tokio::spawn(async move {
168                (h)(p).await;
169            });
170        }
171
172        // Apply context-level routes to the newly created page so handlers
173        // registered before the page existed still intercept its requests.
174        let routes = self.inner.context_routes.lock().clone();
175        for entry in routes {
176            let handler = Arc::clone(&entry.handler);
177            page.route(&entry.pattern, move |r| {
178                let h = Arc::clone(&handler);
179                async move {
180                    (h)(r).await;
181                }
182            })
183            .await?;
184        }
185
186        Ok(page)
187    }
188
189    /// Add a script evaluated before each document load (applied to new pages).
190    pub async fn add_init_script(&self, script: &str) -> Result<()> {
191        self.inner.init_scripts.lock().push(script.to_string());
192        // Retroactively apply to existing pages.
193        let pages = self.inner.pages.lock().clone();
194        for p in pages {
195            p.add_init_script(script).await?;
196        }
197        Ok(())
198    }
199
200    /// Set the default action timeout (ms) for pages in this context.
201    pub fn set_default_timeout(&self, timeout_ms: u64) {
202        self.inner.default_timeout_ms.store(timeout_ms, Ordering::Relaxed);
203        for p in self.inner.pages.lock().iter() {
204            p.set_default_timeout(timeout_ms);
205        }
206    }
207
208    /// Set extra HTTP headers sent on every request (applied to new + existing pages).
209    pub async fn set_extra_http_headers(&self, headers: Headers) -> Result<()> {
210        *self.inner.extra_http_headers.lock() = Some(headers.clone());
211        let pages = self.inner.pages.lock().clone();
212        for p in pages {
213            p.set_extra_http_headers(headers.clone()).await?;
214        }
215        Ok(())
216    }
217
218    /// Return a standalone HTTP client ([`APIRequestContext`]) tied to this
219    /// context. Its default headers are seeded (best-effort) from the
220    /// context's `extra_http_headers`. The client is otherwise independent of
221    /// the browser — it makes direct HTTP requests.
222    pub fn request(&self) -> APIRequestContext {
223        let headers = self
224            .inner
225            .extra_http_headers
226            .lock()
227            .clone()
228            .unwrap_or_default();
229        APIRequestContext::new(headers)
230    }
231
232    /// All pages opened in this context.
233    pub fn pages(&self) -> Vec<Page> {
234        self.inner.pages.lock().clone()
235    }
236
237    /// The owning browser.
238    pub fn browser(&self) -> Browser {
239        self.inner.browser.clone()
240    }
241
242    // --- Cookies (via the Storage domain, scoped by browserContextId) --------
243
244    /// Open (once) and cache a hidden about:blank page in this context, for
245    /// domain commands that need a page session.
246    #[allow(dead_code)]
247    async fn ensure_utility_page(&self) -> Result<Page> {
248        if let Some(p) = self.inner.utility_page.lock().clone() {
249            return Ok(p);
250        }
251        // Open a hidden about:blank page in this context for domain commands.
252        let page = self.new_page().await?;
253        *self.inner.utility_page.lock() = Some(page.clone());
254        Ok(page)
255    }
256
257    /// Add cookies to this context.
258    pub async fn add_cookies(&self, cookies: &[crate::types::Cookie]) -> Result<()> {
259        // Storage.setCookies with browserContextId is only valid on the
260        // browser-level session, so route it there rather than a page session.
261        let mut params = json!({ "cookies": serde_json::to_value(cookies)? });
262        if let Some(id) = &self.inner.browser_context_id {
263            params["browserContextId"] = json!(id);
264        }
265        self.inner
266            .browser
267            .browser_session()
268            .send("Storage.setCookies", params)
269            .await?;
270        Ok(())
271    }
272
273    /// Get cookies for this context.
274    pub async fn cookies(&self) -> Result<Vec<Value>> {
275        let mut params = json!({});
276        if let Some(id) = &self.inner.browser_context_id {
277            params["browserContextId"] = json!(id);
278        }
279        let v = self
280            .inner
281            .browser
282            .browser_session()
283            .send("Storage.getCookies", params)
284            .await?;
285        let cookies = v
286            .get("cookies")
287            .cloned()
288            .unwrap_or_else(|| Value::Array(vec![]));
289        Ok(cookies
290            .as_array()
291            .cloned()
292            .unwrap_or_default())
293    }
294
295    /// Clear cookies for this context.
296    pub async fn clear_cookies(&self) -> Result<()> {
297        let mut params = json!({});
298        if let Some(id) = &self.inner.browser_context_id {
299            params["browserContextId"] = json!(id);
300        }
301        self.inner
302            .browser
303            .browser_session()
304            .send("Storage.clearCookies", params)
305            .await?;
306        Ok(())
307    }
308
309    // --- Events --------------------------------------------------------------
310
311    /// Register a handler called for each new page in this context.
312    pub fn on_page<F, Fut>(&self, handler: F)
313    where
314        F: Fn(Page) -> Fut + Send + Sync + 'static,
315        Fut: Future<Output = ()> + Send + 'static,
316    {
317        let h: PageHandler = Arc::new(move |p| Box::pin(handler(p)));
318        self.inner.on_page_handlers.lock().push(h);
319    }
320
321    /// Close the context and all its pages. Disposes an isolated context.
322    pub async fn close(&self) -> Result<()> {
323        if *self.inner.closed.lock() {
324            return Ok(());
325        }
326        *self.inner.closed.lock() = true;
327
328        // Close pages.
329        let pages = std::mem::take(&mut *self.inner.pages.lock());
330        for p in pages {
331            let _ = p.close().await;
332        }
333        if let Some(_util) = self.inner.utility_page.lock().take() {
334            // already covered above if tracked; utility may not be in pages list
335        }
336
337        if let Some(bcid) = &self.inner.browser_context_id {
338            let _ = self
339                .inner
340                .browser
341                .browser_session()
342                .send("Target.disposeBrowserContext", json!({"browserContextId": bcid}))
343                .await;
344        }
345        Ok(())
346    }
347
348    // ----- Permissions & storage -----
349
350    /// Grant browser permissions (e.g. `["geolocation", "clipboard-read"]`).
351    /// Applies browser-wide (CDP `Browser.grantPermissions` is not context-scoped).
352    pub async fn grant_permissions(&self, permissions: &[&str]) -> Result<()> {
353        self.inner
354            .browser
355            .browser_session()
356            .send(
357                "Browser.grantPermissions",
358                json!({ "permissions": permissions }),
359            )
360            .await
361            .map(|_: Value| ())
362    }
363
364    /// Reset all granted permissions.
365    pub async fn clear_permissions(&self) -> Result<()> {
366        self.inner
367            .browser
368            .browser_session()
369            .send("Browser.resetPermissions", json!({}))
370            .await
371            .map(|_: Value| ())
372    }
373
374    /// Snapshot storage state: cookies plus per-origin localStorage.
375    ///
376    /// For each page in the context, `localStorage` is captured grouped by
377    /// `location.origin`. Pages whose origin is `"null"` (e.g. `about:blank`,
378    /// `data:` URLs) are skipped.
379    pub async fn storage_state(&self) -> Result<crate::types::StorageState> {
380        let cookies = self.cookies().await?;
381
382        // Collect localStorage per unique origin across all pages.
383        let mut origins: Vec<crate::types::OriginStorage> = Vec::new();
384        for page in self.pages() {
385            // Capture { origin, entries } in one round-trip.
386            let v: Value = page
387                .evaluate(
388                    "JSON.stringify({ origin: location.origin, entries: Object.entries(localStorage).map(([n,v]) => ({name:n, value:v})) })",
389                )
390                .await?;
391            // `evaluate` deserializes the JSON string into a Value (string). Parse it.
392            let parsed: Value = match &v {
393                Value::String(s) => serde_json::from_str(s).unwrap_or(Value::Null),
394                other => other.clone(),
395            };
396            let origin = parsed
397                .get("origin")
398                .and_then(|o| o.as_str())
399                .unwrap_or("")
400                .to_string();
401            // Skip null-opaque origins (about:blank, data:, etc.).
402            if origin.is_empty() || origin == "null" {
403                continue;
404            }
405            let entries = parsed
406                .get("entries")
407                .and_then(|e| e.as_array())
408                .cloned()
409                .unwrap_or_default();
410            let local_storage: Vec<crate::types::NameValue> = entries
411                .into_iter()
412                .filter_map(|e| {
413                    Some(crate::types::NameValue {
414                        name: e.get("name")?.as_str()?.to_string(),
415                        value: e.get("value")?.as_str()?.to_string(),
416                    })
417                })
418                .collect();
419
420            if let Some(existing) = origins.iter_mut().find(|o| o.origin == origin) {
421                // Merge: later pages win on key collisions.
422                for nv in local_storage {
423                    if let Some(pos) = existing.localStorage.iter().position(|x| x.name == nv.name) {
424                        existing.localStorage[pos].value = nv.value;
425                    } else {
426                        existing.localStorage.push(nv);
427                    }
428                }
429            } else {
430                origins.push(crate::types::OriginStorage {
431                    origin,
432                    localStorage: local_storage,
433                });
434            }
435        }
436
437        Ok(crate::types::StorageState { cookies, origins })
438    }
439
440    /// Restore storage state previously captured by [`Self::storage_state`]:
441    /// cookies are added via the Storage domain, and each origin's localStorage
442    /// is set by evaluating `localStorage.setItem` on a page at that origin
443    /// (an existing page if one matches, otherwise a freshly created one).
444    pub async fn set_storage_state(
445        &self,
446        state: &crate::types::StorageState,
447    ) -> Result<()> {
448        // --- Cookies ---
449        if !state.cookies.is_empty() {
450            // The stored cookies are the raw Storage cookie objects; re-apply them
451            // through the Storage domain scoped to this context (browser-level
452            // session, since browserContextId is only valid there).
453            let mut params = json!({ "cookies": state.cookies });
454            if let Some(id) = &self.inner.browser_context_id {
455                params["browserContextId"] = json!(id);
456            }
457            self.inner
458                .browser
459                .browser_session()
460                .send("Storage.setCookies", params)
461                .await?;
462        }
463
464        // --- localStorage per origin ---
465        for origin_state in &state.origins {
466            if origin_state.localStorage.is_empty() {
467                continue;
468            }
469            // Find an existing page already at this origin, else create one.
470            let mut target: Option<Page> = None;
471            for p in self.pages() {
472                let p_origin: String = match p.evaluate::<String>("location.origin").await {
473                    Ok(o) => o,
474                    Err(_) => continue,
475                };
476                if p_origin == origin_state.origin {
477                    target = Some(p);
478                    break;
479                }
480            }
481            let page = match target {
482                Some(p) => p,
483                None => {
484                    let p = self.new_page().await?;
485                    let url = format!("{}/", origin_state.origin);
486                    p.goto(&url, None).await?;
487                    p
488                }
489            };
490
491            // Batch all setItem calls in a single eval with the entries as the arg.
492            let _: Value = page
493                .evaluate_with_arg(
494                    "(() => { for (const {name, value} of arg) localStorage.setItem(name, value); return null; })()",
495                    &origin_state.localStorage,
496                )
497                .await?;
498        }
499
500        Ok(())
501    }
502
503    // ----- Network interception (context-level routing) -----
504
505    /// Register a route that intercepts matching requests on ALL pages in this
506    /// context — including pages created after this call. The handler must
507    /// continue/fulfill/abort the [`Route`](crate::Route).
508    ///
509    /// A context route is applied to each page by registering the equivalent
510    /// page-level route on it. Context-level and page-level routes are
511    /// otherwise independent.
512    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
513    where
514        F: Fn(Route) -> Fut + Send + Sync + 'static,
515        Fut: Future<Output = ()> + Send + 'static,
516    {
517        let handler: RouteHandler = Arc::new(move |r| Box::pin(handler(r)));
518        let entry = RouteEntry {
519            pattern: pattern.to_string(),
520            handler: Arc::clone(&handler),
521        };
522        self.inner.context_routes.lock().push(entry);
523
524        // Apply to every existing page, sharing the same handler Arc.
525        let pat = pattern.to_string();
526        for page in self.pages() {
527            let h = Arc::clone(&handler);
528            page.route(&pat, move |r| {
529                let h = Arc::clone(&h);
530                async move {
531                    (h)(r).await;
532                }
533            })
534            .await?;
535        }
536        Ok(())
537    }
538
539    /// Remove the first context-level route registered with exactly `pattern`
540    /// and remove the matching page-level route on each page.
541    pub async fn unroute(&self, pattern: &str) -> Result<()> {
542        let mut routes = self.inner.context_routes.lock();
543        if let Some(pos) = routes.iter().position(|e| e.pattern == pattern) {
544            routes.remove(pos);
545        }
546        drop(routes);
547
548        for page in self.pages() {
549            page.unroute(pattern).await?;
550        }
551        Ok(())
552    }
553
554    /// Remove all context-level routes and all page-level routes on each page.
555    pub async fn unroute_all(&self) -> Result<()> {
556        self.inner.context_routes.lock().clear();
557        for page in self.pages() {
558            page.unroute_all().await?;
559        }
560        Ok(())
561    }
562
563    /// A performance tracing handle bound to the browser-level CDP session.
564    ///
565    /// Mirrors Playwright's `context.tracing()`. The Tracing domain is
566    /// browser-level; `tracing()` returns clones of a single shared handle so
567    /// [`Tracing::start`] and [`Tracing::stop`] operate on the same state.
568    pub fn tracing(&self) -> crate::Tracing {
569        self.inner.tracing.clone()
570    }
571}