cdp_core/
emulation.rs

1use crate::error::Result;
2use crate::page::Page;
3use cdp_protocol::emulation::{
4    ClearGeolocationOverride, ClearGeolocationOverrideReturnObject, MediaFeature, SetEmulatedMedia,
5    SetEmulatedMediaReturnObject, SetGeolocationOverride, SetGeolocationOverrideReturnObject,
6    SetLocaleOverride, SetLocaleOverrideReturnObject, SetTimezoneOverride,
7    SetTimezoneOverrideReturnObject, SetUserAgentOverride, SetUserAgentOverrideReturnObject,
8    UserAgentBrandVersion, UserAgentMetadata,
9};
10use std::sync::Arc;
11
12/// A group of emulation overrides applied to a target.
13#[derive(Clone, Debug, Default)]
14pub struct EmulationConfig {
15    pub geolocation: Option<Geolocation>,
16    pub timezone_id: Option<String>,
17    pub locale: Option<String>,
18    pub media: Option<MediaEmulation>,
19    pub user_agent: Option<UserAgentOverride>,
20}
21
22impl EmulationConfig {
23    pub fn with_geolocation(mut self, geolocation: Geolocation) -> Self {
24        self.geolocation = Some(geolocation);
25        self
26    }
27
28    pub fn with_timezone<T: Into<String>>(mut self, timezone: T) -> Self {
29        self.timezone_id = Some(timezone.into());
30        self
31    }
32
33    pub fn with_locale<T: Into<String>>(mut self, locale: T) -> Self {
34        self.locale = Some(locale.into());
35        self
36    }
37
38    pub fn with_media(mut self, media: MediaEmulation) -> Self {
39        self.media = Some(media);
40        self
41    }
42
43    pub fn with_user_agent(mut self, override_data: UserAgentOverride) -> Self {
44        self.user_agent = Some(override_data);
45        self
46    }
47}
48
49/// Geolocation parameters for `Emulation.setGeolocationOverride`.
50#[derive(Clone, Debug)]
51pub struct Geolocation {
52    pub latitude: f64,
53    pub longitude: f64,
54    pub accuracy: Option<f64>,
55    pub altitude: Option<f64>,
56    pub altitude_accuracy: Option<f64>,
57    pub heading: Option<f64>,
58    pub speed: Option<f64>,
59}
60
61impl Geolocation {
62    pub fn new(latitude: f64, longitude: f64) -> Self {
63        Self {
64            latitude,
65            longitude,
66            accuracy: None,
67            altitude: None,
68            altitude_accuracy: None,
69            heading: None,
70            speed: None,
71        }
72    }
73
74    pub fn with_accuracy(mut self, accuracy: f64) -> Self {
75        self.accuracy = Some(accuracy);
76        self
77    }
78
79    pub fn with_altitude(mut self, altitude: f64) -> Self {
80        self.altitude = Some(altitude);
81        self
82    }
83
84    pub fn with_altitude_accuracy(mut self, altitude_accuracy: f64) -> Self {
85        self.altitude_accuracy = Some(altitude_accuracy);
86        self
87    }
88
89    pub fn with_heading(mut self, heading: f64) -> Self {
90        self.heading = Some(heading);
91        self
92    }
93
94    pub fn with_speed(mut self, speed: f64) -> Self {
95        self.speed = Some(speed);
96        self
97    }
98}
99
100impl Default for Geolocation {
101    fn default() -> Self {
102        Self::new(0.0, 0.0)
103    }
104}
105
106/// Media emulation configuration.
107#[derive(Clone, Debug, Default)]
108pub struct MediaEmulation {
109    pub media_type: Option<String>,
110    pub features: Vec<MediaFeatureOverride>,
111}
112
113impl MediaEmulation {
114    pub fn with_media_type<T: Into<String>>(mut self, media: T) -> Self {
115        self.media_type = Some(media.into());
116        self
117    }
118
119    pub fn with_feature(mut self, feature: MediaFeatureOverride) -> Self {
120        self.features.push(feature);
121        self
122    }
123}
124
125/// A single media feature override.
126#[derive(Clone, Debug)]
127pub struct MediaFeatureOverride {
128    pub name: String,
129    pub value: String,
130}
131
132impl MediaFeatureOverride {
133    pub fn new<N: Into<String>, V: Into<String>>(name: N, value: V) -> Self {
134        Self {
135            name: name.into(),
136            value: value.into(),
137        }
138    }
139}
140
141/// High level user agent override description.
142#[derive(Clone, Debug)]
143pub struct UserAgentOverride {
144    pub user_agent: String,
145    pub accept_language: Option<String>,
146    pub platform: Option<String>,
147    pub metadata: Option<UserAgentMetadataOverride>,
148}
149
150impl UserAgentOverride {
151    pub fn new<T: Into<String>>(user_agent: T) -> Self {
152        Self {
153            user_agent: user_agent.into(),
154            accept_language: None,
155            platform: None,
156            metadata: None,
157        }
158    }
159
160    pub fn with_accept_language<T: Into<String>>(mut self, value: T) -> Self {
161        self.accept_language = Some(value.into());
162        self
163    }
164
165    pub fn with_platform<T: Into<String>>(mut self, value: T) -> Self {
166        self.platform = Some(value.into());
167        self
168    }
169
170    pub fn with_metadata(mut self, metadata: UserAgentMetadataOverride) -> Self {
171        self.metadata = Some(metadata);
172        self
173    }
174}
175
176/// Structured metadata for `SetUserAgentOverride`.
177#[derive(Clone, Debug, Default)]
178pub struct UserAgentMetadataOverride {
179    pub brands: Vec<UserAgentBrand>,
180    pub full_version_list: Vec<UserAgentBrand>,
181    pub full_version: Option<String>,
182    pub platform: Option<String>,
183    pub platform_version: Option<String>,
184    pub architecture: Option<String>,
185    pub model: Option<String>,
186    pub mobile: Option<bool>,
187    pub bitness: Option<String>,
188    pub wow64: Option<bool>,
189    pub form_factors: Vec<String>,
190}
191
192impl UserAgentMetadataOverride {
193    pub fn with_brand(mut self, brand: UserAgentBrand) -> Self {
194        self.brands.push(brand);
195        self
196    }
197
198    pub fn with_full_version_entry(mut self, brand: UserAgentBrand) -> Self {
199        self.full_version_list.push(brand);
200        self
201    }
202
203    pub fn with_full_version<T: Into<String>>(mut self, version: T) -> Self {
204        self.full_version = Some(version.into());
205        self
206    }
207
208    pub fn with_platform<T: Into<String>>(mut self, platform: T) -> Self {
209        self.platform = Some(platform.into());
210        self
211    }
212
213    pub fn with_platform_version<T: Into<String>>(mut self, version: T) -> Self {
214        self.platform_version = Some(version.into());
215        self
216    }
217
218    pub fn with_architecture<T: Into<String>>(mut self, arch: T) -> Self {
219        self.architecture = Some(arch.into());
220        self
221    }
222
223    pub fn with_model<T: Into<String>>(mut self, model: T) -> Self {
224        self.model = Some(model.into());
225        self
226    }
227
228    pub fn with_mobile(mut self, mobile: bool) -> Self {
229        self.mobile = Some(mobile);
230        self
231    }
232
233    pub fn with_bitness<T: Into<String>>(mut self, bitness: T) -> Self {
234        self.bitness = Some(bitness.into());
235        self
236    }
237
238    pub fn with_wow64(mut self, wow64: bool) -> Self {
239        self.wow64 = Some(wow64);
240        self
241    }
242
243    pub fn with_form_factor<T: Into<String>>(mut self, factor: T) -> Self {
244        self.form_factors.push(factor.into());
245        self
246    }
247
248    fn to_cdp(&self) -> UserAgentMetadata {
249        UserAgentMetadata {
250            brands: if self.brands.is_empty() {
251                None
252            } else {
253                Some(self.brands.iter().map(|brand| brand.to_cdp()).collect())
254            },
255            full_version_list: if self.full_version_list.is_empty() {
256                None
257            } else {
258                Some(
259                    self.full_version_list
260                        .iter()
261                        .map(|brand| brand.to_cdp())
262                        .collect(),
263                )
264            },
265            full_version: self.full_version.clone(),
266            platform: self.platform.clone().unwrap_or_default(),
267            platform_version: self.platform_version.clone().unwrap_or_default(),
268            architecture: self.architecture.clone().unwrap_or_default(),
269            model: self.model.clone().unwrap_or_default(),
270            mobile: self.mobile.unwrap_or(false),
271            bitness: self.bitness.clone(),
272            wow_64: self.wow64,
273            form_factors: if self.form_factors.is_empty() {
274                None
275            } else {
276                Some(self.form_factors.clone())
277            },
278        }
279    }
280}
281
282/// Browser brand/version pair used in UA metadata.
283#[derive(Clone, Debug)]
284pub struct UserAgentBrand {
285    pub brand: String,
286    pub version: String,
287}
288
289impl UserAgentBrand {
290    pub fn new<B: Into<String>, V: Into<String>>(brand: B, version: V) -> Self {
291        Self {
292            brand: brand.into(),
293            version: version.into(),
294        }
295    }
296
297    fn to_cdp(&self) -> UserAgentBrandVersion {
298        UserAgentBrandVersion {
299            brand: self.brand.clone(),
300            version: self.version.clone(),
301        }
302    }
303}
304
305/// Controller that exposes Emulation domain commands for a page.
306pub struct EmulationController {
307    page: Arc<Page>,
308}
309
310impl EmulationController {
311    pub(crate) fn new(page: Arc<Page>) -> Self {
312        Self { page }
313    }
314
315    /// Applies a collection of emulation overrides to the target page.
316    ///
317    /// # Examples
318    /// ```no_run
319    /// # use cdp_core::{EmulationConfig, Geolocation, Page};
320    /// # use std::sync::Arc;
321    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
322    /// let geolocation = Geolocation::new(37.7749, -122.4194).with_accuracy(1.0);
323    /// let config = EmulationConfig::default().with_geolocation(geolocation);
324    /// page.emulation().apply_config(&config).await?;
325    /// # Ok(())
326    /// # }
327    /// ```
328    pub async fn apply_config(&self, config: &EmulationConfig) -> Result<()> {
329        if let Some(geolocation) = &config.geolocation {
330            self.set_geolocation(geolocation.clone()).await?;
331        }
332        if let Some(timezone) = &config.timezone_id {
333            self.set_timezone(timezone).await?;
334        }
335        if let Some(locale) = &config.locale {
336            self.set_locale(Some(locale.as_str())).await?;
337        }
338        if let Some(media) = &config.media {
339            self.set_media(media.clone()).await?;
340        }
341        if let Some(user_agent) = &config.user_agent {
342            self.set_user_agent(user_agent.clone()).await?;
343        }
344        Ok(())
345    }
346
347    /// Overrides the page's geolocation values until cleared.
348    pub async fn set_geolocation(&self, geolocation: Geolocation) -> Result<()> {
349        let method = SetGeolocationOverride {
350            latitude: Some(geolocation.latitude),
351            longitude: Some(geolocation.longitude),
352            accuracy: geolocation.accuracy,
353            altitude: geolocation.altitude,
354            altitude_accuracy: geolocation.altitude_accuracy,
355            heading: geolocation.heading,
356            speed: geolocation.speed,
357        };
358        let _: SetGeolocationOverrideReturnObject =
359            self.page.session.send_command(method, None).await?;
360        Ok(())
361    }
362
363    /// Removes any geolocation override previously applied.
364    pub async fn clear_geolocation(&self) -> Result<()> {
365        let method = ClearGeolocationOverride(None);
366        let _: ClearGeolocationOverrideReturnObject =
367            self.page.session.send_command(method, None).await?;
368        Ok(())
369    }
370
371    /// Sets the emulated timezone identifier (IANA format).
372    pub async fn set_timezone<T: Into<String>>(&self, timezone_id: T) -> Result<()> {
373        let method = SetTimezoneOverride {
374            timezone_id: timezone_id.into(),
375        };
376        let _: SetTimezoneOverrideReturnObject =
377            self.page.session.send_command(method, None).await?;
378        Ok(())
379    }
380
381    /// Resets the timezone override to the browser default.
382    pub async fn reset_timezone(&self) -> Result<()> {
383        self.set_timezone("").await
384    }
385
386    /// Overrides the page locale (for example "en-US").
387    pub async fn set_locale(&self, locale: Option<&str>) -> Result<()> {
388        let method = SetLocaleOverride {
389            locale: locale.map(|value| value.to_string()),
390        };
391        let _: SetLocaleOverrideReturnObject = self.page.session.send_command(method, None).await?;
392        Ok(())
393    }
394
395    /// Applies media emulation settings, such as `prefers-color-scheme`.
396    pub async fn set_media(&self, media: MediaEmulation) -> Result<()> {
397        let method = SetEmulatedMedia {
398            media: media.media_type.clone(),
399            features: to_cdp_media_features(&media.features),
400        };
401        let _: SetEmulatedMediaReturnObject = self.page.session.send_command(method, None).await?;
402        Ok(())
403    }
404
405    /// Clears any previously applied media emulation overrides.
406    pub async fn clear_media(&self) -> Result<()> {
407        let method = SetEmulatedMedia {
408            media: None,
409            features: None,
410        };
411        let _: SetEmulatedMediaReturnObject = self.page.session.send_command(method, None).await?;
412        Ok(())
413    }
414
415    /// Overrides the user agent string and optional metadata.
416    pub async fn set_user_agent(&self, override_data: UserAgentOverride) -> Result<()> {
417        let method = SetUserAgentOverride {
418            user_agent: override_data.user_agent,
419            accept_language: override_data.accept_language,
420            platform: override_data.platform,
421            user_agent_metadata: override_data
422                .metadata
423                .as_ref()
424                .map(UserAgentMetadataOverride::to_cdp),
425        };
426        let _: SetUserAgentOverrideReturnObject =
427            self.page.session.send_command(method, None).await?;
428        Ok(())
429    }
430}
431
432fn to_cdp_media_features(features: &[MediaFeatureOverride]) -> Option<Vec<MediaFeature>> {
433    if features.is_empty() {
434        None
435    } else {
436        Some(
437            features
438                .iter()
439                .map(|feature| MediaFeature {
440                    name: feature.name.clone(),
441                    value: feature.value.clone(),
442                })
443                .collect(),
444        )
445    }
446}