Skip to main content

browser_automation_cli/
constants.rs

1//! Named constants for `browser-automation-cli` (anti-hardcode).
2
3/// Network throttling presets aligned with Chrome DevTools / Puppeteer PredefinedNetworkConditions.
4#[derive(Debug, Clone, Copy)]
5pub struct NetworkPreset {
6    /// Human-readable preset name (`Slow 3G`, `Fast 4G`, ...).
7    pub name: &'static str,
8    /// When true, network is forced offline.
9    pub offline: bool,
10    /// Extra RTT latency in milliseconds.
11    pub latency_ms: f64,
12    /// Download throughput in bytes/sec (`-1.0` = unlimited).
13    pub download_throughput: f64,
14    /// Upload throughput in bytes/sec (`-1.0` = unlimited).
15    pub upload_throughput: f64,
16}
17
18/// Throughput -1 means no throttle (unlimited).
19pub const NETWORK_PRESETS: &[NetworkPreset] = &[
20    NetworkPreset {
21        name: "No throttling",
22        offline: false,
23        latency_ms: 0.0,
24        download_throughput: -1.0,
25        upload_throughput: -1.0,
26    },
27    NetworkPreset {
28        name: "Offline",
29        offline: true,
30        latency_ms: 0.0,
31        download_throughput: 0.0,
32        upload_throughput: 0.0,
33    },
34    NetworkPreset {
35        name: "Slow 3G",
36        offline: false,
37        latency_ms: 400.0,
38        download_throughput: (500.0 * 1024.0) / 8.0 * 0.8,
39        upload_throughput: (500.0 * 1024.0) / 8.0 * 0.8,
40    },
41    NetworkPreset {
42        name: "Fast 3G",
43        offline: false,
44        latency_ms: 150.0,
45        download_throughput: (1.6 * 1024.0 * 1024.0) / 8.0 * 0.9,
46        upload_throughput: (750.0 * 1024.0) / 8.0 * 0.9,
47    },
48    NetworkPreset {
49        name: "Slow 4G",
50        offline: false,
51        latency_ms: 20.0,
52        download_throughput: (1.6 * 1024.0 * 1024.0) / 8.0,
53        upload_throughput: (750.0 * 1024.0) / 8.0,
54    },
55    NetworkPreset {
56        name: "Fast 4G",
57        offline: false,
58        latency_ms: 20.0,
59        download_throughput: (9.0 * 1024.0 * 1024.0) / 8.0,
60        upload_throughput: (1.5 * 1024.0 * 1024.0) / 8.0,
61    },
62];
63
64/// Lookup a network preset by case-insensitive name.
65pub fn network_preset_by_name(name: &str) -> Option<&'static NetworkPreset> {
66    let n = name.trim();
67    NETWORK_PRESETS
68        .iter()
69        .find(|p| p.name.eq_ignore_ascii_case(n))
70}
71
72/// List known network preset names for help and validation.
73pub fn network_preset_names() -> Vec<&'static str> {
74    NETWORK_PRESETS.iter().map(|p| p.name).collect()
75}
76
77/// Parsed viewport string: `WxHxDPR[,mobile][,touch][,landscape]`.
78#[derive(Debug, Clone, PartialEq)]
79pub struct ViewportSpec {
80    /// CSS width in pixels.
81    pub width: i32,
82    /// CSS height in pixels.
83    pub height: i32,
84    /// Device pixel ratio.
85    pub device_scale_factor: f64,
86    /// Mobile metric emulation.
87    pub mobile: bool,
88    /// Touch support flag.
89    pub has_touch: bool,
90    /// Landscape orientation flag.
91    pub is_landscape: bool,
92}
93
94/// Parse `WxHxDPR` with optional `,mobile`, `,touch`, `,landscape` flags.
95pub fn parse_viewport_spec(raw: &str) -> Result<ViewportSpec, String> {
96    let mut parts = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty());
97    let dims = parts
98        .next()
99        .ok_or_else(|| "viewport empty; expected WxHxDPR".to_string())?;
100    let mut nums = dims.split('x').map(|s| s.trim());
101    let width: i32 = nums
102        .next()
103        .ok_or("viewport missing width")?
104        .parse()
105        .map_err(|_| "viewport width must be integer")?;
106    let height: i32 = nums
107        .next()
108        .ok_or("viewport missing height")?
109        .parse()
110        .map_err(|_| "viewport height must be integer")?;
111    let device_scale_factor: f64 = nums
112        .next()
113        .map(|s| {
114            s.parse()
115                .map_err(|_| "viewport dpr must be number".to_string())
116        })
117        .transpose()?
118        .unwrap_or(1.0);
119    let mut mobile = false;
120    let mut has_touch = false;
121    let mut is_landscape = false;
122    for flag in parts {
123        match flag.to_ascii_lowercase().as_str() {
124            "mobile" => mobile = true,
125            "touch" => has_touch = true,
126            "landscape" => is_landscape = true,
127            other => return Err(format!("unknown viewport flag: {other}")),
128        }
129    }
130    Ok(ViewportSpec {
131        width,
132        height,
133        device_scale_factor,
134        mobile,
135        has_touch,
136        is_landscape,
137    })
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn preset_lookup() {
146        assert!(network_preset_by_name("Slow 3G").is_some());
147        assert!(network_preset_by_name("offline").is_some());
148        assert!(network_preset_by_name("nope").is_none());
149    }
150
151    #[test]
152    fn viewport_parse() {
153        let v = parse_viewport_spec("412x823x1.75,mobile,touch").unwrap();
154        assert_eq!(v.width, 412);
155        assert_eq!(v.height, 823);
156        assert!((v.device_scale_factor - 1.75).abs() < 0.001);
157        assert!(v.mobile && v.has_touch);
158    }
159}