viewpoint_core/context/emulation/
mod.rs

1//! Emulation methods for BrowserContext.
2//!
3//! This module provides methods for geolocation, offline mode, and HTTP headers.
4
5use std::collections::HashMap;
6
7use tracing::{debug, instrument};
8
9use viewpoint_cdp::protocol::emulation::{
10    ClearGeolocationOverrideParams, SetGeolocationOverrideParams,
11};
12use viewpoint_cdp::protocol::network::{
13    EmulateNetworkConditionsParams, SetExtraHTTPHeadersParams,
14};
15
16use super::BrowserContext;
17use crate::error::ContextError;
18
19impl BrowserContext {
20    /// Clear the geolocation override.
21    ///
22    /// # Errors
23    ///
24    /// Returns an error if clearing geolocation fails.
25    #[instrument(level = "debug", skip(self))]
26    pub async fn clear_geolocation(&self) -> Result<(), ContextError> {
27        if self.is_closed() {
28            return Err(ContextError::Closed);
29        }
30
31        // Clear on all pages
32        let pages = self.pages.read().await;
33        for page in pages.iter() {
34            if !page.session_id.is_empty() {
35                self.connection()
36                    .send_command::<_, serde_json::Value>(
37                        "Emulation.clearGeolocationOverride",
38                        Some(ClearGeolocationOverrideParams::default()),
39                        Some(&page.session_id),
40                    )
41                    .await?;
42            }
43        }
44
45        Ok(())
46    }
47
48    /// Set extra HTTP headers to be sent with every request.
49    ///
50    /// # Example
51    ///
52    /// ```no_run
53    /// use viewpoint_core::BrowserContext;
54    /// use std::collections::HashMap;
55    ///
56    /// # async fn example(context: &BrowserContext) -> Result<(), viewpoint_core::CoreError> {
57    /// let mut headers = HashMap::new();
58    /// headers.insert("Authorization".to_string(), "Bearer token123".to_string());
59    /// context.set_extra_http_headers(headers).await?;
60    /// # Ok(())
61    /// # }
62    /// ```
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if setting headers fails.
67    #[instrument(level = "debug", skip(self, headers))]
68    pub async fn set_extra_http_headers(
69        &self,
70        headers: HashMap<String, String>,
71    ) -> Result<(), ContextError> {
72        if self.is_closed() {
73            return Err(ContextError::Closed);
74        }
75
76        debug!(count = headers.len(), "Setting extra HTTP headers");
77
78        // Set on all pages
79        let pages = self.pages.read().await;
80        for page in pages.iter() {
81            if !page.session_id.is_empty() {
82                self.connection()
83                    .send_command::<_, serde_json::Value>(
84                        "Network.setExtraHTTPHeaders",
85                        Some(SetExtraHTTPHeadersParams {
86                            headers: headers.clone(),
87                        }),
88                        Some(&page.session_id),
89                    )
90                    .await?;
91            }
92        }
93
94        Ok(())
95    }
96
97    /// Set offline mode.
98    ///
99    /// # Example
100    ///
101    /// ```no_run
102    /// use viewpoint_core::BrowserContext;
103    ///
104    /// # async fn example(context: &BrowserContext) -> Result<(), viewpoint_core::CoreError> {
105    /// // Go offline
106    /// context.set_offline(true).await?;
107    ///
108    /// // Go back online
109    /// context.set_offline(false).await?;
110    /// # Ok(())
111    /// # }
112    /// ```
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if setting offline mode fails.
117    #[instrument(level = "debug", skip(self))]
118    pub async fn set_offline(&self, offline: bool) -> Result<(), ContextError> {
119        if self.is_closed() {
120            return Err(ContextError::Closed);
121        }
122
123        debug!(offline = offline, "Setting offline mode");
124
125        let params = if offline {
126            EmulateNetworkConditionsParams::offline()
127        } else {
128            EmulateNetworkConditionsParams::online()
129        };
130
131        // Set on all pages
132        let pages = self.pages.read().await;
133        for page in pages.iter() {
134            if !page.session_id.is_empty() {
135                self.connection()
136                    .send_command::<_, serde_json::Value>(
137                        "Network.emulateNetworkConditions",
138                        Some(params.clone()),
139                        Some(&page.session_id),
140                    )
141                    .await?;
142            }
143        }
144
145        Ok(())
146    }
147}
148
149// =============================================================================
150// Set Geolocation Builder
151// =============================================================================
152
153/// Builder for setting geolocation.
154#[derive(Debug)]
155pub struct SetGeolocationBuilder<'a> {
156    context: &'a BrowserContext,
157    latitude: f64,
158    longitude: f64,
159    accuracy: f64,
160}
161
162impl<'a> SetGeolocationBuilder<'a> {
163    pub(crate) fn new(context: &'a BrowserContext, latitude: f64, longitude: f64) -> Self {
164        Self {
165            context,
166            latitude,
167            longitude,
168            accuracy: 0.0,
169        }
170    }
171
172    /// Set the accuracy in meters.
173    #[must_use]
174    pub fn accuracy(mut self, accuracy: f64) -> Self {
175        self.accuracy = accuracy;
176        self
177    }
178
179    /// Execute the geolocation setting.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if setting geolocation fails.
184    pub async fn await_(self) -> Result<(), ContextError> {
185        if self.context.is_closed() {
186            return Err(ContextError::Closed);
187        }
188
189        debug!(
190            latitude = self.latitude,
191            longitude = self.longitude,
192            accuracy = self.accuracy,
193            "Setting geolocation"
194        );
195
196        let params = SetGeolocationOverrideParams::with_accuracy(
197            self.latitude,
198            self.longitude,
199            self.accuracy,
200        );
201
202        // Set on all pages
203        let pages = self.context.pages.read().await;
204        for page in pages.iter() {
205            if !page.session_id.is_empty() {
206                self.context
207                    .connection()
208                    .send_command::<_, serde_json::Value>(
209                        "Emulation.setGeolocationOverride",
210                        Some(params.clone()),
211                        Some(&page.session_id),
212                    )
213                    .await?;
214            }
215        }
216
217        Ok(())
218    }
219}
220
221// Make the builder awaitable
222impl<'a> std::future::IntoFuture for SetGeolocationBuilder<'a> {
223    type Output = Result<(), ContextError>;
224    type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
225
226    fn into_future(self) -> Self::IntoFuture {
227        Box::pin(self.await_())
228    }
229}