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 is a placeholder implementation. The actual implementation requires
95/// integration with a live browser page object from chromiumoxide or fantoccini.
96///
97/// For chromiumoxide, use `Page::emulate_media` or send a CDP command:
98/// ```text
99/// Emulation.setEmulatedMedia with features: [{name: "prefers-color-scheme", value: "dark"}]
100/// ```
101///
102/// For fantoccini (WebDriver), use Chrome DevTools Protocol via the session:
103/// ```text
104/// session.issue_cmd(Command::CustomCommand("Emulation.setEmulatedMedia", params))
105/// ```
106///
107/// # Arguments
108///
109/// * `options` - The emulate media options
110///
111/// # Errors
112///
113/// Returns an error if the engine is unsupported.
114pub async fn emulate_media(options: EmulateMediaOptions) -> Result<(), anyhow::Error> {
115    match options.engine {
116        EngineType::Chromiumoxide | EngineType::Fantoccini => {
117            // Placeholder: actual implementation would send CDP command to the page:
118            // Emulation.setEmulatedMedia with features: [{ name: "prefers-color-scheme", value: ... }]
119            tracing::debug!(
120                "emulate_media: engine={}, color_scheme={:?}",
121                options.engine,
122                options.color_scheme.as_ref().map(|cs| cs.as_str())
123            );
124            Ok(())
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use std::str::FromStr;
133
134    #[test]
135    fn color_scheme_as_str() {
136        assert_eq!(ColorScheme::Light.as_str(), "light");
137        assert_eq!(ColorScheme::Dark.as_str(), "dark");
138        assert_eq!(ColorScheme::NoPreference.as_str(), "no-preference");
139    }
140
141    #[test]
142    fn color_scheme_display() {
143        assert_eq!(ColorScheme::Light.to_string(), "light");
144        assert_eq!(ColorScheme::Dark.to_string(), "dark");
145        assert_eq!(ColorScheme::NoPreference.to_string(), "no-preference");
146    }
147
148    #[test]
149    fn color_scheme_from_str_valid() {
150        assert_eq!(ColorScheme::from_str("light").unwrap(), ColorScheme::Light);
151        assert_eq!(ColorScheme::from_str("dark").unwrap(), ColorScheme::Dark);
152        assert_eq!(
153            ColorScheme::from_str("no-preference").unwrap(),
154            ColorScheme::NoPreference
155        );
156    }
157
158    #[test]
159    fn color_scheme_from_str_invalid() {
160        assert!(ColorScheme::from_str("invalid").is_err());
161        assert!(ColorScheme::from_str("").is_err());
162        assert!(ColorScheme::from_str("DARK").is_err());
163    }
164
165    #[test]
166    fn emulate_media_options_dark() {
167        let opts = EmulateMediaOptions::dark(EngineType::Chromiumoxide);
168        assert_eq!(opts.color_scheme, Some(ColorScheme::Dark));
169        assert_eq!(opts.engine, EngineType::Chromiumoxide);
170    }
171
172    #[test]
173    fn emulate_media_options_light() {
174        let opts = EmulateMediaOptions::light(EngineType::Chromiumoxide);
175        assert_eq!(opts.color_scheme, Some(ColorScheme::Light));
176    }
177
178    #[test]
179    fn emulate_media_options_no_preference() {
180        let opts = EmulateMediaOptions::no_preference(EngineType::Chromiumoxide);
181        assert_eq!(opts.color_scheme, Some(ColorScheme::NoPreference));
182    }
183
184    #[test]
185    fn emulate_media_options_reset() {
186        let opts = EmulateMediaOptions::reset(EngineType::Chromiumoxide);
187        assert_eq!(opts.color_scheme, None);
188    }
189
190    #[tokio::test]
191    async fn emulate_media_chromiumoxide() {
192        let opts = EmulateMediaOptions::dark(EngineType::Chromiumoxide);
193        assert!(emulate_media(opts).await.is_ok());
194    }
195
196    #[tokio::test]
197    async fn emulate_media_fantoccini() {
198        let opts = EmulateMediaOptions::light(EngineType::Fantoccini);
199        assert!(emulate_media(opts).await.is_ok());
200    }
201}