Skip to main content

cdp_browser_lite/
error.rs

1use std::io;
2use std::path::PathBuf;
3use std::time::Duration;
4
5use cdp_lite::error::CdpError;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
9pub enum BrowserError {
10    #[error(
11        "Chrome executable not found; searched: {searched:?}. Set CHROME_PATH or use chrome_path()"
12    )]
13    ExecutableNotFound { searched: Vec<PathBuf> },
14
15    #[error("failed to spawn '{path}': {source}")]
16    SpawnFailed {
17        path: PathBuf,
18        #[source]
19        source: io::Error,
20    },
21
22    #[error("Chrome exited before opening the debugging port{hint}")]
23    EarlyExit { hint: String },
24
25    #[error("Chrome did not open the DevTools port within {timeout:?}")]
26    StartupTimeout { timeout: Duration },
27
28    #[error("port {port} is in use by a non-Chrome process")]
29    PortConflict { port: u16 },
30
31    #[error("no Chrome CDP endpoint reachable at {host}:{port}")]
32    RemoteUnavailable { host: String, port: u16 },
33
34    #[error("invalid configuration: {0}")]
35    InvalidConfig(String),
36
37    #[error("browser was stopped")]
38    Stopped,
39
40    #[error("profile error: {0}")]
41    Profile(String),
42
43    #[error(transparent)]
44    Cdp(#[from] CdpError),
45
46    #[error(transparent)]
47    Io(#[from] io::Error),
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    fn display(err: &BrowserError) -> String {
55        format!("{err}")
56    }
57
58    #[test]
59    fn executable_not_found_hint_mentions_chrome_path() {
60        let err = BrowserError::ExecutableNotFound {
61            searched: vec![PathBuf::from("/usr/bin/chrome")],
62        };
63        let msg = display(&err);
64        assert!(
65            msg.contains("CHROME_PATH"),
66            "missing CHROME_PATH hint in: {msg}"
67        );
68        assert!(
69            msg.contains("/usr/bin/chrome"),
70            "missing searched path in: {msg}"
71        );
72    }
73
74    #[test]
75    fn spawn_failed_shows_path_and_underlying_io() {
76        let err = BrowserError::SpawnFailed {
77            path: PathBuf::from("/no/such/bin"),
78            source: io::Error::new(io::ErrorKind::NotFound, "nope"),
79        };
80        let msg = display(&err);
81        assert!(
82            msg.contains("/no/such/bin"),
83            "missing binary path in: {msg}"
84        );
85        let source = std::error::Error::source(&err)
86            .map(|s| format!("{s}"))
87            .unwrap_or_default();
88        assert!(
89            source.contains("nope"),
90            "missing underlying IO message in chain: {source}"
91        );
92    }
93
94    #[test]
95    fn early_exit_message_has_hole_for_hint() {
96        let err = BrowserError::EarlyExit {
97            hint: " (close existing Chrome windows)".to_string(),
98        };
99        let msg = display(&err);
100        assert!(msg.contains("Chrome"), "missing Chrome context: {msg}");
101        assert!(
102            msg.contains("debugging port"),
103            "missing port context: {msg}"
104        );
105        assert!(
106            msg.contains("close existing Chrome windows"),
107            "missing hint text: {msg}"
108        );
109    }
110
111    #[test]
112    fn early_exit_with_empty_hint_omits_blank_parentheses() {
113        let err = BrowserError::EarlyExit {
114            hint: String::new(),
115        };
116        let msg = display(&err);
117        assert!(!msg.contains("()"), "unexpected empty parens in: {msg}");
118    }
119
120    #[test]
121    fn startup_timeout_mentions_devtools_port() {
122        let err = BrowserError::StartupTimeout {
123            timeout: Duration::from_secs(10),
124        };
125        let msg = display(&err);
126        assert!(
127            msg.contains("DevTools port"),
128            "missing DevTools context: {msg}"
129        );
130    }
131
132    #[test]
133    fn port_conflict_mentions_port_number() {
134        let err = BrowserError::PortConflict { port: 9222 };
135        let msg = display(&err);
136        assert!(msg.contains("9222"), "missing port: {msg}");
137    }
138
139    #[test]
140    fn remote_unavailable_mentions_host_and_port() {
141        let err = BrowserError::RemoteUnavailable {
142            host: "10.0.0.5".to_string(),
143            port: 9222,
144        };
145        let msg = display(&err);
146        assert!(msg.contains("10.0.0.5"), "missing host: {msg}");
147        assert!(msg.contains("9222"), "missing port: {msg}");
148    }
149
150    #[test]
151    fn invalid_config_carries_message() {
152        let err = BrowserError::InvalidConfig("bad".to_string());
153        let msg = display(&err);
154        assert!(msg.contains("invalid"), "missing 'invalid' word: {msg}");
155        assert!(msg.contains("bad"), "missing nested message: {msg}");
156    }
157
158    #[test]
159    fn stopped_is_self_describing() {
160        let msg = display(&BrowserError::Stopped);
161        assert!(
162            msg.to_lowercase().contains("stopped"),
163            "missing 'stopped' word: {msg}"
164        );
165    }
166
167    #[test]
168    fn profile_carries_message() {
169        let err = BrowserError::Profile("cannot create dir".to_string());
170        let msg = display(&err);
171        assert!(msg.contains("profile"), "missing 'profile' word: {msg}");
172        assert!(msg.contains("cannot create dir"), "missing detail: {msg}");
173    }
174
175    #[test]
176    fn test_error_display_messages_are_actionable() {
177        let cases = [
178            BrowserError::ExecutableNotFound { searched: vec![] },
179            BrowserError::EarlyExit {
180                hint: String::new(),
181            },
182            BrowserError::InvalidConfig("test".into()),
183            BrowserError::Stopped,
184            BrowserError::Profile("test".into()),
185        ];
186        for err in cases {
187            let msg = display(&err);
188            assert!(!msg.trim().is_empty(), "{err:?} produced empty display");
189        }
190    }
191
192    #[test]
193    fn from_io_error() {
194        let io_err = io::Error::other("boom");
195        let err: BrowserError = io_err.into();
196        assert!(matches!(err, BrowserError::Io(_)));
197    }
198
199    #[test]
200    fn from_cdp_error() {
201        let cdp_err = CdpError::Disconnected;
202        let err: BrowserError = cdp_err.into();
203        assert!(matches!(err, BrowserError::Cdp(_)));
204    }
205}