Skip to main content

cdp_core/browser/
manager.rs

1use super::{
2    discovery::{find_available_port, find_running_browser_port},
3    launcher::{BrowserLaunchOptions, BrowserType, LaunchedBrowser},
4    ws_endpoints::resolve_browser_ws_url,
5};
6use crate::emulation::EmulationConfig;
7use crate::error::Result;
8use crate::page::Page;
9use crate::session::Session;
10use crate::transport::{cdp_protocol::*, websocket_connection::*};
11
12use cdp_protocol::browser::{
13    PermissionDescriptor, PermissionSetting, PermissionType, ResetPermissions,
14    ResetPermissionsReturnObject, SetDownloadBehavior, SetDownloadBehaviorBehaviorOption,
15    SetDownloadBehaviorReturnObject, SetPermission, SetPermissionReturnObject,
16};
17use cdp_protocol::target::{
18    AttachToTargetReturnObject, CreateBrowserContext, CreateBrowserContextReturnObject,
19    CreateTarget, CreateTargetReturnObject, DisposeBrowserContext,
20};
21use futures_util::StreamExt;
22use serde::Deserialize;
23use serde_json::Value;
24use std::collections::HashMap;
25use std::path::{Path, PathBuf};
26use std::sync::{Arc, Weak};
27use std::time::Duration;
28use tokio::sync::{Mutex, mpsc};
29use tokio_tungstenite::connect_async;
30use url::Url;
31
32/// Options applied when creating a [`BrowserContext`].
33#[derive(Clone, Debug, Default)]
34pub struct BrowserContextOptions {
35    pub dispose_on_detach: Option<bool>,
36    pub proxy_server: Option<String>,
37    pub proxy_bypass_list: Option<String>,
38    pub origins_with_universal_network_access: Option<Vec<String>>,
39    pub download: Option<DownloadOptions>,
40    pub permission_grants: Vec<PermissionGrant>,
41    pub permission_overrides: Vec<PermissionOverride>,
42    pub emulation: Option<EmulationConfig>,
43}
44
45impl BrowserContextOptions {
46    pub fn with_emulation(mut self, emulation: EmulationConfig) -> Self {
47        self.emulation = Some(emulation);
48        self
49    }
50}
51
52/// Configuration for download behavior within a context.
53#[derive(Clone, Debug)]
54pub struct DownloadOptions {
55    pub behavior: DownloadBehavior,
56    pub download_path: Option<PathBuf>,
57    pub events_enabled: Option<bool>,
58}
59
60impl DownloadOptions {
61    pub fn new(behavior: DownloadBehavior) -> Self {
62        Self {
63            behavior,
64            download_path: None,
65            events_enabled: None,
66        }
67    }
68
69    pub fn with_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
70        self.download_path = Some(path.into());
71        self
72    }
73
74    pub fn with_events_enabled(mut self, enabled: bool) -> Self {
75        self.events_enabled = Some(enabled);
76        self
77    }
78}
79
80/// Supported Chrome download behaviors.
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
82pub enum DownloadBehavior {
83    Deny,
84    #[default]
85    Allow,
86    AllowAndName,
87    Default,
88}
89
90impl DownloadBehavior {
91    fn as_cdp_behavior(self) -> SetDownloadBehaviorBehaviorOption {
92        match self {
93            DownloadBehavior::Deny => SetDownloadBehaviorBehaviorOption::Deny,
94            DownloadBehavior::Allow => SetDownloadBehaviorBehaviorOption::Allow,
95            DownloadBehavior::AllowAndName => SetDownloadBehaviorBehaviorOption::AllowAndName,
96            DownloadBehavior::Default => SetDownloadBehaviorBehaviorOption::Default,
97        }
98    }
99}
100
101/// A batch of permissions that should be granted when a context is created.
102#[derive(Clone, Debug, Default)]
103pub struct PermissionGrant {
104    pub origin: Option<String>,
105    pub permissions: Vec<PermissionType>,
106}
107
108impl PermissionGrant {
109    pub fn new<I>(permissions: I) -> Self
110    where
111        I: IntoIterator<Item = PermissionType>,
112    {
113        Self {
114            origin: None,
115            permissions: permissions.into_iter().collect(),
116        }
117    }
118
119    pub fn with_origin<T: Into<String>>(mut self, origin: T) -> Self {
120        self.origin = Some(origin.into());
121        self
122    }
123}
124
125/// Fine-grained permission override using a [`PermissionDescriptor`].
126#[derive(Clone, Debug)]
127pub struct PermissionOverride {
128    pub descriptor: PermissionDescriptor,
129    pub setting: PermissionSetting,
130    pub origin: Option<String>,
131    pub embedded_origin: Option<String>,
132}
133
134impl PermissionOverride {
135    pub fn new(descriptor: PermissionDescriptor, setting: PermissionSetting) -> Self {
136        Self {
137            descriptor,
138            setting,
139            origin: None,
140            embedded_origin: None,
141        }
142    }
143
144    pub fn with_origin<T: Into<String>>(mut self, origin: T) -> Self {
145        self.origin = Some(origin.into());
146        self
147    }
148}
149
150#[derive(Default)]
151struct BrowserContextState {
152    closed: bool,
153    emulation_config: Option<EmulationConfig>,
154    pages: Vec<Weak<Page>>,
155}
156
157/// Builder used to launch or connect to a Chromium-based browser instance.
158///
159/// # Examples
160/// ```no_run
161/// use cdp_core::Browser;
162///
163/// # async fn example() -> cdp_core::Result<()> {
164/// let browser = Browser::launcher()
165///     .port(9222)
166///     .launch()
167///     .await?;
168/// # let _ = browser;
169/// # Ok(())
170/// # }
171/// ```
172pub struct Launcher {
173    port: Option<u16>,
174    connect_addr: Option<String>,
175    browser_type: BrowserType,
176    launch_options: BrowserLaunchOptions,
177}
178
179impl Default for Launcher {
180    fn default() -> Self {
181        Self {
182            port: None,
183            connect_addr: None,
184            browser_type: BrowserType::Chrome,
185            launch_options: BrowserLaunchOptions::default(),
186        }
187    }
188}
189
190impl Launcher {
191    pub fn port(mut self, port: u16) -> Self {
192        self.port = Some(port);
193        self
194    }
195
196    pub fn connect_to_existing(mut self, addr: &str) -> Self {
197        self.connect_addr = Some(addr.to_string());
198        self
199    }
200
201    pub fn browser(mut self, browser: BrowserType) -> Self {
202        self.browser_type = browser;
203        self
204    }
205
206    pub fn launch_options(mut self, options: BrowserLaunchOptions) -> Self {
207        self.launch_options = options;
208        self
209    }
210
211    pub fn configure_options(mut self, configure: impl FnOnce(&mut BrowserLaunchOptions)) -> Self {
212        configure(&mut self.launch_options);
213        self
214    }
215
216    pub fn disable_images(mut self, disable: bool) -> Self {
217        self.launch_options.disable_image_loading = disable;
218        self
219    }
220
221    pub fn mute_audio(mut self, mute: bool) -> Self {
222        self.launch_options.mute_audio = mute;
223        self
224    }
225
226    pub fn incognito(mut self, incognito: bool) -> Self {
227        self.launch_options.incognito = incognito;
228        self
229    }
230
231    pub fn user_data_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
232        self.launch_options.user_data_dir = Some(path.into());
233        self
234    }
235
236    pub fn clear_user_data_dir(mut self) -> Self {
237        self.launch_options.user_data_dir = None;
238        self
239    }
240
241    pub fn profile_directory<S: Into<String>>(mut self, profile: S) -> Self {
242        self.launch_options.profile_directory = Some(profile.into());
243        self
244    }
245
246    pub fn clear_profile_directory(mut self) -> Self {
247        self.launch_options.profile_directory = None;
248        self
249    }
250
251    pub fn add_extension<P: Into<PathBuf>>(mut self, path: P) -> Self {
252        self.launch_options.add_extension(path);
253        self
254    }
255
256    pub fn remove_extension<P: AsRef<Path>>(mut self, path: P) -> Self {
257        self.launch_options.remove_extension(path);
258        self
259    }
260
261    pub fn clear_extensions(mut self) -> Self {
262        self.launch_options.clear_extensions();
263        self
264    }
265
266    pub fn disable_extensions_except<I, S>(mut self, ids: I) -> Self
267    where
268        I: IntoIterator<Item = S>,
269        S: Into<String>,
270    {
271        self.launch_options.disable_extensions_except(ids);
272        self
273    }
274
275    pub fn remove_default_flag<S: Into<String>>(mut self, flag: S) -> Self {
276        self.launch_options.remove_default_flag(flag);
277        self
278    }
279
280    pub fn arg<S: Into<String>>(mut self, arg: S) -> Self {
281        self.launch_options.add_arg(arg);
282        self
283    }
284
285    pub fn set_switch_flag<S: Into<String>>(mut self, switch: S) -> Self {
286        self.launch_options.set_switch_flag(switch);
287        self
288    }
289
290    pub fn set_switch_value<S, V>(mut self, switch: S, value: V) -> Self
291    where
292        S: Into<String>,
293        V: Into<String>,
294    {
295        self.launch_options.set_switch_value(switch, value);
296        self
297    }
298
299    pub fn clear_switch<S: Into<String>>(mut self, switch: S) -> Self {
300        self.launch_options.clear_switch(switch);
301        self
302    }
303
304    pub fn enable_feature<S: Into<String>>(mut self, feature: S) -> Self {
305        self.launch_options.enable_feature(feature);
306        self
307    }
308
309    pub fn disable_feature<S: Into<String>>(mut self, feature: S) -> Self {
310        self.launch_options.disable_feature(feature);
311        self
312    }
313
314    pub fn force_field_trial<S: Into<String>>(mut self, trial: S) -> Self {
315        self.launch_options.force_field_trial(trial);
316        self
317    }
318
319    pub async fn launch(self) -> Result<Arc<Browser>> {
320        let Launcher {
321            port,
322            connect_addr,
323            browser_type,
324            launch_options,
325        } = self;
326
327        if let Some(addr) = connect_addr {
328            let url = resolve_browser_ws_url(&addr).await?;
329            return Browser::connect(url, None).await;
330        }
331
332        let (ws_url, process) = match find_running_browser_port(browser_type) {
333            Ok(found_port) => {
334                let addr = format!("http://127.0.0.1:{}", found_port);
335                tracing::info!("Connecting to existing browser at {}", addr);
336                let url = resolve_browser_ws_url(&addr).await?;
337                (url, None)
338            }
339            Err(_) => {
340                let selected_port = if let Some(p) = port {
341                    p
342                } else {
343                    find_available_port().await?
344                };
345                let launched = browser_type.launch_with_options(selected_port, launch_options)?;
346                let addr = format!("http://127.0.0.1:{}", launched.debug_port);
347                tracing::info!("Launched new browser at {}", addr);
348                let url = resolve_browser_ws_url(&addr).await?;
349                (url, Some(ChromeProcess(launched)))
350            }
351        };
352
353        Browser::connect(ws_url, process).await
354    }
355}
356
357/// RAII guard that terminates the spawned browser process when dropped.
358struct ChromeProcess(LaunchedBrowser);
359impl Drop for ChromeProcess {
360    fn drop(&mut self) {
361        self.0.child.kill().ok();
362    }
363}
364
365/// High-level interface for working with a Chrome DevTools Protocol browser.
366///
367/// Instances are typically produced via [`Browser::launcher`].
368pub struct Browser {
369    internals: Arc<ConnectionInternals>,
370    session_event_senders: Arc<Mutex<HashMap<String, mpsc::Sender<CdpEvent>>>>,
371    active_pages: Arc<Mutex<HashMap<String, Arc<Page>>>>, // K: sessionId, V: Arc<Page>
372    browser_contexts: Arc<Mutex<HashMap<String, Weak<BrowserContext>>>>,
373    _chrome_process: Option<ChromeProcess>,
374}
375
376impl Browser {
377    /// Returns a [`Launcher`] configured with default settings.
378    ///
379    /// # Examples
380    /// ```no_run
381    /// use cdp_core::Browser;
382    ///
383    /// # async fn example() -> cdp_core::Result<()> {
384    /// let browser = Browser::launcher().launch().await?;
385    /// # let _ = browser;
386    /// # Ok(())
387    /// # }
388    /// ```
389    pub fn launcher() -> Launcher {
390        Launcher::default()
391    }
392
393    async fn connect(ws_url: Url, process: Option<ChromeProcess>) -> Result<Arc<Browser>> {
394        let (ws_stream, _) = connect_async(ws_url.as_str()).await?;
395        let (writer, reader) = ws_stream.split();
396        let connection = Arc::new(ConnectionInternals::new(writer));
397        let active_pages = Arc::new(Mutex::new(HashMap::new()));
398        let session_event_senders = Arc::new(Mutex::new(HashMap::new()));
399        let browser_contexts = Arc::new(Mutex::new(HashMap::new()));
400        tokio::spawn(central_message_dispatcher(
401            reader,
402            Arc::clone(&connection),
403            Arc::clone(&active_pages),
404            Arc::clone(&session_event_senders),
405        ));
406        let browser = Browser {
407            internals: connection,
408            session_event_senders,
409            active_pages,
410            browser_contexts,
411            _chrome_process: process,
412        };
413        let browser_arc = Arc::new(browser);
414        Ok(browser_arc)
415    }
416
417    /// Creates a new, isolated browser context using default options.
418    /// This is equivalent to opening a new incognito window.
419    ///
420    /// # Examples
421    /// ```no_run
422    /// use cdp_core::Browser;
423    ///
424    /// # async fn example() -> cdp_core::Result<()> {
425    /// let browser = Browser::launcher().launch().await?;
426    /// let context = browser.new_context().await?;
427    /// # let _ = context;
428    /// # Ok(())
429    /// # }
430    /// ```
431    pub async fn new_context(self: &Arc<Self>) -> Result<Arc<BrowserContext>> {
432        self.new_context_with_options(BrowserContextOptions::default())
433            .await
434    }
435
436    /// Creates a new browser context with the provided options.
437    ///
438    /// # Examples
439    /// ```no_run
440    /// use cdp_core::{Browser, BrowserContextOptions};
441    ///
442    /// # async fn example() -> cdp_core::Result<()> {
443    /// let browser = Browser::launcher().launch().await?;
444    /// let options = BrowserContextOptions {
445    ///     dispose_on_detach: Some(true),
446    ///     ..Default::default()
447    /// };
448    /// let context = browser.new_context_with_options(options).await?;
449    /// # let _ = context;
450    /// # Ok(())
451    /// # }
452    /// ```
453    pub async fn new_context_with_options(
454        self: &Arc<Self>,
455        options: BrowserContextOptions,
456    ) -> Result<Arc<BrowserContext>> {
457        let method = CreateBrowserContext {
458            dispose_on_detach: options.dispose_on_detach,
459            proxy_server: options.proxy_server.clone(),
460            proxy_bypass_list: options.proxy_bypass_list.clone(),
461            origins_with_universal_network_access: options
462                .origins_with_universal_network_access
463                .clone(),
464        };
465        let obj = self
466            .send_command::<_, CreateBrowserContextReturnObject>(method, None)
467            .await?;
468        let context_id = obj.browser_context_id;
469        println!("Created new BrowserContext with ID: {}", context_id);
470
471        let context = Arc::new(BrowserContext::new(context_id.clone(), Arc::clone(self)));
472        self.register_context(&context).await;
473
474        if let Err(err) = context.apply_options(&options).await {
475            self.unregister_context(&context_id).await;
476            return Err(err);
477        }
478
479        Ok(context)
480    }
481
482    /// Returns all live browser contexts tracked by this browser.
483    pub async fn contexts(&self) -> Vec<Arc<BrowserContext>> {
484        let mut contexts = Vec::new();
485        let mut guard = self.browser_contexts.lock().await;
486        guard.retain(|_, weak| match weak.upgrade() {
487            Some(context) => {
488                contexts.push(context);
489                true
490            }
491            None => false,
492        });
493        contexts
494    }
495
496    /// Attempts to fetch an existing browser context by id.
497    pub async fn get_context(&self, id: &str) -> Option<Arc<BrowserContext>> {
498        let mut guard = self.browser_contexts.lock().await;
499        if let Some(weak) = guard.get(id)
500            && let Some(context) = weak.upgrade()
501        {
502            return Some(context);
503        }
504        guard.remove(id);
505        None
506    }
507
508    async fn register_context(&self, context: &Arc<BrowserContext>) {
509        let id = context.id().to_string();
510        self.browser_contexts
511            .lock()
512            .await
513            .insert(id, Arc::downgrade(context));
514    }
515
516    async fn unregister_context(&self, id: &str) {
517        self.browser_contexts.lock().await.remove(id);
518    }
519
520    /// Opens a new page in the default browser context.
521    ///
522    /// # Examples
523    /// ```no_run
524    /// use cdp_core::Browser;
525    ///
526    /// # async fn example() -> cdp_core::Result<()> {
527    /// let browser = Browser::launcher().launch().await?;
528    /// let page = browser.new_page().await?;
529    /// # let _ = page;
530    /// # Ok(())
531    /// # }
532    /// ```
533    pub async fn new_page(self: &Arc<Self>) -> Result<Arc<Page>> {
534        // We call a new method on BrowserContext, passing `None` for the context id
535        self.new_page_in_context(None).await
536    }
537
538    /// The underlying method that actually creates pages
539    pub(crate) async fn new_page_in_context(&self, context_id: Option<&str>) -> Result<Arc<Page>> {
540        let (event_sender, event_receiver) = mpsc::channel(crate::DEFAULT_CHANNEL_CAPACITY);
541        let method = CreateTarget {
542            url: "about:blank".to_string(),
543            left: None,
544            top: None,
545            width: None,
546            height: None,
547            window_state: None,
548            browser_context_id: context_id.map(|s| s.to_string()),
549            enable_begin_frame_control: None,
550            new_window: None,
551            background: None,
552            for_tab: None,
553            hidden: None,
554            focus: Some(true),
555        };
556        let obj = self
557            .send_command::<_, CreateTargetReturnObject>(method, None)
558            .await?;
559        let method = cdp_protocol::target::AttachToTarget {
560            target_id: obj.target_id,
561            flatten: Some(true),
562        };
563        let obj = self
564            .send_command::<_, AttachToTargetReturnObject>(method, None)
565            .await?;
566        self.session_event_senders
567            .lock()
568            .await
569            .insert(obj.session_id.clone(), event_sender);
570
571        let session = Session::new(
572            Some(obj.session_id.clone()),
573            Arc::clone(&self.internals),
574            event_receiver,
575        );
576        let page = Arc::new(Page::new_from_browser(Arc::new(session)));
577
578        // Enable all required domains through the DomainManager helper.
579        page.domain_manager.enable_required_domains().await?;
580
581        self.active_pages
582            .lock()
583            .await
584            .insert(obj.session_id.clone(), Arc::clone(&page));
585        Ok(page)
586    }
587
588    pub(crate) async fn send_command<
589        M: serde::Serialize + std::fmt::Debug + cdp_protocol::types::Method,
590        R: for<'de> Deserialize<'de>,
591    >(
592        &self,
593        method: M,
594        timeout: Option<Duration>,
595    ) -> Result<R> {
596        self.internals.send(method, None, timeout).await
597    }
598}
599
600/// Represents an isolated browser context (similar to an incognito profile).
601pub struct BrowserContext {
602    id: String,            // The browserContextId from CDP
603    browser: Arc<Browser>, // Reference back to the parent browser
604    state: Mutex<BrowserContextState>,
605}
606
607impl BrowserContext {
608    fn new(id: String, browser: Arc<Browser>) -> Self {
609        Self {
610            id,
611            browser,
612            state: Mutex::new(BrowserContextState::default()),
613        }
614    }
615
616    pub fn id(&self) -> &str {
617        &self.id
618    }
619
620    async fn apply_options(&self, options: &BrowserContextOptions) -> Result<()> {
621        for grant in &options.permission_grants {
622            self.apply_permission_grant(grant).await?;
623        }
624
625        for permission in &options.permission_overrides {
626            self.set_permission_override(permission).await?;
627        }
628
629        if let Some(download) = &options.download {
630            self.set_download_behavior(download).await?;
631        }
632
633        if let Some(emulation) = &options.emulation {
634            self.set_emulation_config(emulation.clone()).await?;
635        }
636
637        Ok(())
638    }
639
640    /// Creates a new page within this specific browser context.
641    ///
642    /// # Examples
643    /// ```no_run
644    /// use cdp_core::Browser;
645    ///
646    /// # async fn example() -> cdp_core::Result<()> {
647    /// let browser = Browser::launcher().launch().await?;
648    /// let context = browser.new_context().await?;
649    /// let page = context.new_page().await?;
650    /// # let _ = page;
651    /// # Ok(())
652    /// # }
653    /// ```
654    pub async fn new_page(&self) -> Result<Arc<Page>> {
655        let page = self.browser.new_page_in_context(Some(&self.id)).await?;
656        self.register_page(&page).await;
657        self.apply_emulation_to_page(&page).await?;
658        Ok(page)
659    }
660
661    /// Grants the provided permissions for the optional origin.
662    ///
663    /// Each [`PermissionType`] is sent as a separate `Browser.setPermission` call
664    /// because [`PermissionDescriptor`] accepts a single permission name string.
665    pub async fn grant_permissions(
666        &self,
667        origin: Option<&str>,
668        permissions: &[PermissionType],
669    ) -> Result<()> {
670        for permission in permissions {
671            // Serialize the enum variant to its CDP wire-format name (e.g. "geolocation").
672            let name = serde_json::to_value(permission)
673                .ok()
674                .and_then(|v| v.as_str().map(|s| s.to_string()))
675                .unwrap_or_else(|| format!("{:?}", permission));
676
677            let method = SetPermission {
678                permission: PermissionDescriptor {
679                    name,
680                    sysex: None,
681                    user_visible_only: None,
682                    allow_without_sanitization: None,
683                    allow_without_gesture: None,
684                    pan_tilt_zoom: None,
685                },
686                origin: origin.map(|v| v.to_string()),
687                setting: PermissionSetting::Granted,
688                browser_context_id: Some(self.id.clone()),
689                embedded_origin: None,
690            };
691            let _: SetPermissionReturnObject = self.browser.send_command(method, None).await?;
692        }
693        Ok(())
694    }
695
696    /// Applies a permission grant described by [`PermissionGrant`].
697    pub async fn apply_permission_grant(&self, grant: &PermissionGrant) -> Result<()> {
698        self.grant_permissions(grant.origin.as_deref(), &grant.permissions)
699            .await
700    }
701
702    /// Sets a fine-grained permission override.
703    pub async fn set_permission_override(&self, permission: &PermissionOverride) -> Result<()> {
704        let method = SetPermission {
705            permission: permission.descriptor.clone(),
706            setting: permission.setting.clone(),
707            origin: permission.origin.clone(),
708            embedded_origin: permission.embedded_origin.clone(),
709            browser_context_id: Some(self.id.clone()),
710        };
711        let _: SetPermissionReturnObject = self.browser.send_command(method, None).await?;
712        Ok(())
713    }
714
715    /// Resets all permission overrides that have been applied to this context.
716    pub async fn reset_permissions(&self) -> Result<()> {
717        let method = ResetPermissions {
718            browser_context_id: Some(self.id.clone()),
719        };
720        let _: ResetPermissionsReturnObject = self.browser.send_command(method, None).await?;
721        Ok(())
722    }
723
724    /// Configures how downloads should be handled within this context.
725    pub async fn set_download_behavior(&self, options: &DownloadOptions) -> Result<()> {
726        let download_path = options
727            .download_path
728            .as_ref()
729            .map(|path| path.to_string_lossy().into_owned());
730        let method = SetDownloadBehavior {
731            behavior: options.behavior.as_cdp_behavior(),
732            browser_context_id: Some(self.id.clone()),
733            download_path,
734            events_enabled: options.events_enabled,
735        };
736        let _: SetDownloadBehaviorReturnObject = self.browser.send_command(method, None).await?;
737        Ok(())
738    }
739
740    /// Resets download handling back to Chrome defaults.
741    pub async fn clear_download_behavior(&self) -> Result<()> {
742        let method = SetDownloadBehavior {
743            behavior: SetDownloadBehaviorBehaviorOption::Default,
744            browser_context_id: Some(self.id.clone()),
745            download_path: None,
746            events_enabled: None,
747        };
748        let _: SetDownloadBehaviorReturnObject = self.browser.send_command(method, None).await?;
749        Ok(())
750    }
751
752    /// Applies the provided emulation configuration to all existing pages and stores it for future pages.
753    pub async fn set_emulation_config(&self, config: EmulationConfig) -> Result<()> {
754        let pages = {
755            let mut state = self.state.lock().await;
756            state.emulation_config = Some(config.clone());
757            let mut pages = Vec::new();
758            state.pages.retain(|weak| match weak.upgrade() {
759                Some(page) => {
760                    pages.push(page);
761                    true
762                }
763                None => false,
764            });
765            pages
766        };
767
768        for page in pages {
769            page.emulation().apply_config(&config).await?;
770        }
771
772        Ok(())
773    }
774
775    /// Clears the stored emulation configuration and best-effort resets overrides on existing pages.
776    pub async fn clear_emulation_config(&self) -> Result<()> {
777        let (pages, previous) = {
778            let mut state = self.state.lock().await;
779            let previous = state.emulation_config.take();
780            let mut pages = Vec::new();
781            state.pages.retain(|weak| match weak.upgrade() {
782                Some(page) => {
783                    pages.push(page);
784                    true
785                }
786                None => false,
787            });
788            (pages, previous)
789        };
790
791        if let Some(config) = previous {
792            for page in pages {
793                let controller = page.emulation();
794                if config.geolocation.is_some() {
795                    controller.clear_geolocation().await?;
796                }
797                if config.timezone_id.is_some() {
798                    controller.reset_timezone().await?;
799                }
800                if config.locale.is_some() {
801                    controller.set_locale(None).await?;
802                }
803                if config.media.is_some() {
804                    controller.clear_media().await?;
805                }
806                // User agent overrides remain active unless explicitly replaced.
807            }
808        }
809
810        Ok(())
811    }
812
813    /// Closes this browser context and all pages within it.
814    pub async fn close(&self) -> Result<()> {
815        {
816            let mut state = self.state.lock().await;
817            if state.closed {
818                return Ok(());
819            }
820            state.closed = true;
821        }
822
823        let method = DisposeBrowserContext {
824            browser_context_id: self.id.clone(),
825        };
826        let result: Result<Value> = self.browser.send_command(method, None).await;
827
828        if let Err(err) = result {
829            let mut state = self.state.lock().await;
830            state.closed = false;
831            return Err(err);
832        }
833
834        {
835            let mut state = self.state.lock().await;
836            state.pages.clear();
837            state.emulation_config = None;
838        }
839
840        self.browser.unregister_context(&self.id).await;
841        Ok(())
842    }
843
844    async fn register_page(&self, page: &Arc<Page>) {
845        let mut state = self.state.lock().await;
846        state.pages.retain(|weak| weak.upgrade().is_some());
847        let weak = Arc::downgrade(page);
848        if !state
849            .pages
850            .iter()
851            .any(|existing| Weak::ptr_eq(existing, &weak))
852        {
853            state.pages.push(weak);
854        }
855    }
856
857    async fn apply_emulation_to_page(&self, page: &Arc<Page>) -> Result<()> {
858        let config = {
859            let state = self.state.lock().await;
860            state.emulation_config.clone()
861        };
862
863        if let Some(config) = config {
864            page.emulation().apply_config(&config).await?;
865        }
866
867        Ok(())
868    }
869}