browser_commander/elements/
selectors.rs1use regex::Regex;
7use std::sync::LazyLock;
8
9static TEXT_SELECTOR_PATTERN: LazyLock<Regex> =
11 LazyLock::new(|| Regex::new(r#"^:text\(["'](.+?)["']\)$"#).expect("Invalid regex pattern"));
12
13static NTH_OF_TYPE_PATTERN: LazyLock<Regex> =
15 LazyLock::new(|| Regex::new(r#"(.+?):nth-of-type\((\d+)\)"#).expect("Invalid regex pattern"));
16
17#[derive(Debug, Clone, PartialEq)]
19pub enum ParsedSelector {
20 Css(String),
22 Text {
24 text: String,
25 element: Option<String>,
26 },
27 XPath(String),
29}
30
31pub fn is_text_selector(selector: &str) -> bool {
43 TEXT_SELECTOR_PATTERN.is_match(selector)
44}
45
46pub fn extract_text_from_selector(selector: &str) -> Option<String> {
56 TEXT_SELECTOR_PATTERN
57 .captures(selector)
58 .map(|caps| caps[1].to_string())
59}
60
61pub fn normalize_selector(selector: &str) -> ParsedSelector {
74 let trimmed = selector.trim();
75
76 if let Some(text) = extract_text_from_selector(trimmed) {
78 return ParsedSelector::Text {
79 text,
80 element: None,
81 };
82 }
83
84 if trimmed.starts_with("//") || trimmed.starts_with("(//") {
86 return ParsedSelector::XPath(trimmed.to_string());
87 }
88
89 ParsedSelector::Css(trimmed.to_string())
91}
92
93pub fn build_text_selector(text: &str, element_type: Option<&str>) -> String {
107 match element_type {
109 Some(el) => format!("//{}[contains(text(), '{}')]", el, text),
110 None => format!("//*[contains(text(), '{}')]", text),
111 }
112}
113
114pub fn has_nth_of_type(selector: &str) -> bool {
124 NTH_OF_TYPE_PATTERN.is_match(selector)
125}
126
127pub fn parse_nth_of_type(selector: &str) -> Option<(String, usize)> {
137 NTH_OF_TYPE_PATTERN.captures(selector).and_then(|caps| {
138 let base = caps[1].to_string();
139 let index = caps[2].parse::<usize>().ok()?;
140 Some((base, index))
141 })
142}
143
144pub fn escape_selector_value(value: &str) -> String {
154 value
155 .replace('\\', "\\\\")
156 .replace('"', "\\\"")
157 .replace('\'', "\\'")
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn is_text_selector_true_for_text_selectors() {
166 assert!(is_text_selector(":text(\"Submit\")"));
167 assert!(is_text_selector(":text('Submit')"));
168 assert!(is_text_selector(":text(\"Click me\")"));
169 }
170
171 #[test]
172 fn is_text_selector_false_for_css_selectors() {
173 assert!(!is_text_selector("button"));
174 assert!(!is_text_selector(".class"));
175 assert!(!is_text_selector("#id"));
176 assert!(!is_text_selector("[data-test]"));
177 }
178
179 #[test]
180 fn extract_text_from_selector_extracts_text() {
181 assert_eq!(
182 extract_text_from_selector(":text(\"Submit\")"),
183 Some("Submit".to_string())
184 );
185 assert_eq!(
186 extract_text_from_selector(":text('Click me')"),
187 Some("Click me".to_string())
188 );
189 }
190
191 #[test]
192 fn extract_text_from_selector_returns_none_for_css() {
193 assert_eq!(extract_text_from_selector("button"), None);
194 assert_eq!(extract_text_from_selector(".class"), None);
195 }
196
197 #[test]
198 fn normalize_selector_handles_css() {
199 assert_eq!(
200 normalize_selector("button"),
201 ParsedSelector::Css("button".to_string())
202 );
203 assert_eq!(
204 normalize_selector(" .class "),
205 ParsedSelector::Css(".class".to_string())
206 );
207 }
208
209 #[test]
210 fn normalize_selector_handles_text() {
211 assert_eq!(
212 normalize_selector(":text(\"Submit\")"),
213 ParsedSelector::Text {
214 text: "Submit".to_string(),
215 element: None
216 }
217 );
218 }
219
220 #[test]
221 fn normalize_selector_handles_xpath() {
222 assert_eq!(
223 normalize_selector("//button"),
224 ParsedSelector::XPath("//button".to_string())
225 );
226 assert_eq!(
227 normalize_selector("(//div)[1]"),
228 ParsedSelector::XPath("(//div)[1]".to_string())
229 );
230 }
231
232 #[test]
233 fn build_text_selector_without_element() {
234 let selector = build_text_selector("Submit", None);
235 assert!(selector.contains("Submit"));
236 assert!(selector.contains("contains(text()"));
237 }
238
239 #[test]
240 fn build_text_selector_with_element() {
241 let selector = build_text_selector("Submit", Some("button"));
242 assert!(selector.contains("button"));
243 assert!(selector.contains("Submit"));
244 }
245
246 #[test]
247 fn has_nth_of_type_detects_pattern() {
248 assert!(has_nth_of_type("button:nth-of-type(1)"));
249 assert!(has_nth_of_type("div.class:nth-of-type(2)"));
250 assert!(!has_nth_of_type("button"));
251 assert!(!has_nth_of_type("button:first-child"));
252 }
253
254 #[test]
255 fn parse_nth_of_type_extracts_parts() {
256 assert_eq!(
257 parse_nth_of_type("button:nth-of-type(1)"),
258 Some(("button".to_string(), 1))
259 );
260 assert_eq!(
261 parse_nth_of_type("div.class:nth-of-type(3)"),
262 Some(("div.class".to_string(), 3))
263 );
264 assert_eq!(parse_nth_of_type("button"), None);
265 }
266
267 #[test]
268 fn escape_selector_value_escapes_special_chars() {
269 assert_eq!(escape_selector_value("test"), "test");
270 assert_eq!(escape_selector_value("test\"value"), "test\\\"value");
271 assert_eq!(escape_selector_value("test'value"), "test\\'value");
272 assert_eq!(escape_selector_value("test\\value"), "test\\\\value");
273 }
274}