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