Skip to main content

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, UserAgentMetadataBuilder,
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    #[deprecated]
182    pub full_version: Option<String>,
183    pub platform: Option<String>,
184    pub platform_version: Option<String>,
185    pub architecture: Option<String>,
186    pub model: Option<String>,
187    pub mobile: Option<bool>,
188    pub bitness: Option<String>,
189    pub wow64: Option<bool>,
190    pub form_factors: Vec<String>,
191}
192
193impl UserAgentMetadataOverride {
194    pub fn with_brand(mut self, brand: UserAgentBrand) -> Self {
195        self.brands.push(brand);
196        self
197    }
198
199    pub fn with_full_version_entry(mut self, brand: UserAgentBrand) -> Self {
200        self.full_version_list.push(brand);
201        self
202    }
203
204    #[deprecated]
205    #[allow(deprecated)]
206    pub fn with_full_version<T: Into<String>>(mut self, version: T) -> Self {
207        self.full_version = Some(version.into());
208        self
209    }
210
211    pub fn with_platform<T: Into<String>>(mut self, platform: T) -> Self {
212        self.platform = Some(platform.into());
213        self
214    }
215
216    pub fn with_platform_version<T: Into<String>>(mut self, version: T) -> Self {
217        self.platform_version = Some(version.into());
218        self
219    }
220
221    pub fn with_architecture<T: Into<String>>(mut self, arch: T) -> Self {
222        self.architecture = Some(arch.into());
223        self
224    }
225
226    pub fn with_model<T: Into<String>>(mut self, model: T) -> Self {
227        self.model = Some(model.into());
228        self
229    }
230
231    pub fn with_mobile(mut self, mobile: bool) -> Self {
232        self.mobile = Some(mobile);
233        self
234    }
235
236    pub fn with_bitness<T: Into<String>>(mut self, bitness: T) -> Self {
237        self.bitness = Some(bitness.into());
238        self
239    }
240
241    pub fn with_wow64(mut self, wow64: bool) -> Self {
242        self.wow64 = Some(wow64);
243        self
244    }
245
246    pub fn with_form_factor<T: Into<String>>(mut self, factor: T) -> Self {
247        self.form_factors.push(factor.into());
248        self
249    }
250
251    fn to_cdp(&self) -> UserAgentMetadata {
252        let mut builder = UserAgentMetadataBuilder::default();
253
254        if !self.brands.is_empty() {
255            builder.brands(
256                self.brands
257                    .iter()
258                    .map(|brand| brand.to_cdp())
259                    .collect::<Vec<_>>(),
260            );
261        }
262
263        if !self.full_version_list.is_empty() {
264            builder.full_version_list(
265                self.full_version_list
266                    .iter()
267                    .map(|brand| brand.to_cdp())
268                    .collect::<Vec<_>>(),
269            );
270        }
271
272        #[allow(deprecated)]
273        if let Some(version) = &self.full_version {
274            builder.full_version(version.clone());
275        }
276
277        builder.platform(self.platform.clone().unwrap_or_default());
278        builder.platform_version(self.platform_version.clone().unwrap_or_default());
279        builder.architecture(self.architecture.clone().unwrap_or_default());
280        builder.model(self.model.clone().unwrap_or_default());
281        builder.mobile(self.mobile.unwrap_or(false));
282
283        if let Some(bitness) = &self.bitness {
284            builder.bitness(bitness.clone());
285        }
286
287        if let Some(wow64) = self.wow64 {
288            builder.wow_64(wow64);
289        }
290
291        if !self.form_factors.is_empty() {
292            builder.form_factors(self.form_factors.clone());
293        }
294
295        builder.build().expect("Failed to build UserAgentMetadata")
296    }
297}
298
299/// Browser brand/version pair used in UA metadata.
300#[derive(Clone, Debug)]
301pub struct UserAgentBrand {
302    pub brand: String,
303    pub version: String,
304}
305
306impl UserAgentBrand {
307    pub fn new<B: Into<String>, V: Into<String>>(brand: B, version: V) -> Self {
308        Self {
309            brand: brand.into(),
310            version: version.into(),
311        }
312    }
313
314    fn to_cdp(&self) -> UserAgentBrandVersion {
315        UserAgentBrandVersion {
316            brand: self.brand.clone(),
317            version: self.version.clone(),
318        }
319    }
320}
321
322/// Controller that exposes Emulation domain commands for a page.
323pub struct EmulationController {
324    page: Arc<Page>,
325}
326
327impl EmulationController {
328    pub(crate) fn new(page: Arc<Page>) -> Self {
329        Self { page }
330    }
331
332    /// Applies a collection of emulation overrides to the target page.
333    ///
334    /// # Examples
335    /// ```no_run
336    /// # use cdp_core::{EmulationConfig, Geolocation, Page};
337    /// # use std::sync::Arc;
338    /// # async fn example(page: Arc<Page>) -> anyhow::Result<()> {
339    /// let geolocation = Geolocation::new(37.7749, -122.4194).with_accuracy(1.0);
340    /// let config = EmulationConfig::default().with_geolocation(geolocation);
341    /// page.emulation().apply_config(&config).await?;
342    /// # Ok(())
343    /// # }
344    /// ```
345    pub async fn apply_config(&self, config: &EmulationConfig) -> Result<()> {
346        if let Some(geolocation) = &config.geolocation {
347            self.set_geolocation(geolocation.clone()).await?;
348        }
349        if let Some(timezone) = &config.timezone_id {
350            self.set_timezone(timezone).await?;
351        }
352        if let Some(locale) = &config.locale {
353            self.set_locale(Some(locale.as_str())).await?;
354        }
355        if let Some(media) = &config.media {
356            self.set_media(media.clone()).await?;
357        }
358        if let Some(user_agent) = &config.user_agent {
359            self.set_user_agent(user_agent.clone()).await?;
360        }
361        Ok(())
362    }
363
364    /// Overrides the page's geolocation values until cleared.
365    pub async fn set_geolocation(&self, geolocation: Geolocation) -> Result<()> {
366        let method = SetGeolocationOverride {
367            latitude: Some(geolocation.latitude),
368            longitude: Some(geolocation.longitude),
369            accuracy: geolocation.accuracy,
370            altitude: geolocation.altitude,
371            altitude_accuracy: geolocation.altitude_accuracy,
372            heading: geolocation.heading,
373            speed: geolocation.speed,
374        };
375        let _: SetGeolocationOverrideReturnObject =
376            self.page.session.send_command(method, None).await?;
377        Ok(())
378    }
379
380    /// Removes any geolocation override previously applied.
381    pub async fn clear_geolocation(&self) -> Result<()> {
382        let method = ClearGeolocationOverride(None);
383        let _: ClearGeolocationOverrideReturnObject =
384            self.page.session.send_command(method, None).await?;
385        Ok(())
386    }
387
388    /// Sets the emulated timezone identifier (IANA format).
389    pub async fn set_timezone<T: Into<String>>(&self, timezone_id: T) -> Result<()> {
390        let method = SetTimezoneOverride {
391            timezone_id: timezone_id.into(),
392        };
393        let _: SetTimezoneOverrideReturnObject =
394            self.page.session.send_command(method, None).await?;
395        Ok(())
396    }
397
398    /// Resets the timezone override to the browser default.
399    pub async fn reset_timezone(&self) -> Result<()> {
400        self.set_timezone("").await
401    }
402
403    /// Overrides the page locale (for example "en-US").
404    pub async fn set_locale(&self, locale: Option<&str>) -> Result<()> {
405        let method = SetLocaleOverride {
406            locale: locale.map(|value| value.to_string()),
407        };
408        let _: SetLocaleOverrideReturnObject = self.page.session.send_command(method, None).await?;
409        Ok(())
410    }
411
412    /// Applies media emulation settings, such as `prefers-color-scheme`.
413    pub async fn set_media(&self, media: MediaEmulation) -> Result<()> {
414        let method = SetEmulatedMedia {
415            media: media.media_type.clone(),
416            features: to_cdp_media_features(&media.features),
417        };
418        let _: SetEmulatedMediaReturnObject = self.page.session.send_command(method, None).await?;
419        Ok(())
420    }
421
422    /// Clears any previously applied media emulation overrides.
423    pub async fn clear_media(&self) -> Result<()> {
424        let method = SetEmulatedMedia {
425            media: None,
426            features: None,
427        };
428        let _: SetEmulatedMediaReturnObject = self.page.session.send_command(method, None).await?;
429        Ok(())
430    }
431
432    /// Overrides the user agent string and optional metadata.
433    pub async fn set_user_agent(&self, override_data: UserAgentOverride) -> Result<()> {
434        let method = SetUserAgentOverride {
435            user_agent: override_data.user_agent,
436            accept_language: override_data.accept_language,
437            platform: override_data.platform,
438            user_agent_metadata: override_data
439                .metadata
440                .as_ref()
441                .map(UserAgentMetadataOverride::to_cdp),
442        };
443        let _: SetUserAgentOverrideReturnObject =
444            self.page.session.send_command(method, None).await?;
445        Ok(())
446    }
447}
448
449fn to_cdp_media_features(features: &[MediaFeatureOverride]) -> Option<Vec<MediaFeature>> {
450    if features.is_empty() {
451        None
452    } else {
453        Some(
454            features
455                .iter()
456                .map(|feature| MediaFeature {
457                    name: feature.name.clone(),
458                    value: feature.value.clone(),
459                })
460                .collect(),
461        )
462    }
463}