presentar_test/
selector.rs

1//! CSS-like selector parsing for widget queries.
2//!
3//! Supports:
4//! - `"Button"` - by widget type
5//! - `"#submit-btn"` - by ID
6//! - `"[data-testid='login']"` - by test ID
7
8use presentar_core::Widget;
9
10/// Parsed selector.
11#[derive(Debug, Clone, PartialEq)]
12pub enum Selector {
13    /// Match by widget type name
14    Type(String),
15    /// Match by ID (e.g., `#my-id`)
16    Id(String),
17    /// Match by test ID (e.g., `[data-testid='foo']`)
18    TestId(String),
19    /// Match by class (e.g., `.my-class`)
20    Class(String),
21    /// Match by attribute (e.g., `[aria-label='foo']`)
22    Attribute { name: String, value: String },
23    /// Descendant combinator (e.g., `Container Button`)
24    Descendant(Box<Selector>, Box<Selector>),
25    /// Child combinator (e.g., `Row > Button`)
26    Child(Box<Selector>, Box<Selector>),
27}
28
29impl Selector {
30    /// Parse a selector string.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if the selector is invalid.
35    pub fn parse(input: &str) -> Result<Self, SelectorError> {
36        SelectorParser::new(input).parse()
37    }
38
39    /// Check if this selector matches a widget.
40    #[must_use]
41    pub fn matches(&self, widget: &dyn Widget) -> bool {
42        match self {
43            Self::Type(name) => {
44                // Simplified type matching - would compare actual TypeId
45                // For now, always false since we can't easily get type names
46                name.is_empty()
47            }
48            Self::Id(_id) => {
49                // Would need widget.id() method
50                false
51            }
52            Self::TestId(id) => Widget::test_id(widget) == Some(id.as_str()),
53            Self::Class(_class) => {
54                // Would need widget.classes() method
55                false
56            }
57            Self::Attribute { name, value } => {
58                if name == "data-testid" {
59                    Widget::test_id(widget) == Some(value.as_str())
60                } else if name == "aria-label" {
61                    widget.accessible_name() == Some(value.as_str())
62                } else {
63                    false
64                }
65            }
66            Self::Descendant(_, _) | Self::Child(_, _) => {
67                // Would need parent context
68                false
69            }
70        }
71    }
72}
73
74/// Selector parser.
75pub struct SelectorParser<'a> {
76    input: &'a str,
77    pos: usize,
78}
79
80impl<'a> SelectorParser<'a> {
81    /// Create a new parser.
82    #[must_use]
83    pub const fn new(input: &'a str) -> Self {
84        Self { input, pos: 0 }
85    }
86
87    /// Parse the selector.
88    pub fn parse(&mut self) -> Result<Selector, SelectorError> {
89        self.skip_whitespace();
90
91        if self.input.is_empty() {
92            return Err(SelectorError::Empty);
93        }
94
95        self.parse_selector()
96    }
97
98    fn parse_selector(&mut self) -> Result<Selector, SelectorError> {
99        let first = self.peek_char().ok_or(SelectorError::Empty)?;
100
101        match first {
102            '#' => self.parse_id(),
103            '.' => self.parse_class(),
104            '[' => self.parse_attribute(),
105            _ if first.is_alphabetic() => self.parse_type(),
106            _ => Err(SelectorError::UnexpectedChar(first)),
107        }
108    }
109
110    fn parse_id(&mut self) -> Result<Selector, SelectorError> {
111        self.advance(); // Skip '#'
112        let id = self.read_identifier()?;
113        Ok(Selector::Id(id))
114    }
115
116    fn parse_class(&mut self) -> Result<Selector, SelectorError> {
117        self.advance(); // Skip '.'
118        let class = self.read_identifier()?;
119        Ok(Selector::Class(class))
120    }
121
122    fn parse_type(&mut self) -> Result<Selector, SelectorError> {
123        let name = self.read_identifier()?;
124        Ok(Selector::Type(name))
125    }
126
127    fn parse_attribute(&mut self) -> Result<Selector, SelectorError> {
128        self.advance(); // Skip '['
129
130        let name = self.read_until('=');
131        if name.is_empty() {
132            return Err(SelectorError::InvalidAttribute);
133        }
134
135        self.advance(); // Skip '='
136
137        // Skip optional quote
138        let quote = self.peek_char();
139        if quote == Some('\'') || quote == Some('"') {
140            self.advance();
141        }
142
143        let value = self.read_until_any(&['\'', '"', ']']);
144
145        // Skip closing quote if present
146        if self.peek_char() == Some('\'') || self.peek_char() == Some('"') {
147            self.advance();
148        }
149
150        // Skip ']'
151        if self.peek_char() != Some(']') {
152            return Err(SelectorError::UnclosedAttribute);
153        }
154        self.advance();
155
156        // Special case for data-testid
157        if name == "data-testid" {
158            Ok(Selector::TestId(value))
159        } else {
160            Ok(Selector::Attribute { name, value })
161        }
162    }
163
164    fn read_identifier(&mut self) -> Result<String, SelectorError> {
165        let start = self.pos;
166        while let Some(c) = self.peek_char() {
167            if c.is_alphanumeric() || c == '-' || c == '_' {
168                self.advance();
169            } else {
170                break;
171            }
172        }
173
174        if self.pos == start {
175            return Err(SelectorError::ExpectedIdentifier);
176        }
177
178        Ok(self.input[start..self.pos].to_string())
179    }
180
181    fn read_until(&mut self, stop: char) -> String {
182        let start = self.pos;
183        while let Some(c) = self.peek_char() {
184            if c == stop {
185                break;
186            }
187            self.advance();
188        }
189        self.input[start..self.pos].to_string()
190    }
191
192    fn read_until_any(&mut self, stops: &[char]) -> String {
193        let start = self.pos;
194        while let Some(c) = self.peek_char() {
195            if stops.contains(&c) {
196                break;
197            }
198            self.advance();
199        }
200        self.input[start..self.pos].to_string()
201    }
202
203    fn skip_whitespace(&mut self) {
204        while let Some(c) = self.peek_char() {
205            if c.is_whitespace() {
206                self.advance();
207            } else {
208                break;
209            }
210        }
211    }
212
213    fn peek_char(&self) -> Option<char> {
214        self.input[self.pos..].chars().next()
215    }
216
217    fn advance(&mut self) {
218        if let Some(c) = self.peek_char() {
219            self.pos += c.len_utf8();
220        }
221    }
222}
223
224/// Selector parsing error.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum SelectorError {
227    /// Empty selector
228    Empty,
229    /// Unexpected character
230    UnexpectedChar(char),
231    /// Expected identifier
232    ExpectedIdentifier,
233    /// Invalid attribute syntax
234    InvalidAttribute,
235    /// Unclosed attribute bracket
236    UnclosedAttribute,
237}
238
239impl std::fmt::Display for SelectorError {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        match self {
242            Self::Empty => write!(f, "empty selector"),
243            Self::UnexpectedChar(c) => write!(f, "unexpected character: '{c}'"),
244            Self::ExpectedIdentifier => write!(f, "expected identifier"),
245            Self::InvalidAttribute => write!(f, "invalid attribute syntax"),
246            Self::UnclosedAttribute => write!(f, "unclosed attribute bracket"),
247        }
248    }
249}
250
251impl std::error::Error for SelectorError {}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_parse_type() {
259        let sel = Selector::parse("Button").unwrap();
260        assert_eq!(sel, Selector::Type("Button".to_string()));
261    }
262
263    #[test]
264    fn test_parse_id() {
265        let sel = Selector::parse("#submit-btn").unwrap();
266        assert_eq!(sel, Selector::Id("submit-btn".to_string()));
267    }
268
269    #[test]
270    fn test_parse_class() {
271        let sel = Selector::parse(".primary").unwrap();
272        assert_eq!(sel, Selector::Class("primary".to_string()));
273    }
274
275    #[test]
276    fn test_parse_test_id() {
277        let sel = Selector::parse("[data-testid='login']").unwrap();
278        assert_eq!(sel, Selector::TestId("login".to_string()));
279    }
280
281    #[test]
282    fn test_parse_test_id_double_quotes() {
283        let sel = Selector::parse("[data-testid=\"login\"]").unwrap();
284        assert_eq!(sel, Selector::TestId("login".to_string()));
285    }
286
287    #[test]
288    fn test_parse_attribute() {
289        let sel = Selector::parse("[aria-label='Close']").unwrap();
290        assert_eq!(
291            sel,
292            Selector::Attribute {
293                name: "aria-label".to_string(),
294                value: "Close".to_string(),
295            }
296        );
297    }
298
299    #[test]
300    fn test_parse_empty_error() {
301        let result = Selector::parse("");
302        assert_eq!(result, Err(SelectorError::Empty));
303    }
304
305    #[test]
306    fn test_parse_whitespace() {
307        let sel = Selector::parse("  Button  ").unwrap();
308        assert_eq!(sel, Selector::Type("Button".to_string()));
309    }
310
311    #[test]
312    fn test_selector_error_display() {
313        assert_eq!(SelectorError::Empty.to_string(), "empty selector");
314        assert_eq!(
315            SelectorError::UnexpectedChar('@').to_string(),
316            "unexpected character: '@'"
317        );
318    }
319
320    // =========================================================================
321    // Additional Selector Error Display Tests
322    // =========================================================================
323
324    #[test]
325    fn test_selector_error_display_all_variants() {
326        assert_eq!(SelectorError::Empty.to_string(), "empty selector");
327        assert_eq!(
328            SelectorError::ExpectedIdentifier.to_string(),
329            "expected identifier"
330        );
331        assert_eq!(
332            SelectorError::InvalidAttribute.to_string(),
333            "invalid attribute syntax"
334        );
335        assert_eq!(
336            SelectorError::UnclosedAttribute.to_string(),
337            "unclosed attribute bracket"
338        );
339    }
340
341    // =========================================================================
342    // Type Selector Tests
343    // =========================================================================
344
345    #[test]
346    fn test_parse_type_with_hyphen() {
347        let sel = Selector::parse("data-table").unwrap();
348        assert_eq!(sel, Selector::Type("data-table".to_string()));
349    }
350
351    #[test]
352    fn test_parse_type_with_underscore() {
353        let sel = Selector::parse("my_widget").unwrap();
354        assert_eq!(sel, Selector::Type("my_widget".to_string()));
355    }
356
357    #[test]
358    fn test_parse_type_with_numbers() {
359        let sel = Selector::parse("Button2").unwrap();
360        assert_eq!(sel, Selector::Type("Button2".to_string()));
361    }
362
363    #[test]
364    fn test_parse_type_case_sensitive() {
365        let sel1 = Selector::parse("Button").unwrap();
366        let sel2 = Selector::parse("button").unwrap();
367        assert_ne!(sel1, sel2);
368    }
369
370    // =========================================================================
371    // ID Selector Tests
372    // =========================================================================
373
374    #[test]
375    fn test_parse_id_with_numbers() {
376        let sel = Selector::parse("#item-123").unwrap();
377        assert_eq!(sel, Selector::Id("item-123".to_string()));
378    }
379
380    #[test]
381    fn test_parse_id_with_underscores() {
382        let sel = Selector::parse("#my_element").unwrap();
383        assert_eq!(sel, Selector::Id("my_element".to_string()));
384    }
385
386    #[test]
387    fn test_parse_id_simple() {
388        let sel = Selector::parse("#main").unwrap();
389        assert_eq!(sel, Selector::Id("main".to_string()));
390    }
391
392    // =========================================================================
393    // Class Selector Tests
394    // =========================================================================
395
396    #[test]
397    fn test_parse_class_with_numbers() {
398        let sel = Selector::parse(".col-12").unwrap();
399        assert_eq!(sel, Selector::Class("col-12".to_string()));
400    }
401
402    #[test]
403    fn test_parse_class_with_underscores() {
404        let sel = Selector::parse(".btn_primary").unwrap();
405        assert_eq!(sel, Selector::Class("btn_primary".to_string()));
406    }
407
408    // =========================================================================
409    // Attribute Selector Tests
410    // =========================================================================
411
412    #[test]
413    fn test_parse_attribute_role() {
414        let sel = Selector::parse("[role='button']").unwrap();
415        assert_eq!(
416            sel,
417            Selector::Attribute {
418                name: "role".to_string(),
419                value: "button".to_string(),
420            }
421        );
422    }
423
424    #[test]
425    fn test_parse_attribute_disabled() {
426        let sel = Selector::parse("[disabled='true']").unwrap();
427        assert_eq!(
428            sel,
429            Selector::Attribute {
430                name: "disabled".to_string(),
431                value: "true".to_string(),
432            }
433        );
434    }
435
436    #[test]
437    fn test_parse_attribute_with_spaces_in_value() {
438        let sel = Selector::parse("[aria-label='Click to submit']").unwrap();
439        assert_eq!(
440            sel,
441            Selector::Attribute {
442                name: "aria-label".to_string(),
443                value: "Click to submit".to_string(),
444            }
445        );
446    }
447
448    #[test]
449    fn test_parse_testid_variations() {
450        // Single quotes
451        let sel1 = Selector::parse("[data-testid='foo']").unwrap();
452        assert_eq!(sel1, Selector::TestId("foo".to_string()));
453
454        // Double quotes
455        let sel2 = Selector::parse("[data-testid=\"bar\"]").unwrap();
456        assert_eq!(sel2, Selector::TestId("bar".to_string()));
457    }
458
459    // =========================================================================
460    // Error Cases
461    // =========================================================================
462
463    #[test]
464    fn test_parse_unexpected_char() {
465        let result = Selector::parse("@invalid");
466        assert_eq!(result, Err(SelectorError::UnexpectedChar('@')));
467    }
468
469    #[test]
470    fn test_parse_unexpected_char_special() {
471        assert!(Selector::parse("!invalid").is_err());
472        assert!(Selector::parse("$invalid").is_err());
473        assert!(Selector::parse("*invalid").is_err());
474    }
475
476    #[test]
477    fn test_parse_empty_id() {
478        let result = Selector::parse("#");
479        assert_eq!(result, Err(SelectorError::ExpectedIdentifier));
480    }
481
482    #[test]
483    fn test_parse_empty_class() {
484        let result = Selector::parse(".");
485        assert_eq!(result, Err(SelectorError::ExpectedIdentifier));
486    }
487
488    #[test]
489    fn test_parse_unclosed_attribute() {
490        let result = Selector::parse("[data-testid='foo'");
491        assert_eq!(result, Err(SelectorError::UnclosedAttribute));
492    }
493
494    #[test]
495    fn test_parse_invalid_attribute_no_equals() {
496        let result = Selector::parse("[disabled]");
497        // This parses the name but then expects '=' - currently reads until '=' which is empty
498        assert!(result.is_err());
499    }
500
501    // =========================================================================
502    // Whitespace Handling Tests
503    // =========================================================================
504
505    #[test]
506    fn test_parse_leading_whitespace() {
507        let sel = Selector::parse("   #main").unwrap();
508        assert_eq!(sel, Selector::Id("main".to_string()));
509    }
510
511    #[test]
512    fn test_parse_trailing_whitespace() {
513        let sel = Selector::parse(".button   ").unwrap();
514        assert_eq!(sel, Selector::Class("button".to_string()));
515    }
516
517    #[test]
518    fn test_parse_only_whitespace() {
519        let result = Selector::parse("   ");
520        assert_eq!(result, Err(SelectorError::Empty));
521    }
522
523    // =========================================================================
524    // Selector Equality Tests
525    // =========================================================================
526
527    #[test]
528    fn test_selector_equality() {
529        let sel1 = Selector::parse("#main").unwrap();
530        let sel2 = Selector::parse("#main").unwrap();
531        assert_eq!(sel1, sel2);
532    }
533
534    #[test]
535    fn test_selector_inequality_different_types() {
536        let id = Selector::parse("#main").unwrap();
537        let class = Selector::parse(".main").unwrap();
538        assert_ne!(id, class);
539    }
540
541    #[test]
542    fn test_selector_inequality_different_values() {
543        let sel1 = Selector::parse("#main").unwrap();
544        let sel2 = Selector::parse("#header").unwrap();
545        assert_ne!(sel1, sel2);
546    }
547
548    // =========================================================================
549    // Selector Clone Tests
550    // =========================================================================
551
552    #[test]
553    fn test_selector_clone() {
554        let sel = Selector::parse("[data-testid='foo']").unwrap();
555        let cloned = sel.clone();
556        assert_eq!(sel, cloned);
557    }
558
559    // =========================================================================
560    // SelectorParser Tests
561    // =========================================================================
562
563    #[test]
564    fn test_parser_new() {
565        let parser = SelectorParser::new("Button");
566        assert_eq!(parser.input, "Button");
567        assert_eq!(parser.pos, 0);
568    }
569
570    // =========================================================================
571    // Complex Selectors (Descendant/Child) - Structure Tests
572    // =========================================================================
573
574    #[test]
575    fn test_selector_descendant_structure() {
576        // Test the structure of descendant selectors
577        let parent = Box::new(Selector::Type("Container".to_string()));
578        let child = Box::new(Selector::Type("Button".to_string()));
579        let desc = Selector::Descendant(parent, child);
580
581        // Verify it's a descendant selector
582        matches!(desc, Selector::Descendant(_, _));
583    }
584
585    #[test]
586    fn test_selector_child_structure() {
587        // Test the structure of child selectors
588        let parent = Box::new(Selector::Type("Row".to_string()));
589        let child = Box::new(Selector::Type("Column".to_string()));
590        let sel = Selector::Child(parent, child);
591
592        // Verify it's a child selector
593        matches!(sel, Selector::Child(_, _));
594    }
595
596    // =========================================================================
597    // Debug Format Tests
598    // =========================================================================
599
600    #[test]
601    fn test_selector_debug_format() {
602        let sel = Selector::parse("#main").unwrap();
603        let debug = format!("{:?}", sel);
604        assert!(debug.contains("Id"));
605        assert!(debug.contains("main"));
606    }
607
608    #[test]
609    fn test_selector_error_debug_format() {
610        let err = SelectorError::Empty;
611        let debug = format!("{:?}", err);
612        assert!(debug.contains("Empty"));
613    }
614
615    // =========================================================================
616    // Unicode Handling (if applicable)
617    // =========================================================================
618
619    #[test]
620    fn test_parse_unicode_in_attribute_value() {
621        let sel = Selector::parse("[aria-label='日本語']").unwrap();
622        assert_eq!(
623            sel,
624            Selector::Attribute {
625                name: "aria-label".to_string(),
626                value: "日本語".to_string(),
627            }
628        );
629    }
630
631    #[test]
632    fn test_parse_emoji_in_attribute_value() {
633        let sel = Selector::parse("[aria-label='Hello 👋']").unwrap();
634        assert_eq!(
635            sel,
636            Selector::Attribute {
637                name: "aria-label".to_string(),
638                value: "Hello 👋".to_string(),
639            }
640        );
641    }
642}