llm_browser_testkit/
selectors.rs1#[must_use]
15pub fn sanitize_selector(raw: &str) -> String {
16 let mut s = raw.trim().to_owned();
17 if let Some(rest) = s.strip_prefix("```") {
19 let rest = rest.strip_suffix("```").unwrap_or(rest);
20 s = rest.trim().to_owned();
21 if let Some((_, after_lang)) = s.split_once('\n') {
22 s = after_lang.trim().to_owned();
23 }
24 }
25 if s.len() >= 2 && s.starts_with('`') && s.ends_with('`') {
27 s = s[1..s.len() - 1].to_owned();
28 }
29 if s.len() >= 2
31 && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
32 {
33 s = s[1..s.len() - 1].to_owned();
34 }
35 s.trim().to_owned()
36}
37
38#[must_use]
44pub fn selector_is_useless(sel: &str) -> bool {
45 let s = sel.trim();
46 if s.is_empty() {
47 return true;
48 }
49 let lower = s.to_lowercase();
50 matches!(
51 lower.as_str(),
52 "*" | ":not(*)" | "null" | "undefined" | "none" | "n/a" | "na" | "nil"
53 ) || lower.starts_with(":not(")
54 || lower.starts_with('{')
55 || lower.starts_with("json")
56 || s.contains('\n')
57}
58
59pub fn validate_selector(sel: &str) -> Result<(), String> {
70 let mut parens = 0i32;
71 let mut brackets = 0i32;
72 let mut in_quote: Option<char> = None;
73 for c in sel.chars() {
74 if let Some(q) = in_quote {
75 if c == q {
76 in_quote = None;
77 }
78 continue;
79 }
80 match c {
81 '"' | '\'' => in_quote = Some(c),
82 '(' => parens += 1,
83 ')' => parens -= 1,
84 '[' => brackets += 1,
85 ']' => brackets -= 1,
86 _ => {}
87 }
88 if parens < 0 || brackets < 0 {
89 return Err(format!("unbalanced '{c}' in selector: {sel:?}"));
90 }
91 }
92 if in_quote.is_some() {
93 return Err(format!("unterminated quote in selector: {sel:?}"));
94 }
95 if parens != 0 || brackets != 0 {
96 return Err(format!("unbalanced brackets in selector: {sel:?}"));
97 }
98 Ok(())
99}
100
101#[must_use]
104pub fn selector_matches_js(selector: &str) -> String {
105 let escaped = selector.replace('\\', "\\\\").replace('\'', "\\'");
106 format!("document.querySelector('{escaped}') !== null")
107}
108
109#[cfg(test)]
110mod tests {
111 use super::{sanitize_selector, selector_is_useless, validate_selector};
112
113 #[test]
114 fn test_sanitize_trims_whitespace() {
115 assert_eq!(sanitize_selector(" #login "), "#login");
116 }
117
118 #[test]
119 fn test_sanitize_strips_code_fence() {
120 assert_eq!(
121 sanitize_selector("```css\nbutton.btn--primary\n```"),
122 "button.btn--primary"
123 );
124 }
125
126 #[test]
127 fn test_sanitize_strips_quotes() {
128 assert_eq!(sanitize_selector("\"button#send\""), "button#send");
129 assert_eq!(
130 sanitize_selector("'input[name=email]'"),
131 "input[name=email]"
132 );
133 assert_eq!(sanitize_selector("`a.login-link`"), "a.login-link");
134 }
135
136 #[test]
137 fn test_sanitize_empty() {
138 assert_eq!(sanitize_selector("```\n\n```"), "");
139 assert_eq!(sanitize_selector(" "), "");
140 }
141
142 #[test]
143 fn test_sanitize_quoted_fenced() {
144 assert_eq!(sanitize_selector("```css\n\"div.card\"\n```"), "div.card");
145 }
146
147 #[test]
148 fn test_useless_selectors() {
149 for bad in [
150 "",
151 " ",
152 "*",
153 ":not(*)",
154 ":not(anything)",
155 "null",
156 "undefined",
157 "none",
158 "N/A",
159 "{",
160 "{\"a\":1}",
161 "json {\"a\":1}",
162 ] {
163 assert!(selector_is_useless(bad), "expected {bad:?} to be useless");
164 }
165 }
166
167 #[test]
168 fn test_useful_selectors() {
169 for good in [
170 "#id",
171 "button.btn",
172 "input[name=email]",
173 ":is(a, button)",
174 "a > span.x",
175 "div[aria-label='New chat']",
176 ] {
177 assert!(!selector_is_useless(good), "expected {good:?} to be useful");
178 }
179 }
180
181 #[test]
182 fn test_validate_balanced() {
183 assert!(validate_selector("div[aria-label='x']:nth-child(2)").is_ok());
184 assert!(validate_selector("a:has(> span)").is_ok());
185 }
186
187 #[test]
188 fn test_validate_unbalanced() {
189 assert!(validate_selector("div[aria-label").is_err());
190 assert!(validate_selector("div:nth-child(2").is_err());
191 assert!(validate_selector("div:not(").is_err());
192 assert!(validate_selector("div[aria-label='x]").is_err());
193 }
194}