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