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::{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}