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