use presentar_core::Widget;
#[derive(Debug, Clone, PartialEq)]
pub enum Selector {
Type(String),
Id(String),
TestId(String),
Class(String),
Attribute { name: String, value: String },
Descendant(Box<Selector>, Box<Selector>),
Child(Box<Selector>, Box<Selector>),
}
impl Selector {
pub fn parse(input: &str) -> Result<Self, SelectorError> {
SelectorParser::new(input).parse()
}
#[must_use]
pub fn matches(&self, widget: &dyn Widget) -> bool {
match self {
Self::Type(name) => {
name.is_empty()
}
Self::Id(_id) => {
false
}
Self::TestId(id) => Widget::test_id(widget) == Some(id.as_str()),
Self::Class(_class) => {
false
}
Self::Attribute { name, value } => {
if name == "data-testid" {
Widget::test_id(widget) == Some(value.as_str())
} else if name == "aria-label" {
widget.accessible_name() == Some(value.as_str())
} else {
false
}
}
Self::Descendant(_, _) | Self::Child(_, _) => {
false
}
}
}
}
pub struct SelectorParser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> SelectorParser<'a> {
#[must_use]
pub const fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
pub fn parse(&mut self) -> Result<Selector, SelectorError> {
self.skip_whitespace();
if self.input.is_empty() {
return Err(SelectorError::Empty);
}
self.parse_selector()
}
fn parse_selector(&mut self) -> Result<Selector, SelectorError> {
let first = self.peek_char().ok_or(SelectorError::Empty)?;
match first {
'#' => self.parse_id(),
'.' => self.parse_class(),
'[' => self.parse_attribute(),
_ if first.is_alphabetic() => self.parse_type(),
_ => Err(SelectorError::UnexpectedChar(first)),
}
}
fn parse_id(&mut self) -> Result<Selector, SelectorError> {
self.advance(); let id = self.read_identifier()?;
Ok(Selector::Id(id))
}
fn parse_class(&mut self) -> Result<Selector, SelectorError> {
self.advance(); let class = self.read_identifier()?;
Ok(Selector::Class(class))
}
fn parse_type(&mut self) -> Result<Selector, SelectorError> {
let name = self.read_identifier()?;
Ok(Selector::Type(name))
}
fn parse_attribute(&mut self) -> Result<Selector, SelectorError> {
self.advance();
let name = self.read_until('=');
if name.is_empty() {
return Err(SelectorError::InvalidAttribute);
}
self.advance();
let quote = self.peek_char();
if quote == Some('\'') || quote == Some('"') {
self.advance();
}
let value = self.read_until_any(&['\'', '"', ']']);
if self.peek_char() == Some('\'') || self.peek_char() == Some('"') {
self.advance();
}
if self.peek_char() != Some(']') {
return Err(SelectorError::UnclosedAttribute);
}
self.advance();
if name == "data-testid" {
Ok(Selector::TestId(value))
} else {
Ok(Selector::Attribute { name, value })
}
}
fn read_identifier(&mut self) -> Result<String, SelectorError> {
let start = self.pos;
while let Some(c) = self.peek_char() {
if c.is_alphanumeric() || c == '-' || c == '_' {
self.advance();
} else {
break;
}
}
if self.pos == start {
return Err(SelectorError::ExpectedIdentifier);
}
Ok(self.input[start..self.pos].to_string())
}
fn read_until(&mut self, stop: char) -> String {
let start = self.pos;
while let Some(c) = self.peek_char() {
if c == stop {
break;
}
self.advance();
}
self.input[start..self.pos].to_string()
}
fn read_until_any(&mut self, stops: &[char]) -> String {
let start = self.pos;
while let Some(c) = self.peek_char() {
if stops.contains(&c) {
break;
}
self.advance();
}
self.input[start..self.pos].to_string()
}
fn skip_whitespace(&mut self) {
while let Some(c) = self.peek_char() {
if c.is_whitespace() {
self.advance();
} else {
break;
}
}
}
fn peek_char(&self) -> Option<char> {
self.input[self.pos..].chars().next()
}
fn advance(&mut self) {
if let Some(c) = self.peek_char() {
self.pos += c.len_utf8();
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectorError {
Empty,
UnexpectedChar(char),
ExpectedIdentifier,
InvalidAttribute,
UnclosedAttribute,
}
impl std::fmt::Display for SelectorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => write!(f, "empty selector"),
Self::UnexpectedChar(c) => write!(f, "unexpected character: '{c}'"),
Self::ExpectedIdentifier => write!(f, "expected identifier"),
Self::InvalidAttribute => write!(f, "invalid attribute syntax"),
Self::UnclosedAttribute => write!(f, "unclosed attribute bracket"),
}
}
}
impl std::error::Error for SelectorError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_type() {
let sel = Selector::parse("Button").unwrap();
assert_eq!(sel, Selector::Type("Button".to_string()));
}
#[test]
fn test_parse_id() {
let sel = Selector::parse("#submit-btn").unwrap();
assert_eq!(sel, Selector::Id("submit-btn".to_string()));
}
#[test]
fn test_parse_class() {
let sel = Selector::parse(".primary").unwrap();
assert_eq!(sel, Selector::Class("primary".to_string()));
}
#[test]
fn test_parse_test_id() {
let sel = Selector::parse("[data-testid='login']").unwrap();
assert_eq!(sel, Selector::TestId("login".to_string()));
}
#[test]
fn test_parse_test_id_double_quotes() {
let sel = Selector::parse("[data-testid=\"login\"]").unwrap();
assert_eq!(sel, Selector::TestId("login".to_string()));
}
#[test]
fn test_parse_attribute() {
let sel = Selector::parse("[aria-label='Close']").unwrap();
assert_eq!(
sel,
Selector::Attribute {
name: "aria-label".to_string(),
value: "Close".to_string(),
}
);
}
#[test]
fn test_parse_empty_error() {
let result = Selector::parse("");
assert_eq!(result, Err(SelectorError::Empty));
}
#[test]
fn test_parse_whitespace() {
let sel = Selector::parse(" Button ").unwrap();
assert_eq!(sel, Selector::Type("Button".to_string()));
}
#[test]
fn test_selector_error_display() {
assert_eq!(SelectorError::Empty.to_string(), "empty selector");
assert_eq!(
SelectorError::UnexpectedChar('@').to_string(),
"unexpected character: '@'"
);
}
#[test]
fn test_selector_error_display_all_variants() {
assert_eq!(SelectorError::Empty.to_string(), "empty selector");
assert_eq!(
SelectorError::ExpectedIdentifier.to_string(),
"expected identifier"
);
assert_eq!(
SelectorError::InvalidAttribute.to_string(),
"invalid attribute syntax"
);
assert_eq!(
SelectorError::UnclosedAttribute.to_string(),
"unclosed attribute bracket"
);
}
#[test]
fn test_parse_type_with_hyphen() {
let sel = Selector::parse("data-table").unwrap();
assert_eq!(sel, Selector::Type("data-table".to_string()));
}
#[test]
fn test_parse_type_with_underscore() {
let sel = Selector::parse("my_widget").unwrap();
assert_eq!(sel, Selector::Type("my_widget".to_string()));
}
#[test]
fn test_parse_type_with_numbers() {
let sel = Selector::parse("Button2").unwrap();
assert_eq!(sel, Selector::Type("Button2".to_string()));
}
#[test]
fn test_parse_type_case_sensitive() {
let sel1 = Selector::parse("Button").unwrap();
let sel2 = Selector::parse("button").unwrap();
assert_ne!(sel1, sel2);
}
#[test]
fn test_parse_id_with_numbers() {
let sel = Selector::parse("#item-123").unwrap();
assert_eq!(sel, Selector::Id("item-123".to_string()));
}
#[test]
fn test_parse_id_with_underscores() {
let sel = Selector::parse("#my_element").unwrap();
assert_eq!(sel, Selector::Id("my_element".to_string()));
}
#[test]
fn test_parse_id_simple() {
let sel = Selector::parse("#main").unwrap();
assert_eq!(sel, Selector::Id("main".to_string()));
}
#[test]
fn test_parse_class_with_numbers() {
let sel = Selector::parse(".col-12").unwrap();
assert_eq!(sel, Selector::Class("col-12".to_string()));
}
#[test]
fn test_parse_class_with_underscores() {
let sel = Selector::parse(".btn_primary").unwrap();
assert_eq!(sel, Selector::Class("btn_primary".to_string()));
}
#[test]
fn test_parse_attribute_role() {
let sel = Selector::parse("[role='button']").unwrap();
assert_eq!(
sel,
Selector::Attribute {
name: "role".to_string(),
value: "button".to_string(),
}
);
}
#[test]
fn test_parse_attribute_disabled() {
let sel = Selector::parse("[disabled='true']").unwrap();
assert_eq!(
sel,
Selector::Attribute {
name: "disabled".to_string(),
value: "true".to_string(),
}
);
}
#[test]
fn test_parse_attribute_with_spaces_in_value() {
let sel = Selector::parse("[aria-label='Click to submit']").unwrap();
assert_eq!(
sel,
Selector::Attribute {
name: "aria-label".to_string(),
value: "Click to submit".to_string(),
}
);
}
#[test]
fn test_parse_testid_variations() {
let sel1 = Selector::parse("[data-testid='foo']").unwrap();
assert_eq!(sel1, Selector::TestId("foo".to_string()));
let sel2 = Selector::parse("[data-testid=\"bar\"]").unwrap();
assert_eq!(sel2, Selector::TestId("bar".to_string()));
}
#[test]
fn test_parse_unexpected_char() {
let result = Selector::parse("@invalid");
assert_eq!(result, Err(SelectorError::UnexpectedChar('@')));
}
#[test]
fn test_parse_unexpected_char_special() {
assert!(Selector::parse("!invalid").is_err());
assert!(Selector::parse("$invalid").is_err());
assert!(Selector::parse("*invalid").is_err());
}
#[test]
fn test_parse_empty_id() {
let result = Selector::parse("#");
assert_eq!(result, Err(SelectorError::ExpectedIdentifier));
}
#[test]
fn test_parse_empty_class() {
let result = Selector::parse(".");
assert_eq!(result, Err(SelectorError::ExpectedIdentifier));
}
#[test]
fn test_parse_unclosed_attribute() {
let result = Selector::parse("[data-testid='foo'");
assert_eq!(result, Err(SelectorError::UnclosedAttribute));
}
#[test]
fn test_parse_invalid_attribute_no_equals() {
let result = Selector::parse("[disabled]");
assert!(result.is_err());
}
#[test]
fn test_parse_leading_whitespace() {
let sel = Selector::parse(" #main").unwrap();
assert_eq!(sel, Selector::Id("main".to_string()));
}
#[test]
fn test_parse_trailing_whitespace() {
let sel = Selector::parse(".button ").unwrap();
assert_eq!(sel, Selector::Class("button".to_string()));
}
#[test]
fn test_parse_only_whitespace() {
let result = Selector::parse(" ");
assert_eq!(result, Err(SelectorError::Empty));
}
#[test]
fn test_selector_equality() {
let sel1 = Selector::parse("#main").unwrap();
let sel2 = Selector::parse("#main").unwrap();
assert_eq!(sel1, sel2);
}
#[test]
fn test_selector_inequality_different_types() {
let id = Selector::parse("#main").unwrap();
let class = Selector::parse(".main").unwrap();
assert_ne!(id, class);
}
#[test]
fn test_selector_inequality_different_values() {
let sel1 = Selector::parse("#main").unwrap();
let sel2 = Selector::parse("#header").unwrap();
assert_ne!(sel1, sel2);
}
#[test]
fn test_selector_clone() {
let sel = Selector::parse("[data-testid='foo']").unwrap();
let cloned = sel.clone();
assert_eq!(sel, cloned);
}
#[test]
fn test_parser_new() {
let parser = SelectorParser::new("Button");
assert_eq!(parser.input, "Button");
assert_eq!(parser.pos, 0);
}
#[test]
fn test_selector_descendant_structure() {
let parent = Box::new(Selector::Type("Container".to_string()));
let child = Box::new(Selector::Type("Button".to_string()));
let desc = Selector::Descendant(parent, child);
matches!(desc, Selector::Descendant(_, _));
}
#[test]
fn test_selector_child_structure() {
let parent = Box::new(Selector::Type("Row".to_string()));
let child = Box::new(Selector::Type("Column".to_string()));
let sel = Selector::Child(parent, child);
matches!(sel, Selector::Child(_, _));
}
#[test]
fn test_selector_debug_format() {
let sel = Selector::parse("#main").unwrap();
let debug = format!("{:?}", sel);
assert!(debug.contains("Id"));
assert!(debug.contains("main"));
}
#[test]
fn test_selector_error_debug_format() {
let err = SelectorError::Empty;
let debug = format!("{:?}", err);
assert!(debug.contains("Empty"));
}
#[test]
fn test_parse_unicode_in_attribute_value() {
let sel = Selector::parse("[aria-label='日本語']").unwrap();
assert_eq!(
sel,
Selector::Attribute {
name: "aria-label".to_string(),
value: "日本語".to_string(),
}
);
}
#[test]
fn test_parse_emoji_in_attribute_value() {
let sel = Selector::parse("[aria-label='Hello 👋']").unwrap();
assert_eq!(
sel,
Selector::Attribute {
name: "aria-label".to_string(),
value: "Hello 👋".to_string(),
}
);
}
}