use regex::Regex;
use std::sync::LazyLock;
static TEXT_SELECTOR_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"^:text\(["'](.+?)["']\)$"#).expect("Invalid regex pattern"));
static NTH_OF_TYPE_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"(.+?):nth-of-type\((\d+)\)"#).expect("Invalid regex pattern"));
#[derive(Debug, Clone, PartialEq)]
pub enum ParsedSelector {
Css(String),
Text {
text: String,
element: Option<String>,
},
XPath(String),
}
pub fn is_text_selector(selector: &str) -> bool {
TEXT_SELECTOR_PATTERN.is_match(selector)
}
pub fn extract_text_from_selector(selector: &str) -> Option<String> {
TEXT_SELECTOR_PATTERN
.captures(selector)
.map(|caps| caps[1].to_string())
}
pub fn normalize_selector(selector: &str) -> ParsedSelector {
let trimmed = selector.trim();
if let Some(text) = extract_text_from_selector(trimmed) {
return ParsedSelector::Text {
text,
element: None,
};
}
if trimmed.starts_with("//") || trimmed.starts_with("(//") {
return ParsedSelector::XPath(trimmed.to_string());
}
ParsedSelector::Css(trimmed.to_string())
}
pub fn build_text_selector(text: &str, element_type: Option<&str>) -> String {
match element_type {
Some(el) => format!("//{}[contains(text(), '{}')]", el, text),
None => format!("//*[contains(text(), '{}')]", text),
}
}
pub fn has_nth_of_type(selector: &str) -> bool {
NTH_OF_TYPE_PATTERN.is_match(selector)
}
pub fn parse_nth_of_type(selector: &str) -> Option<(String, usize)> {
NTH_OF_TYPE_PATTERN.captures(selector).and_then(|caps| {
let base = caps[1].to_string();
let index = caps[2].parse::<usize>().ok()?;
Some((base, index))
})
}
pub fn escape_selector_value(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\'', "\\'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_text_selector_true_for_text_selectors() {
assert!(is_text_selector(":text(\"Submit\")"));
assert!(is_text_selector(":text('Submit')"));
assert!(is_text_selector(":text(\"Click me\")"));
}
#[test]
fn is_text_selector_false_for_css_selectors() {
assert!(!is_text_selector("button"));
assert!(!is_text_selector(".class"));
assert!(!is_text_selector("#id"));
assert!(!is_text_selector("[data-test]"));
}
#[test]
fn extract_text_from_selector_extracts_text() {
assert_eq!(
extract_text_from_selector(":text(\"Submit\")"),
Some("Submit".to_string())
);
assert_eq!(
extract_text_from_selector(":text('Click me')"),
Some("Click me".to_string())
);
}
#[test]
fn extract_text_from_selector_returns_none_for_css() {
assert_eq!(extract_text_from_selector("button"), None);
assert_eq!(extract_text_from_selector(".class"), None);
}
#[test]
fn normalize_selector_handles_css() {
assert_eq!(
normalize_selector("button"),
ParsedSelector::Css("button".to_string())
);
assert_eq!(
normalize_selector(" .class "),
ParsedSelector::Css(".class".to_string())
);
}
#[test]
fn normalize_selector_handles_text() {
assert_eq!(
normalize_selector(":text(\"Submit\")"),
ParsedSelector::Text {
text: "Submit".to_string(),
element: None
}
);
}
#[test]
fn normalize_selector_handles_xpath() {
assert_eq!(
normalize_selector("//button"),
ParsedSelector::XPath("//button".to_string())
);
assert_eq!(
normalize_selector("(//div)[1]"),
ParsedSelector::XPath("(//div)[1]".to_string())
);
}
#[test]
fn build_text_selector_without_element() {
let selector = build_text_selector("Submit", None);
assert!(selector.contains("Submit"));
assert!(selector.contains("contains(text()"));
}
#[test]
fn build_text_selector_with_element() {
let selector = build_text_selector("Submit", Some("button"));
assert!(selector.contains("button"));
assert!(selector.contains("Submit"));
}
#[test]
fn has_nth_of_type_detects_pattern() {
assert!(has_nth_of_type("button:nth-of-type(1)"));
assert!(has_nth_of_type("div.class:nth-of-type(2)"));
assert!(!has_nth_of_type("button"));
assert!(!has_nth_of_type("button:first-child"));
}
#[test]
fn parse_nth_of_type_extracts_parts() {
assert_eq!(
parse_nth_of_type("button:nth-of-type(1)"),
Some(("button".to_string(), 1))
);
assert_eq!(
parse_nth_of_type("div.class:nth-of-type(3)"),
Some(("div.class".to_string(), 3))
);
assert_eq!(parse_nth_of_type("button"), None);
}
#[test]
fn escape_selector_value_escapes_special_chars() {
assert_eq!(escape_selector_value("test"), "test");
assert_eq!(escape_selector_value("test\"value"), "test\\\"value");
assert_eq!(escape_selector_value("test'value"), "test\\'value");
assert_eq!(escape_selector_value("test\\value"), "test\\\\value");
}
}