1use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, BrowserError>;
10
11#[derive(Error, Debug)]
16pub enum BrowserError {
17 #[error("Browser launch failed: {reason}")]
19 LaunchFailed {
20 reason: String,
22 },
23
24 #[error("CDP error during '{operation}': {message}")]
26 CdpError {
27 operation: String,
29 message: String,
31 },
32
33 #[error("Browser pool exhausted (active={active}, max={max})")]
35 PoolExhausted {
36 active: usize,
38 max: usize,
40 },
41
42 #[error("Timeout after {duration_ms}ms during '{operation}'")]
44 Timeout {
45 operation: String,
47 duration_ms: u64,
49 },
50
51 #[error("Navigation to '{url}' failed: {reason}")]
53 NavigationFailed {
54 url: String,
56 reason: String,
58 },
59
60 #[error("Script execution failed: {reason}")]
62 ScriptExecutionFailed {
63 script: String,
65 reason: String,
67 },
68
69 #[error("Browser connection error: {reason}")]
71 ConnectionError {
72 url: String,
74 reason: String,
76 },
77
78 #[error("Configuration error: {0}")]
80 ConfigError(String),
81
82 #[error("I/O error: {0}")]
84 Io(#[from] std::io::Error),
85}
86
87impl From<chromiumoxide::error::CdpError> for BrowserError {
88 fn from(err: chromiumoxide::error::CdpError) -> Self {
89 Self::CdpError {
90 operation: "unknown".to_string(),
91 message: err.to_string(),
92 }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn launch_failed_display() {
102 let e = BrowserError::LaunchFailed {
103 reason: "binary not found".to_string(),
104 };
105 assert!(e.to_string().contains("binary not found"));
106 }
107
108 #[test]
109 fn pool_exhausted_display() {
110 let e = BrowserError::PoolExhausted {
111 active: 10,
112 max: 10,
113 };
114 assert!(e.to_string().contains("10"));
115 }
116
117 #[test]
118 fn navigation_failed_includes_url() {
119 let e = BrowserError::NavigationFailed {
120 url: "https://example.com".to_string(),
121 reason: "DNS failure".to_string(),
122 };
123 assert!(e.to_string().contains("example.com"));
124 assert!(e.to_string().contains("DNS failure"));
125 }
126
127 #[test]
128 fn timeout_display() {
129 let e = BrowserError::Timeout {
130 operation: "page.load".to_string(),
131 duration_ms: 30_000,
132 };
133 assert!(e.to_string().contains("30000"));
134 }
135
136 #[test]
137 fn cdp_error_display() {
138 let e = BrowserError::CdpError {
139 operation: "Page.navigate".to_string(),
140 message: "Target closed".to_string(),
141 };
142 let s = e.to_string();
143 assert!(s.contains("Page.navigate"));
144 assert!(s.contains("Target closed"));
145 }
146
147 #[test]
148 fn script_execution_failed_display() {
149 let e = BrowserError::ScriptExecutionFailed {
150 script: "document.title".to_string(),
151 reason: "Execution context destroyed".to_string(),
152 };
153 assert!(e.to_string().contains("Execution context destroyed"));
154 }
155
156 #[test]
157 fn connection_error_display() {
158 let e = BrowserError::ConnectionError {
159 url: "ws://127.0.0.1:9222/json/version".to_string(),
160 reason: "connection refused".to_string(),
161 };
162 let s = e.to_string();
163 assert!(s.contains("connection refused"));
164 }
165
166 #[test]
167 fn config_error_display() {
168 let e = BrowserError::ConfigError("pool.max_size must be >= 1".to_string());
169 assert!(e.to_string().contains("pool.max_size"));
170 }
171
172 #[test]
173 fn io_error_wraps_std() {
174 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
175 let e = BrowserError::Io(io);
176 assert!(e.to_string().contains("file not found"));
177 }
178
179 #[test]
180 fn launch_failed_is_debug_printable() {
181 let e = BrowserError::LaunchFailed {
182 reason: "test".to_string(),
183 };
184 assert!(!format!("{e:?}").is_empty());
185 }
186
187 #[test]
188 fn pool_exhausted_reports_both_counts() {
189 let e = BrowserError::PoolExhausted { active: 5, max: 5 };
190 let s = e.to_string();
191 assert!(s.contains("active=5"));
192 assert!(s.contains("max=5"));
193 }
194}