Skip to main content

night_fury_core/
selector.rs

1/// How to identify an element on the page.
2#[derive(Debug, Clone)]
3#[non_exhaustive]
4pub enum Selector {
5    /// @ref index from the last `snapshot()` call (e.g. `Ref(0)` = @e0).
6    Ref(usize),
7    /// CSS selector string.
8    Css(String),
9    /// Visible text match (finds first button/link/label containing this text).
10    Text(String),
11    /// ARIA role and optional accessible name.
12    ///
13    /// Resolved to a CSS attribute selector like `[role="button"][aria-label="Submit"]`.
14    /// When `name` is `None`, matches any element with the given role.
15    Role { role: String, name: Option<String> },
16}
17
18impl Selector {
19    /// Convert a `Role` selector to an equivalent CSS attribute selector string.
20    ///
21    /// Returns `None` for non-`Role` variants.
22    pub fn role_to_css(&self) -> Option<String> {
23        match self {
24            Selector::Role { role, name } => {
25                let mut css = format!("[role=\"{}\"]", css_escape_attr(role));
26                if let Some(n) = name {
27                    let escaped = css_escape_attr(n);
28                    css.push_str(&format!("[aria-label=\"{escaped}\"]"));
29                }
30                Some(css)
31            }
32            _ => None,
33        }
34    }
35}
36
37/// Escape a string for use inside a CSS attribute value (double-quoted).
38fn css_escape_attr(s: &str) -> String {
39    s.replace('\\', "\\\\").replace('"', "\\\"")
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn test_selector_debug() {
48        assert_eq!(format!("{:?}", Selector::Ref(3)), "Ref(3)");
49        assert_eq!(
50            format!("{:?}", Selector::Css("#submit".into())),
51            "Css(\"#submit\")"
52        );
53        assert_eq!(
54            format!("{:?}", Selector::Text("Sign in".into())),
55            "Text(\"Sign in\")"
56        );
57    }
58
59    #[test]
60    fn test_selector_clone() {
61        let a = Selector::Css(".btn".into());
62        let b = a.clone();
63        assert_eq!(format!("{:?}", a), format!("{:?}", b));
64    }
65
66    #[test]
67    fn test_role_to_css_with_name() {
68        let sel = Selector::Role {
69            role: "button".into(),
70            name: Some("Submit".into()),
71        };
72        assert_eq!(
73            sel.role_to_css().unwrap(),
74            "[role=\"button\"][aria-label=\"Submit\"]"
75        );
76    }
77
78    #[test]
79    fn test_role_to_css_without_name() {
80        let sel = Selector::Role {
81            role: "link".into(),
82            name: None,
83        };
84        assert_eq!(sel.role_to_css().unwrap(), "[role=\"link\"]");
85    }
86
87    #[test]
88    fn test_role_to_css_escapes_quotes() {
89        let sel = Selector::Role {
90            role: "button".into(),
91            name: Some("Say \"hello\"".into()),
92        };
93        assert_eq!(
94            sel.role_to_css().unwrap(),
95            "[role=\"button\"][aria-label=\"Say \\\"hello\\\"\"]"
96        );
97    }
98
99    #[test]
100    fn test_role_to_css_returns_none_for_other_variants() {
101        assert!(Selector::Css("div".into()).role_to_css().is_none());
102        assert!(Selector::Text("hi".into()).role_to_css().is_none());
103        assert!(Selector::Ref(0).role_to_css().is_none());
104    }
105
106    #[test]
107    fn test_role_debug() {
108        let sel = Selector::Role {
109            role: "button".into(),
110            name: Some("OK".into()),
111        };
112        let dbg = format!("{:?}", sel);
113        assert!(dbg.contains("Role"));
114        assert!(dbg.contains("button"));
115        assert!(dbg.contains("OK"));
116    }
117}