Skip to main content

browser_automation_cli/
constants.rs

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