Skip to main content

browser_commander/browser/
media.rs

1//! Media emulation for browser automation.
2//!
3//! Provides unified color scheme emulation across browser engines.
4
5use crate::core::engine::EngineType;
6
7/// Supported color scheme values for media emulation.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ColorScheme {
10    /// Light color scheme (`prefers-color-scheme: light`).
11    Light,
12    /// Dark color scheme (`prefers-color-scheme: dark`).
13    Dark,
14    /// No preference (`prefers-color-scheme: no-preference`).
15    NoPreference,
16}
17
18impl ColorScheme {
19    /// Returns the string value used in CDP and browser APIs.
20    pub fn as_str(&self) -> &'static str {
21        match self {
22            ColorScheme::Light => "light",
23            ColorScheme::Dark => "dark",
24            ColorScheme::NoPreference => "no-preference",
25        }
26    }
27}
28
29impl std::fmt::Display for ColorScheme {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{}", self.as_str())
32    }
33}
34
35impl std::str::FromStr for ColorScheme {
36    type Err = anyhow::Error;
37
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        match s {
40            "light" => Ok(ColorScheme::Light),
41            "dark" => Ok(ColorScheme::Dark),
42            "no-preference" => Ok(ColorScheme::NoPreference),
43            _ => Err(anyhow::anyhow!(
44                "Invalid color scheme: \"{}\". Expected one of: light, dark, no-preference",
45                s
46            )),
47        }
48    }
49}
50
51/// Options for media emulation.
52#[derive(Debug, Clone)]
53pub struct EmulateMediaOptions {
54    /// The color scheme to emulate. `None` resets the emulation.
55    pub color_scheme: Option<ColorScheme>,
56    /// The engine type to use.
57    pub engine: EngineType,
58}
59
60impl EmulateMediaOptions {
61    /// Create options with a specific color scheme for the given engine.
62    pub fn new(engine: EngineType, color_scheme: Option<ColorScheme>) -> Self {
63        Self {
64            color_scheme,
65            engine,
66        }
67    }
68
69    /// Create options for dark color scheme.
70    pub fn dark(engine: EngineType) -> Self {
71        Self::new(engine, Some(ColorScheme::Dark))
72    }
73
74    /// Create options for light color scheme.
75    pub fn light(engine: EngineType) -> Self {
76        Self::new(engine, Some(ColorScheme::Light))
77    }
78
79    /// Create options for no-preference color scheme.
80    pub fn no_preference(engine: EngineType) -> Self {
81        Self::new(engine, Some(ColorScheme::NoPreference))
82    }
83
84    /// Create options to reset color scheme emulation.
85    pub fn reset(engine: EngineType) -> Self {
86        Self::new(engine, None)
87    }
88}
89
90/// Emulate media features (e.g. `prefers-color-scheme`) for a browser page.
91///
92/// # Note
93///
94/// This helper validates and records the desired media emulation settings.
95/// Launch-time emulation for live Chromiumoxide, Playwright, and Puppeteer
96/// sessions is applied by the browser launchers/adapters.
97///
98/// For chromiumoxide, use `Page::emulate_media` or send a CDP command:
99/// ```text
100/// Emulation.setEmulatedMedia with features: [{name: "prefers-color-scheme", value: "dark"}]
101/// ```
102///
103/// For Fantoccini (WebDriver), use Chrome DevTools Protocol via the session:
104/// ```text
105/// session.issue_cmd(Command::CustomCommand("Emulation.setEmulatedMedia", params))
106/// ```
107///
108/// # Arguments
109///
110/// * `options` - The emulate media options
111///
112/// # Errors
113///
114/// Returns an error if the engine is unsupported.
115pub async fn emulate_media(options: EmulateMediaOptions) -> Result<(), anyhow::Error> {
116    match options.engine {
117        EngineType::Chromiumoxide
118        | EngineType::Fantoccini
119        | EngineType::Playwright
120        | EngineType::Puppeteer => {
121            // Placeholder: actual implementation would send CDP command to the page:
122            // Emulation.setEmulatedMedia with features: [{ name: "prefers-color-scheme", value: ... }]
123            tracing::debug!(
124                "emulate_media: engine={}, color_scheme={:?}",
125                options.engine,
126                options.color_scheme.as_ref().map(|cs| cs.as_str())
127            );
128            Ok(())
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use std::str::FromStr;
137
138    #[test]
139    fn color_scheme_as_str() {
140        assert_eq!(ColorScheme::Light.as_str(), "light");
141        assert_eq!(ColorScheme::Dark.as_str(), "dark");
142        assert_eq!(ColorScheme::NoPreference.as_str(), "no-preference");
143    }
144
145    #[test]
146    fn color_scheme_display() {
147        assert_eq!(ColorScheme::Light.to_string(), "light");
148        assert_eq!(ColorScheme::Dark.to_string(), "dark");
149        assert_eq!(ColorScheme::NoPreference.to_string(), "no-preference");
150    }
151
152    #[test]
153    fn color_scheme_from_str_valid() {
154        assert_eq!(ColorScheme::from_str("light").unwrap(), ColorScheme::Light);
155        assert_eq!(ColorScheme::from_str("dark").unwrap(), ColorScheme::Dark);
156        assert_eq!(
157            ColorScheme::from_str("no-preference").unwrap(),
158            ColorScheme::NoPreference
159        );
160    }
161
162    #[test]
163    fn color_scheme_from_str_invalid() {
164        assert!(ColorScheme::from_str("invalid").is_err());
165        assert!(ColorScheme::from_str("").is_err());
166        assert!(ColorScheme::from_str("DARK").is_err());
167    }
168
169    #[test]
170    fn emulate_media_options_dark() {
171        let opts = EmulateMediaOptions::dark(EngineType::Chromiumoxide);
172        assert_eq!(opts.color_scheme, Some(ColorScheme::Dark));
173        assert_eq!(opts.engine, EngineType::Chromiumoxide);
174    }
175
176    #[test]
177    fn emulate_media_options_light() {
178        let opts = EmulateMediaOptions::light(EngineType::Chromiumoxide);
179        assert_eq!(opts.color_scheme, Some(ColorScheme::Light));
180    }
181
182    #[test]
183    fn emulate_media_options_no_preference() {
184        let opts = EmulateMediaOptions::no_preference(EngineType::Chromiumoxide);
185        assert_eq!(opts.color_scheme, Some(ColorScheme::NoPreference));
186    }
187
188    #[test]
189    fn emulate_media_options_reset() {
190        let opts = EmulateMediaOptions::reset(EngineType::Chromiumoxide);
191        assert_eq!(opts.color_scheme, None);
192    }
193
194    #[tokio::test]
195    async fn emulate_media_chromiumoxide() {
196        let opts = EmulateMediaOptions::dark(EngineType::Chromiumoxide);
197        assert!(emulate_media(opts).await.is_ok());
198    }
199
200    #[tokio::test]
201    async fn emulate_media_fantoccini() {
202        let opts = EmulateMediaOptions::light(EngineType::Fantoccini);
203        assert!(emulate_media(opts).await.is_ok());
204    }
205}