Skip to main content

earl_protocol_browser/
error.rs

1#[derive(Debug, thiserror::Error)]
2pub enum BrowserError {
3    #[error(
4        "browser step {step} ({action}) failed: element not found — {selector} — completed {completed} of {total} steps"
5    )]
6    ElementNotFound {
7        step: usize,
8        action: String,
9        selector: String,
10        completed: usize,
11        total: usize,
12    },
13
14    #[error(
15        "browser step {step} ({action}) failed: element not interactable — {selector} — completed {completed} of {total} steps"
16    )]
17    ElementNotInteractable {
18        step: usize,
19        action: String,
20        selector: String,
21        completed: usize,
22        total: usize,
23    },
24
25    #[error("browser step {step} (navigate) failed: {reason}")]
26    NavigationFailed { step: usize, reason: String },
27
28    #[error("browser step {step} ({action}) assertion failed: {message}")]
29    AssertionFailed {
30        step: usize,
31        action: String,
32        message: String,
33    },
34
35    #[error("browser renderer crashed at step {step}")]
36    RendererCrashed { step: usize },
37
38    #[error(
39        "browser step {step}: a dialog is blocking the page — add a handle_dialog step before this one"
40    )]
41    DialogBlocking { step: usize },
42
43    #[error(
44        "browser step {step}: a download was triggered — use a download step with save_to to handle it explicitly"
45    )]
46    DownloadBlocked { step: usize },
47
48    #[error(
49        "browser step {step} (click): a new tab was opened — add a tabs step with operation=\"select\" to switch to it"
50    )]
51    NewTabOpened { step: usize },
52
53    #[error("browser step {step} ({action}) timed out after {timeout_ms}ms")]
54    Timeout {
55        step: usize,
56        action: String,
57        timeout_ms: u64,
58    },
59
60    #[error("ref \"{ref_id}\" no longer exists in the accessibility tree (used in {action})")]
61    StaleRef { ref_id: String, action: String },
62
63    #[error(
64        "URL scheme \"{scheme}\" is not allowed in navigate — only http and https are permitted"
65    )]
66    DisallowedScheme { scheme: String },
67
68    // session_id intentionally omitted from the Display string to avoid CWE-532 (cleartext
69    // logging of sensitive identifiers). The caller already knows which session they requested.
70    #[error("browser session is locked by another earl process (PID {pid})")]
71    SessionLocked { session_id: String, pid: u32 },
72
73    #[error(
74        "Chrome not found — install Chrome/Chromium or set EARL_BROWSER_PATH\nPaths tried:\n{paths}"
75    )]
76    ChromeNotFound { paths: String },
77
78    #[error("browser session lost during step {step} ({action}): {reason}")]
79    SessionLost {
80        step: usize,
81        action: String,
82        reason: String,
83    },
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn error_messages_include_context() {
92        let e = BrowserError::ElementNotFound {
93            step: 2,
94            action: "click".into(),
95            selector: "#submit".into(),
96            completed: 1,
97            total: 5,
98        };
99        let msg = e.to_string();
100        assert!(msg.contains("step 2"));
101        assert!(msg.contains("click"));
102        assert!(msg.contains("#submit"));
103        assert!(msg.contains("completed 1 of 5"));
104
105        let e2 = BrowserError::DisallowedScheme {
106            scheme: "file".into(),
107        };
108        assert!(e2.to_string().contains("file"));
109        assert!(e2.to_string().contains("http"));
110    }
111}