viewpoint_core/page/emulation/mod.rs
1//! Page emulation features for media and vision deficiency emulation.
2
3use std::sync::Arc;
4use tracing::{debug, info, instrument};
5
6use viewpoint_cdp::protocol::emulation::{
7 MediaFeature, SetDeviceMetricsOverrideParams, SetEmulatedMediaParams,
8 SetEmulatedVisionDeficiencyParams, ViewportSize, VisionDeficiency as CdpVisionDeficiency,
9};
10use viewpoint_cdp::CdpConnection;
11
12use super::Page;
13use crate::context::{ColorScheme, ForcedColors, ReducedMotion};
14use crate::error::PageError;
15
16/// Media type for CSS media emulation.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum MediaType {
19 /// Screen media type.
20 Screen,
21 /// Print media type.
22 Print,
23}
24
25impl MediaType {
26 /// Convert to CSS media type string.
27 pub fn as_str(&self) -> &'static str {
28 match self {
29 Self::Screen => "screen",
30 Self::Print => "print",
31 }
32 }
33}
34
35/// Vision deficiency types for accessibility testing.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum VisionDeficiency {
38 /// No vision deficiency emulation (normal vision).
39 #[default]
40 None,
41 /// Achromatopsia - complete color blindness.
42 Achromatopsia,
43 /// Blurred vision.
44 BlurredVision,
45 /// Deuteranopia - green-blind (red-green color blindness).
46 Deuteranopia,
47 /// Protanopia - red-blind (red-green color blindness).
48 Protanopia,
49 /// Tritanopia - blue-blind (blue-yellow color blindness).
50 Tritanopia,
51}
52
53impl From<VisionDeficiency> for CdpVisionDeficiency {
54 fn from(deficiency: VisionDeficiency) -> Self {
55 match deficiency {
56 VisionDeficiency::None => CdpVisionDeficiency::None,
57 VisionDeficiency::Achromatopsia => CdpVisionDeficiency::Achromatopsia,
58 VisionDeficiency::BlurredVision => CdpVisionDeficiency::BlurredVision,
59 VisionDeficiency::Deuteranopia => CdpVisionDeficiency::Deuteranopia,
60 VisionDeficiency::Protanopia => CdpVisionDeficiency::Protanopia,
61 VisionDeficiency::Tritanopia => CdpVisionDeficiency::Tritanopia,
62 }
63 }
64}
65
66/// Builder for emulating CSS media features on a page.
67///
68/// Use this to test how your application responds to different media preferences
69/// like dark mode, print media, reduced motion, and forced colors.
70///
71/// # Example
72///
73/// ```ignore
74/// // Emulate dark mode
75/// page.emulate_media()
76/// .color_scheme(ColorScheme::Dark)
77/// .apply()
78/// .await?;
79///
80/// // Emulate print media
81/// page.emulate_media()
82/// .media(MediaType::Print)
83/// .apply()
84/// .await?;
85///
86/// // Combine multiple settings
87/// page.emulate_media()
88/// .color_scheme(ColorScheme::Dark)
89/// .reduced_motion(ReducedMotion::Reduce)
90/// .apply()
91/// .await?;
92///
93/// // Clear all media emulation
94/// page.emulate_media()
95/// .clear()
96/// .await?;
97/// ```
98#[derive(Debug)]
99pub struct EmulateMediaBuilder<'a> {
100 connection: &'a Arc<CdpConnection>,
101 session_id: &'a str,
102 media: Option<MediaType>,
103 color_scheme: Option<ColorScheme>,
104 reduced_motion: Option<ReducedMotion>,
105 forced_colors: Option<ForcedColors>,
106}
107
108impl<'a> EmulateMediaBuilder<'a> {
109 /// Create a new emulate media builder.
110 pub(crate) fn new(connection: &'a Arc<CdpConnection>, session_id: &'a str) -> Self {
111 Self {
112 connection,
113 session_id,
114 media: None,
115 color_scheme: None,
116 reduced_motion: None,
117 forced_colors: None,
118 }
119 }
120
121 /// Set the CSS media type (screen or print).
122 #[must_use]
123 pub fn media(mut self, media: MediaType) -> Self {
124 self.media = Some(media);
125 self
126 }
127
128 /// Set the color scheme preference.
129 ///
130 /// This affects CSS `prefers-color-scheme` media queries.
131 #[must_use]
132 pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
133 self.color_scheme = Some(color_scheme);
134 self
135 }
136
137 /// Set the reduced motion preference.
138 ///
139 /// This affects CSS `prefers-reduced-motion` media queries.
140 #[must_use]
141 pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
142 self.reduced_motion = Some(reduced_motion);
143 self
144 }
145
146 /// Set the forced colors preference.
147 ///
148 /// This affects CSS `forced-colors` media queries.
149 #[must_use]
150 pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
151 self.forced_colors = Some(forced_colors);
152 self
153 }
154
155 /// Apply the media emulation settings.
156 ///
157 /// # Errors
158 ///
159 /// Returns an error if the CDP command fails.
160 #[instrument(level = "debug", skip(self))]
161 pub async fn apply(self) -> Result<(), PageError> {
162 let mut features = Vec::new();
163
164 if let Some(color_scheme) = self.color_scheme {
165 features.push(MediaFeature {
166 name: "prefers-color-scheme".to_string(),
167 value: match color_scheme {
168 ColorScheme::Light => "light".to_string(),
169 ColorScheme::Dark => "dark".to_string(),
170 ColorScheme::NoPreference => "no-preference".to_string(),
171 },
172 });
173 }
174
175 if let Some(reduced_motion) = self.reduced_motion {
176 features.push(MediaFeature {
177 name: "prefers-reduced-motion".to_string(),
178 value: match reduced_motion {
179 ReducedMotion::Reduce => "reduce".to_string(),
180 ReducedMotion::NoPreference => "no-preference".to_string(),
181 },
182 });
183 }
184
185 if let Some(forced_colors) = self.forced_colors {
186 features.push(MediaFeature {
187 name: "forced-colors".to_string(),
188 value: match forced_colors {
189 ForcedColors::Active => "active".to_string(),
190 ForcedColors::None => "none".to_string(),
191 },
192 });
193 }
194
195 let media = self.media.map(|m| m.as_str().to_string());
196 let features_opt = if features.is_empty() { None } else { Some(features) };
197
198 debug!(
199 media = ?media,
200 features_count = features_opt.as_ref().map_or(0, std::vec::Vec::len),
201 "Applying media emulation"
202 );
203
204 self.connection
205 .send_command::<_, serde_json::Value>(
206 "Emulation.setEmulatedMedia",
207 Some(SetEmulatedMediaParams {
208 media,
209 features: features_opt,
210 }),
211 Some(self.session_id),
212 )
213 .await?;
214
215 Ok(())
216 }
217
218 /// Clear all media emulation.
219 ///
220 /// This resets media type and all media features to their defaults.
221 ///
222 /// # Errors
223 ///
224 /// Returns an error if the CDP command fails.
225 #[instrument(level = "debug", skip(self))]
226 pub async fn clear(self) -> Result<(), PageError> {
227 debug!("Clearing media emulation");
228
229 self.connection
230 .send_command::<_, serde_json::Value>(
231 "Emulation.setEmulatedMedia",
232 Some(SetEmulatedMediaParams {
233 media: Some(String::new()), // Empty string clears media type
234 features: Some(Vec::new()), // Empty array clears features
235 }),
236 Some(self.session_id),
237 )
238 .await?;
239
240 Ok(())
241 }
242}
243
244/// Emulate a vision deficiency on the page (implementation).
245///
246/// This is useful for accessibility testing to ensure your application
247/// is usable by people with various types of color blindness.
248///
249/// # Errors
250///
251/// Returns an error if the CDP command fails.
252#[instrument(level = "debug", skip(connection))]
253async fn emulate_vision_deficiency_impl(
254 connection: &Arc<CdpConnection>,
255 session_id: &str,
256 deficiency: VisionDeficiency,
257) -> Result<(), PageError> {
258 debug!(?deficiency, "Emulating vision deficiency");
259
260 connection
261 .send_command::<_, serde_json::Value>(
262 "Emulation.setEmulatedVisionDeficiency",
263 Some(SetEmulatedVisionDeficiencyParams::new(deficiency.into())),
264 Some(session_id),
265 )
266 .await?;
267
268 Ok(())
269}
270
271// =============================================================================
272// Page impl for viewport and emulation methods
273// =============================================================================
274
275impl Page {
276 // =========================================================================
277 // Viewport Methods
278 // =========================================================================
279
280 /// Get the current viewport size.
281 ///
282 /// Returns the width and height in pixels.
283 pub fn viewport_size(&self) -> Option<ViewportSize> {
284 // This would need to be tracked during page creation
285 // For now, return None to indicate it's not set
286 None
287 }
288
289 /// Set the viewport size.
290 ///
291 /// # Example
292 ///
293 /// ```no_run
294 /// # async fn example(page: viewpoint_core::Page) -> Result<(), viewpoint_core::CoreError> {
295 /// page.set_viewport_size(1280, 720).await?;
296 /// # Ok(())
297 /// # }
298 /// ```
299 #[instrument(level = "info", skip(self), fields(width = width, height = height))]
300 pub async fn set_viewport_size(&self, width: i32, height: i32) -> Result<(), PageError> {
301 if self.closed {
302 return Err(PageError::Closed);
303 }
304
305 self.connection
306 .send_command::<_, serde_json::Value>(
307 "Emulation.setDeviceMetricsOverride",
308 Some(SetDeviceMetricsOverrideParams {
309 width,
310 height,
311 device_scale_factor: 1.0,
312 mobile: false,
313 scale: None,
314 screen_width: None,
315 screen_height: None,
316 position_x: None,
317 position_y: None,
318 dont_set_visible_size: None,
319 screen_orientation: None,
320 viewport: None,
321 display_feature: None,
322 device_posture: None,
323 }),
324 Some(&self.session_id),
325 )
326 .await?;
327
328 info!("Viewport size set to {}x{}", width, height);
329 Ok(())
330 }
331
332 /// Bring this page to the front (activate the tab).
333 ///
334 /// # Errors
335 ///
336 /// Returns an error if the page is closed or the CDP command fails.
337 #[instrument(level = "info", skip(self))]
338 pub async fn bring_to_front(&self) -> Result<(), PageError> {
339 if self.closed {
340 return Err(PageError::Closed);
341 }
342
343 self.connection
344 .send_command::<_, serde_json::Value>(
345 "Page.bringToFront",
346 None::<()>,
347 Some(&self.session_id),
348 )
349 .await?;
350
351 info!("Page brought to front");
352 Ok(())
353 }
354
355 // =========================================================================
356 // Emulation Methods
357 // =========================================================================
358
359 /// Create a builder for emulating CSS media features.
360 ///
361 /// Use this to test how your application responds to different media preferences
362 /// like dark mode, print media, reduced motion, and forced colors.
363 ///
364 /// # Example
365 ///
366 /// ```no_run
367 /// use viewpoint_core::{Page, page::MediaType};
368 /// use viewpoint_core::{ColorScheme, ReducedMotion};
369 ///
370 /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
371 /// // Emulate dark mode
372 /// page.emulate_media()
373 /// .color_scheme(ColorScheme::Dark)
374 /// .apply()
375 /// .await?;
376 ///
377 /// // Emulate print media
378 /// page.emulate_media()
379 /// .media(MediaType::Print)
380 /// .apply()
381 /// .await?;
382 ///
383 /// // Combine multiple settings
384 /// page.emulate_media()
385 /// .color_scheme(ColorScheme::Dark)
386 /// .reduced_motion(ReducedMotion::Reduce)
387 /// .apply()
388 /// .await?;
389 ///
390 /// // Clear all media emulation
391 /// page.emulate_media()
392 /// .clear()
393 /// .await?;
394 /// # Ok(())
395 /// # }
396 /// ```
397 pub fn emulate_media(&self) -> EmulateMediaBuilder<'_> {
398 EmulateMediaBuilder::new(&self.connection, &self.session_id)
399 }
400
401 /// Emulate a vision deficiency on the page.
402 ///
403 /// This is useful for accessibility testing to ensure your application
404 /// is usable by people with various types of color blindness.
405 ///
406 /// # Example
407 ///
408 /// ```no_run
409 /// use viewpoint_core::{Page, page::VisionDeficiency};
410 ///
411 /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
412 /// // Emulate deuteranopia (green-blind)
413 /// page.emulate_vision_deficiency(VisionDeficiency::Deuteranopia).await?;
414 ///
415 /// // Clear vision deficiency emulation
416 /// page.emulate_vision_deficiency(VisionDeficiency::None).await?;
417 /// # Ok(())
418 /// # }
419 /// ```
420 ///
421 /// # Errors
422 ///
423 /// Returns an error if the page is closed or the CDP command fails.
424 pub async fn emulate_vision_deficiency(
425 &self,
426 deficiency: VisionDeficiency,
427 ) -> Result<(), PageError> {
428 if self.closed {
429 return Err(PageError::Closed);
430 }
431 emulate_vision_deficiency_impl(&self.connection, &self.session_id, deficiency).await
432 }
433}
434
435#[cfg(test)]
436mod tests;