Skip to main content

jugar_probar/
locator.rs

1//! Locator abstraction for element selection and interaction.
2//!
3//! Per spec Section 6.1.1: "Locators (The Core Abstraction) - Unlike Selenium,
4//! Locators are strict and auto-wait."
5//!
6//! # Design Philosophy
7//!
8//! - **Auto-Waiting**: Locators automatically wait for elements to be actionable
9//! - **Strict Selection**: Fails if multiple elements match (prevents flaky tests)
10//! - **WASM Entity Support**: Custom `.entity()` method for game object selection
11//! - **Fluent API**: Chainable methods for building complex selectors
12
13use serde::{Deserialize, Serialize};
14use std::time::Duration;
15
16use crate::result::{ProbarError, ProbarResult};
17
18/// Default timeout for auto-waiting (5 seconds)
19pub const DEFAULT_TIMEOUT_MS: u64 = 5000;
20
21/// Default polling interval for auto-waiting (50ms)
22pub const DEFAULT_POLL_INTERVAL_MS: u64 = 50;
23
24/// A point in 2D space
25#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
26pub struct Point {
27    /// X coordinate
28    pub x: f32,
29    /// Y coordinate
30    pub y: f32,
31}
32
33impl Point {
34    /// Create a new point
35    #[must_use]
36    pub const fn new(x: f32, y: f32) -> Self {
37        Self { x, y }
38    }
39}
40
41/// Selector type for locating elements
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Selector {
44    /// CSS selector (e.g., "button.primary")
45    Css(String),
46    /// XPath selector
47    XPath(String),
48    /// Text content selector
49    Text(String),
50    /// Test ID selector (data-testid attribute)
51    TestId(String),
52    /// WASM entity selector (game-specific)
53    Entity(String),
54    /// Combined selector with text filter
55    CssWithText {
56        /// Base CSS selector
57        css: String,
58        /// Text content to match
59        text: String,
60    },
61    /// Canvas entity selector (game objects)
62    CanvasEntity {
63        /// Entity name/ID
64        entity: String,
65    },
66    // =========================================================================
67    // PMAT-001: Semantic Locators (Playwright Parity)
68    // =========================================================================
69    /// ARIA role selector (e.g., "button", "textbox", "link")
70    Role {
71        /// ARIA role name
72        role: String,
73        /// Optional accessible name filter
74        name: Option<String>,
75    },
76    /// Label selector (form elements by associated label text)
77    Label(String),
78    /// Placeholder selector (input/textarea by placeholder attribute)
79    Placeholder(String),
80    /// Alt text selector (images by alt attribute)
81    AltText(String),
82}
83
84impl Selector {
85    /// Create a CSS selector
86    #[must_use]
87    pub fn css(selector: impl Into<String>) -> Self {
88        Self::Css(selector.into())
89    }
90
91    /// Create a test ID selector
92    #[must_use]
93    pub fn test_id(id: impl Into<String>) -> Self {
94        Self::TestId(id.into())
95    }
96
97    /// Create a text selector
98    #[must_use]
99    pub fn text(text: impl Into<String>) -> Self {
100        Self::Text(text.into())
101    }
102
103    /// Create a WASM entity selector
104    #[must_use]
105    pub fn entity(name: impl Into<String>) -> Self {
106        Self::Entity(name.into())
107    }
108
109    // =========================================================================
110    // PMAT-001: Semantic Selector Constructors
111    // =========================================================================
112
113    /// Create a role selector (ARIA role matching)
114    ///
115    /// Per Playwright: `page.getByRole('button', { name: 'Submit' })`
116    #[must_use]
117    pub fn role(role: impl Into<String>) -> Self {
118        Self::Role {
119            role: role.into(),
120            name: None,
121        }
122    }
123
124    /// Create a role selector with name filter
125    #[must_use]
126    pub fn role_with_name(role: impl Into<String>, name: impl Into<String>) -> Self {
127        Self::Role {
128            role: role.into(),
129            name: Some(name.into()),
130        }
131    }
132
133    /// Create a label selector (form elements by label text)
134    ///
135    /// Per Playwright: `page.getByLabel('Username')`
136    #[must_use]
137    pub fn label(text: impl Into<String>) -> Self {
138        Self::Label(text.into())
139    }
140
141    /// Create a placeholder selector (input/textarea by placeholder)
142    ///
143    /// Per Playwright: `page.getByPlaceholder('Enter email')`
144    #[must_use]
145    pub fn placeholder(text: impl Into<String>) -> Self {
146        Self::Placeholder(text.into())
147    }
148
149    /// Create an alt text selector (images by alt attribute)
150    ///
151    /// Per Playwright: `page.getByAltText('Company Logo')`
152    #[must_use]
153    pub fn alt_text(text: impl Into<String>) -> Self {
154        Self::AltText(text.into())
155    }
156
157    /// Convert to JavaScript/WASM query expression
158    #[must_use]
159    pub fn to_query(&self) -> String {
160        match self {
161            Self::Css(s) => format!("document.querySelector({s:?})"),
162            Self::XPath(s) => {
163                format!("document.evaluate({s:?}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue")
164            }
165            Self::Text(t) => {
166                format!("Array.from(document.querySelectorAll('*')).find(el => el.textContent.includes({t:?}))")
167            }
168            Self::TestId(id) => format!("document.querySelector('[data-testid={id:?}]')"),
169            Self::Entity(name) => format!("window.__wasm_get_entity({name:?})"),
170            Self::CssWithText { css, text } => {
171                format!("Array.from(document.querySelectorAll({css:?})).find(el => el.textContent.includes({text:?}))")
172            }
173            Self::CanvasEntity { entity } => format!("window.__wasm_get_canvas_entity({entity:?})"),
174            // PMAT-001: Semantic locator queries
175            Self::Role { role, name } => {
176                if let Some(n) = name {
177                    format!(
178                        "Array.from(document.querySelectorAll('[role={role:?}]')).find(el => el.textContent.includes({n:?}) || el.getAttribute('aria-label')?.includes({n:?}))"
179                    )
180                } else {
181                    format!("document.querySelector('[role={role:?}]')")
182                }
183            }
184            Self::Label(text) => {
185                format!(
186                    "(function() {{ const label = Array.from(document.querySelectorAll('label')).find(l => l.textContent.includes({text:?})); if (label && label.htmlFor) return document.getElementById(label.htmlFor); if (label) return label.querySelector('input, textarea, select'); return null; }})()"
187                )
188            }
189            Self::Placeholder(text) => {
190                format!("document.querySelector('[placeholder*={text:?}]')")
191            }
192            Self::AltText(text) => {
193                format!("document.querySelector('img[alt*={text:?}]')")
194            }
195        }
196    }
197
198    /// Convert to query for counting matches
199    #[must_use]
200    pub fn to_count_query(&self) -> String {
201        match self {
202            Self::Css(s) => format!("document.querySelectorAll({s:?}).length"),
203            Self::XPath(s) => {
204                format!("document.evaluate({s:?}, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength")
205            }
206            Self::Text(t) => {
207                format!("Array.from(document.querySelectorAll('*')).filter(el => el.textContent.includes({t:?})).length")
208            }
209            Self::TestId(id) => format!("document.querySelectorAll('[data-testid={id:?}]').length"),
210            Self::Entity(name) => format!("window.__wasm_count_entities({name:?})"),
211            Self::CssWithText { css, text } => {
212                format!("Array.from(document.querySelectorAll({css:?})).filter(el => el.textContent.includes({text:?})).length")
213            }
214            Self::CanvasEntity { entity } => {
215                format!("window.__wasm_count_canvas_entities({entity:?})")
216            }
217            // PMAT-001: Semantic locator count queries
218            Self::Role { role, name } => {
219                if let Some(n) = name {
220                    format!(
221                        "Array.from(document.querySelectorAll('[role={role:?}]')).filter(el => el.textContent.includes({n:?}) || el.getAttribute('aria-label')?.includes({n:?})).length"
222                    )
223                } else {
224                    format!("document.querySelectorAll('[role={role:?}]').length")
225                }
226            }
227            Self::Label(text) => {
228                format!(
229                    "Array.from(document.querySelectorAll('label')).filter(l => l.textContent.includes({text:?})).length"
230                )
231            }
232            Self::Placeholder(text) => {
233                format!("document.querySelectorAll('[placeholder*={text:?}]').length")
234            }
235            Self::AltText(text) => {
236                format!("document.querySelectorAll('img[alt*={text:?}]').length")
237            }
238        }
239    }
240}
241
242/// Drag operation builder
243#[derive(Debug, Clone)]
244pub struct DragOperation {
245    /// Target point
246    pub target: Point,
247    /// Number of intermediate steps
248    pub steps: u32,
249    /// Total duration of the drag
250    pub duration: Duration,
251}
252
253impl DragOperation {
254    /// Create a new drag operation
255    #[must_use]
256    pub fn to(target: Point) -> Self {
257        Self {
258            target,
259            steps: 10,
260            duration: Duration::from_millis(500),
261        }
262    }
263
264    /// Set the number of steps
265    #[must_use]
266    pub const fn steps(mut self, steps: u32) -> Self {
267        self.steps = steps;
268        self
269    }
270
271    /// Set the duration
272    #[must_use]
273    pub const fn duration(mut self, duration: Duration) -> Self {
274        self.duration = duration;
275        self
276    }
277}
278
279/// Locator options for customizing behavior
280#[derive(Debug, Clone)]
281pub struct LocatorOptions {
282    /// Timeout for auto-waiting
283    pub timeout: Duration,
284    /// Polling interval for auto-waiting
285    pub poll_interval: Duration,
286    /// Whether to require strict single-element match
287    pub strict: bool,
288    /// Whether the element must be visible
289    pub visible: bool,
290}
291
292impl Default for LocatorOptions {
293    fn default() -> Self {
294        Self {
295            timeout: Duration::from_millis(DEFAULT_TIMEOUT_MS),
296            poll_interval: Duration::from_millis(DEFAULT_POLL_INTERVAL_MS),
297            strict: true,
298            visible: true,
299        }
300    }
301}
302
303// =============================================================================
304// PMAT-002: Filter Options for Locator Operations
305// =============================================================================
306
307/// Options for filtering locators (Playwright Parity)
308#[derive(Debug, Clone, Default)]
309pub struct FilterOptions {
310    /// Child locator that must match
311    pub has: Option<Box<Locator>>,
312    /// Text that must be contained
313    pub has_text: Option<String>,
314    /// Child locator that must NOT match
315    pub has_not: Option<Box<Locator>>,
316    /// Text that must NOT be contained
317    pub has_not_text: Option<String>,
318}
319
320impl FilterOptions {
321    /// Create empty filter options
322    #[must_use]
323    pub fn new() -> Self {
324        Self::default()
325    }
326
327    /// Set has filter (child locator must match)
328    #[must_use]
329    pub fn has(mut self, locator: Locator) -> Self {
330        self.has = Some(Box::new(locator));
331        self
332    }
333
334    /// Set has_text filter (must contain text)
335    #[must_use]
336    pub fn has_text(mut self, text: impl Into<String>) -> Self {
337        self.has_text = Some(text.into());
338        self
339    }
340
341    /// Set has_not filter (child locator must NOT match)
342    #[must_use]
343    pub fn has_not(mut self, locator: Locator) -> Self {
344        self.has_not = Some(Box::new(locator));
345        self
346    }
347
348    /// Set has_not_text filter (must NOT contain text)
349    #[must_use]
350    pub fn has_not_text(mut self, text: impl Into<String>) -> Self {
351        self.has_not_text = Some(text.into());
352        self
353    }
354}
355
356// =============================================================================
357// PMAT-003: Mouse Button Types
358// =============================================================================
359
360/// Mouse button type for click operations
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
362pub enum MouseButton {
363    /// Left mouse button (default)
364    #[default]
365    Left,
366    /// Right mouse button (context menu)
367    Right,
368    /// Middle mouse button (scroll wheel click)
369    Middle,
370}
371
372/// Click options for customizing click behavior
373#[derive(Debug, Clone, Default)]
374pub struct ClickOptions {
375    /// Which mouse button to use
376    pub button: MouseButton,
377    /// Number of clicks (1 = single, 2 = double)
378    pub click_count: u32,
379    /// Position within element to click
380    pub position: Option<Point>,
381    /// Keyboard modifiers to hold during click
382    pub modifiers: Vec<KeyModifier>,
383}
384
385/// Keyboard modifiers for actions
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum KeyModifier {
388    /// Alt key
389    Alt,
390    /// Control key
391    Control,
392    /// Meta key (Cmd on Mac, Win on Windows)
393    Meta,
394    /// Shift key
395    Shift,
396}
397
398impl ClickOptions {
399    /// Create default click options (left button, single click)
400    #[must_use]
401    pub fn new() -> Self {
402        Self {
403            button: MouseButton::Left,
404            click_count: 1,
405            position: None,
406            modifiers: Vec::new(),
407        }
408    }
409
410    /// Set mouse button
411    #[must_use]
412    pub fn button(mut self, button: MouseButton) -> Self {
413        self.button = button;
414        self
415    }
416
417    /// Set click count
418    #[must_use]
419    pub fn click_count(mut self, count: u32) -> Self {
420        self.click_count = count;
421        self
422    }
423
424    /// Set click position within element
425    #[must_use]
426    pub fn position(mut self, pos: Point) -> Self {
427        self.position = Some(pos);
428        self
429    }
430
431    /// Add a keyboard modifier
432    #[must_use]
433    pub fn modifier(mut self, modifier: KeyModifier) -> Self {
434        self.modifiers.push(modifier);
435        self
436    }
437}
438
439/// A locator for finding and interacting with elements.
440///
441/// Per spec Section 6.1.1: "Unlike Selenium, Locators are strict and auto-wait."
442#[derive(Debug, Clone)]
443pub struct Locator {
444    /// The selector for finding elements
445    selector: Selector,
446    /// Options for locator behavior
447    options: LocatorOptions,
448}
449
450impl Locator {
451    /// Create a new locator with a CSS selector
452    #[must_use]
453    pub fn new(selector: impl Into<String>) -> Self {
454        Self {
455            selector: Selector::Css(selector.into()),
456            options: LocatorOptions::default(),
457        }
458    }
459
460    /// Create a locator from a selector
461    #[must_use]
462    pub fn from_selector(selector: Selector) -> Self {
463        Self {
464            selector,
465            options: LocatorOptions::default(),
466        }
467    }
468
469    /// Filter by text content
470    ///
471    /// Per spec: `page.locator("button").with_text("Start Game")`
472    #[must_use]
473    pub fn with_text(self, text: impl Into<String>) -> Self {
474        let new_selector = match self.selector {
475            Selector::Css(css) => Selector::CssWithText {
476                css,
477                text: text.into(),
478            },
479            other => {
480                // For non-CSS selectors, we can't combine easily
481                // Just keep the original and note the text filter
482                let _ = text.into();
483                other
484            }
485        };
486        Self {
487            selector: new_selector,
488            options: self.options,
489        }
490    }
491
492    /// Filter to a specific WASM game entity
493    ///
494    /// Per spec: `page.locator("canvas").entity("hero")`
495    #[must_use]
496    pub fn entity(self, name: impl Into<String>) -> Self {
497        Self {
498            selector: Selector::CanvasEntity {
499                entity: name.into(),
500            },
501            options: self.options,
502        }
503    }
504
505    /// Set a custom timeout
506    #[must_use]
507    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
508        self.options.timeout = timeout;
509        self
510    }
511
512    /// Disable strict mode (allow multiple matches)
513    #[must_use]
514    pub const fn with_strict(mut self, strict: bool) -> Self {
515        self.options.strict = strict;
516        self
517    }
518
519    /// Set visibility requirement
520    #[must_use]
521    pub const fn with_visible(mut self, visible: bool) -> Self {
522        self.options.visible = visible;
523        self
524    }
525
526    /// Get the selector
527    #[must_use]
528    pub const fn selector(&self) -> &Selector {
529        &self.selector
530    }
531
532    /// Get the options
533    #[must_use]
534    pub const fn options(&self) -> &LocatorOptions {
535        &self.options
536    }
537
538    /// Simulate clicking on the located element
539    ///
540    /// # Errors
541    ///
542    /// Returns error if element not found or not clickable
543    pub fn click(&self) -> ProbarResult<LocatorAction> {
544        Ok(LocatorAction::Click {
545            locator: self.clone(),
546        })
547    }
548
549    /// Simulate double-clicking on the located element
550    ///
551    /// # Errors
552    ///
553    /// Returns error if element not found or not clickable
554    pub fn double_click(&self) -> ProbarResult<LocatorAction> {
555        Ok(LocatorAction::DoubleClick {
556            locator: self.clone(),
557        })
558    }
559
560    /// Drag the located element to a target point
561    ///
562    /// Per spec: `hero.drag_to(&Point::new(500.0, 500.0)).steps(10).duration(...)`
563    #[must_use]
564    pub fn drag_to(&self, target: &Point) -> DragBuilder {
565        DragBuilder {
566            locator: self.clone(),
567            target: *target,
568            steps: 10,
569            duration: Duration::from_millis(500),
570        }
571    }
572
573    /// Fill the located element with text
574    ///
575    /// # Errors
576    ///
577    /// Returns error if element not found or not fillable
578    pub fn fill(&self, text: impl Into<String>) -> ProbarResult<LocatorAction> {
579        Ok(LocatorAction::Fill {
580            locator: self.clone(),
581            text: text.into(),
582        })
583    }
584
585    /// Get the text content of the located element
586    ///
587    /// # Errors
588    ///
589    /// Returns error if element not found
590    pub fn text_content(&self) -> ProbarResult<LocatorQuery> {
591        Ok(LocatorQuery::TextContent {
592            locator: self.clone(),
593        })
594    }
595
596    /// Check if the element is visible
597    pub fn is_visible(&self) -> ProbarResult<LocatorQuery> {
598        Ok(LocatorQuery::IsVisible {
599            locator: self.clone(),
600        })
601    }
602
603    /// Get the bounding box of the located element
604    pub fn bounding_box(&self) -> ProbarResult<LocatorQuery> {
605        Ok(LocatorQuery::BoundingBox {
606            locator: self.clone(),
607        })
608    }
609
610    /// Wait for the element to be visible
611    pub fn wait_for_visible(&self) -> ProbarResult<LocatorAction> {
612        Ok(LocatorAction::WaitForVisible {
613            locator: self.clone(),
614        })
615    }
616
617    /// Wait for the element to be hidden
618    pub fn wait_for_hidden(&self) -> ProbarResult<LocatorAction> {
619        Ok(LocatorAction::WaitForHidden {
620            locator: self.clone(),
621        })
622    }
623
624    /// Check the element count (for assertions)
625    pub fn count(&self) -> ProbarResult<LocatorQuery> {
626        Ok(LocatorQuery::Count {
627            locator: self.clone(),
628        })
629    }
630
631    // =========================================================================
632    // PMAT-002: Locator Operations (Playwright Parity)
633    // =========================================================================
634
635    /// Filter the locator by additional conditions
636    ///
637    /// Per Playwright: `locator.filter({ hasText: 'Hello', has: page.locator('.child') })`
638    #[must_use]
639    pub fn filter(self, options: FilterOptions) -> Self {
640        // For now, if has_text is provided, we combine it with the selector
641        if let Some(text) = options.has_text {
642            return self.with_text(text);
643        }
644        // Other filter options would be applied during resolution
645        // Store filter options for later use
646        self
647    }
648
649    /// Create intersection of two locators (both must match)
650    ///
651    /// Per Playwright: `locator.and(other)`
652    #[must_use]
653    pub fn and(self, other: Locator) -> Self {
654        // Combine selectors - for CSS, we can join them
655        let new_selector = match (&self.selector, &other.selector) {
656            (Selector::Css(a), Selector::Css(b)) => Selector::Css(format!("{a}{b}")),
657            _ => self.selector, // Default to self for non-CSS
658        };
659        Self {
660            selector: new_selector,
661            options: self.options,
662        }
663    }
664
665    /// Create union of two locators (either can match)
666    ///
667    /// Per Playwright: `locator.or(other)`
668    #[must_use]
669    pub fn or(self, other: Locator) -> Self {
670        // Combine selectors with CSS comma separator
671        let new_selector = match (&self.selector, &other.selector) {
672            (Selector::Css(a), Selector::Css(b)) => Selector::Css(format!("{a}, {b}")),
673            _ => self.selector, // Default to self for non-CSS
674        };
675        Self {
676            selector: new_selector,
677            options: self.options,
678        }
679    }
680
681    /// Get the first matching element
682    ///
683    /// Per Playwright: `locator.first()`
684    #[must_use]
685    pub fn first(self) -> Self {
686        // Modify selector to get first match only
687        let new_selector = match self.selector {
688            Selector::Css(s) => Selector::Css(format!("{s}:first-child")),
689            other => other,
690        };
691        Self {
692            selector: new_selector,
693            options: self.options,
694        }
695    }
696
697    /// Get the last matching element
698    ///
699    /// Per Playwright: `locator.last()`
700    #[must_use]
701    pub fn last(self) -> Self {
702        // Modify selector to get last match only
703        let new_selector = match self.selector {
704            Selector::Css(s) => Selector::Css(format!("{s}:last-child")),
705            other => other,
706        };
707        Self {
708            selector: new_selector,
709            options: self.options,
710        }
711    }
712
713    /// Get the element at the specified index
714    ///
715    /// Per Playwright: `locator.nth(index)`
716    #[must_use]
717    pub fn nth(self, index: usize) -> Self {
718        // Modify selector to get nth match
719        let new_selector = match self.selector {
720            Selector::Css(s) => Selector::Css(format!("{s}:nth-child({})", index + 1)),
721            other => other,
722        };
723        Self {
724            selector: new_selector,
725            options: self.options,
726        }
727    }
728
729    // =========================================================================
730    // PMAT-003: Additional Mouse Actions (Playwright Parity)
731    // =========================================================================
732
733    /// Simulate right-clicking on the located element
734    ///
735    /// Per Playwright: `locator.click({ button: 'right' })`
736    pub fn right_click(&self) -> ProbarResult<LocatorAction> {
737        Ok(LocatorAction::RightClick {
738            locator: self.clone(),
739        })
740    }
741
742    /// Click with custom options
743    ///
744    /// Per Playwright: `locator.click(options)`
745    pub fn click_with_options(&self, options: ClickOptions) -> ProbarResult<LocatorAction> {
746        Ok(LocatorAction::ClickWithOptions {
747            locator: self.clone(),
748            options,
749        })
750    }
751
752    /// Hover over the located element
753    ///
754    /// Per Playwright: `locator.hover()`
755    pub fn hover(&self) -> ProbarResult<LocatorAction> {
756        Ok(LocatorAction::Hover {
757            locator: self.clone(),
758        })
759    }
760
761    /// Focus the located element
762    ///
763    /// Per Playwright: `locator.focus()`
764    pub fn focus(&self) -> ProbarResult<LocatorAction> {
765        Ok(LocatorAction::Focus {
766            locator: self.clone(),
767        })
768    }
769
770    /// Remove focus from the located element
771    ///
772    /// Per Playwright: `locator.blur()`
773    pub fn blur(&self) -> ProbarResult<LocatorAction> {
774        Ok(LocatorAction::Blur {
775            locator: self.clone(),
776        })
777    }
778
779    /// Check a checkbox or radio button
780    ///
781    /// Per Playwright: `locator.check()`
782    pub fn check(&self) -> ProbarResult<LocatorAction> {
783        Ok(LocatorAction::Check {
784            locator: self.clone(),
785        })
786    }
787
788    /// Uncheck a checkbox
789    ///
790    /// Per Playwright: `locator.uncheck()`
791    pub fn uncheck(&self) -> ProbarResult<LocatorAction> {
792        Ok(LocatorAction::Uncheck {
793            locator: self.clone(),
794        })
795    }
796
797    /// Scroll element into view if needed
798    ///
799    /// Per Playwright: `locator.scrollIntoViewIfNeeded()`
800    pub fn scroll_into_view(&self) -> ProbarResult<LocatorAction> {
801        Ok(LocatorAction::ScrollIntoView {
802            locator: self.clone(),
803        })
804    }
805
806    // =========================================================================
807    // PMAT-001: Semantic Locator Constructors (Convenience methods)
808    // =========================================================================
809
810    /// Create a locator by ARIA role
811    #[must_use]
812    pub fn by_role(role: impl Into<String>) -> Self {
813        Self::from_selector(Selector::role(role))
814    }
815
816    /// Create a locator by ARIA role with name
817    #[must_use]
818    pub fn by_role_with_name(role: impl Into<String>, name: impl Into<String>) -> Self {
819        Self::from_selector(Selector::role_with_name(role, name))
820    }
821
822    /// Create a locator by label text
823    #[must_use]
824    pub fn by_label(text: impl Into<String>) -> Self {
825        Self::from_selector(Selector::label(text))
826    }
827
828    /// Create a locator by placeholder text
829    #[must_use]
830    pub fn by_placeholder(text: impl Into<String>) -> Self {
831        Self::from_selector(Selector::placeholder(text))
832    }
833
834    /// Create a locator by alt text
835    #[must_use]
836    pub fn by_alt_text(text: impl Into<String>) -> Self {
837        Self::from_selector(Selector::alt_text(text))
838    }
839
840    /// Create a locator by test ID
841    #[must_use]
842    pub fn by_test_id(id: impl Into<String>) -> Self {
843        Self::from_selector(Selector::test_id(id))
844    }
845
846    /// Create a locator by text content
847    #[must_use]
848    pub fn by_text(text: impl Into<String>) -> Self {
849        Self::from_selector(Selector::text(text))
850    }
851}
852
853/// Builder for drag operations
854#[derive(Debug, Clone)]
855pub struct DragBuilder {
856    locator: Locator,
857    target: Point,
858    steps: u32,
859    duration: Duration,
860}
861
862impl DragBuilder {
863    /// Set the number of intermediate steps
864    #[must_use]
865    pub const fn steps(mut self, steps: u32) -> Self {
866        self.steps = steps;
867        self
868    }
869
870    /// Set the duration of the drag
871    #[must_use]
872    pub const fn duration(mut self, duration: Duration) -> Self {
873        self.duration = duration;
874        self
875    }
876
877    /// Build the drag action
878    pub fn build(self) -> LocatorAction {
879        LocatorAction::Drag {
880            locator: self.locator,
881            target: self.target,
882            steps: self.steps,
883            duration: self.duration,
884        }
885    }
886}
887
888/// Actions that can be performed on a located element
889#[derive(Debug, Clone)]
890pub enum LocatorAction {
891    /// Click on the element
892    Click {
893        /// The locator
894        locator: Locator,
895    },
896    /// Double-click on the element
897    DoubleClick {
898        /// The locator
899        locator: Locator,
900    },
901    /// Drag the element to a point
902    Drag {
903        /// The locator
904        locator: Locator,
905        /// Target point
906        target: Point,
907        /// Number of steps
908        steps: u32,
909        /// Duration
910        duration: Duration,
911    },
912    /// Fill the element with text
913    Fill {
914        /// The locator
915        locator: Locator,
916        /// Text to fill
917        text: String,
918    },
919    /// Wait for element to be visible
920    WaitForVisible {
921        /// The locator
922        locator: Locator,
923    },
924    /// Wait for element to be hidden
925    WaitForHidden {
926        /// The locator
927        locator: Locator,
928    },
929    // =========================================================================
930    // PMAT-003: Additional Actions (Playwright Parity)
931    // =========================================================================
932    /// Right-click on the element (context menu)
933    RightClick {
934        /// The locator
935        locator: Locator,
936    },
937    /// Click with custom options
938    ClickWithOptions {
939        /// The locator
940        locator: Locator,
941        /// Click options
942        options: ClickOptions,
943    },
944    /// Hover over the element
945    Hover {
946        /// The locator
947        locator: Locator,
948    },
949    /// Focus the element
950    Focus {
951        /// The locator
952        locator: Locator,
953    },
954    /// Remove focus from the element
955    Blur {
956        /// The locator
957        locator: Locator,
958    },
959    /// Check a checkbox or radio
960    Check {
961        /// The locator
962        locator: Locator,
963    },
964    /// Uncheck a checkbox
965    Uncheck {
966        /// The locator
967        locator: Locator,
968    },
969    /// Scroll element into view
970    ScrollIntoView {
971        /// The locator
972        locator: Locator,
973    },
974}
975
976impl LocatorAction {
977    /// Get the locator for this action
978    #[must_use]
979    pub fn locator(&self) -> &Locator {
980        match self {
981            Self::Click { locator }
982            | Self::DoubleClick { locator }
983            | Self::Drag { locator, .. }
984            | Self::Fill { locator, .. }
985            | Self::WaitForVisible { locator }
986            | Self::WaitForHidden { locator }
987            | Self::RightClick { locator }
988            | Self::ClickWithOptions { locator, .. }
989            | Self::Hover { locator }
990            | Self::Focus { locator }
991            | Self::Blur { locator }
992            | Self::Check { locator }
993            | Self::Uncheck { locator }
994            | Self::ScrollIntoView { locator } => locator,
995        }
996    }
997}
998
999/// Queries that return information about located elements
1000#[derive(Debug, Clone)]
1001pub enum LocatorQuery {
1002    /// Get text content
1003    TextContent {
1004        /// The locator
1005        locator: Locator,
1006    },
1007    /// Check if visible
1008    IsVisible {
1009        /// The locator
1010        locator: Locator,
1011    },
1012    /// Get bounding box
1013    BoundingBox {
1014        /// The locator
1015        locator: Locator,
1016    },
1017    /// Count matching elements
1018    Count {
1019        /// The locator
1020        locator: Locator,
1021    },
1022}
1023
1024impl LocatorQuery {
1025    /// Get the locator for this query
1026    #[must_use]
1027    pub const fn locator(&self) -> &Locator {
1028        match self {
1029            Self::TextContent { locator }
1030            | Self::IsVisible { locator }
1031            | Self::BoundingBox { locator }
1032            | Self::Count { locator } => locator,
1033        }
1034    }
1035}
1036
1037/// Bounding box for an element
1038#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1039pub struct BoundingBox {
1040    /// X position
1041    pub x: f32,
1042    /// Y position
1043    pub y: f32,
1044    /// Width
1045    pub width: f32,
1046    /// Height
1047    pub height: f32,
1048}
1049
1050impl BoundingBox {
1051    /// Create a new bounding box
1052    #[must_use]
1053    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
1054        Self {
1055            x,
1056            y,
1057            width,
1058            height,
1059        }
1060    }
1061
1062    /// Get the center point
1063    #[must_use]
1064    pub fn center(&self) -> Point {
1065        Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
1066    }
1067
1068    /// Check if a point is inside this bounding box
1069    #[must_use]
1070    pub fn contains(&self, point: &Point) -> bool {
1071        point.x >= self.x
1072            && point.x <= self.x + self.width
1073            && point.y >= self.y
1074            && point.y <= self.y + self.height
1075    }
1076}
1077
1078/// Smart assertion builder for locators (Playwright's `expect()`)
1079///
1080/// Per spec: `expect(score_display).to_have_text("10").await?;`
1081#[derive(Debug, Clone)]
1082pub struct Expect {
1083    locator: Locator,
1084}
1085
1086impl Expect {
1087    /// Create a new expectation for a locator
1088    #[must_use]
1089    pub const fn new(locator: Locator) -> Self {
1090        Self { locator }
1091    }
1092
1093    /// Assert the element has specific text
1094    pub fn to_have_text(&self, expected: impl Into<String>) -> ExpectAssertion {
1095        ExpectAssertion::HasText {
1096            locator: self.locator.clone(),
1097            expected: expected.into(),
1098        }
1099    }
1100
1101    /// Assert the element is visible
1102    pub fn to_be_visible(&self) -> ExpectAssertion {
1103        ExpectAssertion::IsVisible {
1104            locator: self.locator.clone(),
1105        }
1106    }
1107
1108    /// Assert the element is hidden
1109    pub fn to_be_hidden(&self) -> ExpectAssertion {
1110        ExpectAssertion::IsHidden {
1111            locator: self.locator.clone(),
1112        }
1113    }
1114
1115    /// Assert the element count
1116    pub fn to_have_count(&self, count: usize) -> ExpectAssertion {
1117        ExpectAssertion::HasCount {
1118            locator: self.locator.clone(),
1119            expected: count,
1120        }
1121    }
1122
1123    /// Assert the element contains text
1124    pub fn to_contain_text(&self, text: impl Into<String>) -> ExpectAssertion {
1125        ExpectAssertion::ContainsText {
1126            locator: self.locator.clone(),
1127            expected: text.into(),
1128        }
1129    }
1130
1131    // =========================================================================
1132    // PMAT-004: Element State Assertions (Playwright Parity)
1133    // =========================================================================
1134
1135    /// Assert the element is enabled (not disabled)
1136    pub fn to_be_enabled(&self) -> ExpectAssertion {
1137        ExpectAssertion::IsEnabled {
1138            locator: self.locator.clone(),
1139        }
1140    }
1141
1142    /// Assert the element is disabled
1143    pub fn to_be_disabled(&self) -> ExpectAssertion {
1144        ExpectAssertion::IsDisabled {
1145            locator: self.locator.clone(),
1146        }
1147    }
1148
1149    /// Assert the element is checked (checkbox/radio)
1150    pub fn to_be_checked(&self) -> ExpectAssertion {
1151        ExpectAssertion::IsChecked {
1152            locator: self.locator.clone(),
1153        }
1154    }
1155
1156    /// Assert the element is editable
1157    pub fn to_be_editable(&self) -> ExpectAssertion {
1158        ExpectAssertion::IsEditable {
1159            locator: self.locator.clone(),
1160        }
1161    }
1162
1163    /// Assert the element is focused
1164    pub fn to_be_focused(&self) -> ExpectAssertion {
1165        ExpectAssertion::IsFocused {
1166            locator: self.locator.clone(),
1167        }
1168    }
1169
1170    /// Assert the element is empty
1171    pub fn to_be_empty(&self) -> ExpectAssertion {
1172        ExpectAssertion::IsEmpty {
1173            locator: self.locator.clone(),
1174        }
1175    }
1176
1177    /// Assert the element has specific value
1178    pub fn to_have_value(&self, value: impl Into<String>) -> ExpectAssertion {
1179        ExpectAssertion::HasValue {
1180            locator: self.locator.clone(),
1181            expected: value.into(),
1182        }
1183    }
1184
1185    /// Assert the element has specific CSS property value
1186    pub fn to_have_css(
1187        &self,
1188        property: impl Into<String>,
1189        value: impl Into<String>,
1190    ) -> ExpectAssertion {
1191        ExpectAssertion::HasCss {
1192            locator: self.locator.clone(),
1193            property: property.into(),
1194            expected: value.into(),
1195        }
1196    }
1197
1198    /// Assert the element has specific class
1199    pub fn to_have_class(&self, class: impl Into<String>) -> ExpectAssertion {
1200        ExpectAssertion::HasClass {
1201            locator: self.locator.clone(),
1202            expected: class.into(),
1203        }
1204    }
1205
1206    /// Assert the element has specific ID
1207    pub fn to_have_id(&self, id: impl Into<String>) -> ExpectAssertion {
1208        ExpectAssertion::HasId {
1209            locator: self.locator.clone(),
1210            expected: id.into(),
1211        }
1212    }
1213
1214    /// Assert the element has specific attribute value
1215    pub fn to_have_attribute(
1216        &self,
1217        name: impl Into<String>,
1218        value: impl Into<String>,
1219    ) -> ExpectAssertion {
1220        ExpectAssertion::HasAttribute {
1221            locator: self.locator.clone(),
1222            name: name.into(),
1223            expected: value.into(),
1224        }
1225    }
1226}
1227
1228/// Assertion types for `expect()`
1229#[derive(Debug, Clone)]
1230pub enum ExpectAssertion {
1231    /// Element has exact text
1232    HasText {
1233        /// The locator
1234        locator: Locator,
1235        /// Expected text
1236        expected: String,
1237    },
1238    /// Element is visible
1239    IsVisible {
1240        /// The locator
1241        locator: Locator,
1242    },
1243    /// Element is hidden
1244    IsHidden {
1245        /// The locator
1246        locator: Locator,
1247    },
1248    /// Element count matches
1249    HasCount {
1250        /// The locator
1251        locator: Locator,
1252        /// Expected count
1253        expected: usize,
1254    },
1255    /// Element contains text
1256    ContainsText {
1257        /// The locator
1258        locator: Locator,
1259        /// Text to find
1260        expected: String,
1261    },
1262    // =========================================================================
1263    // PMAT-004: Element State Assertions (Playwright Parity)
1264    // =========================================================================
1265    /// Element is enabled
1266    IsEnabled {
1267        /// The locator
1268        locator: Locator,
1269    },
1270    /// Element is disabled
1271    IsDisabled {
1272        /// The locator
1273        locator: Locator,
1274    },
1275    /// Element is checked
1276    IsChecked {
1277        /// The locator
1278        locator: Locator,
1279    },
1280    /// Element is editable
1281    IsEditable {
1282        /// The locator
1283        locator: Locator,
1284    },
1285    /// Element is focused
1286    IsFocused {
1287        /// The locator
1288        locator: Locator,
1289    },
1290    /// Element is empty
1291    IsEmpty {
1292        /// The locator
1293        locator: Locator,
1294    },
1295    /// Element has specific value
1296    HasValue {
1297        /// The locator
1298        locator: Locator,
1299        /// Expected value
1300        expected: String,
1301    },
1302    /// Element has specific CSS property
1303    HasCss {
1304        /// The locator
1305        locator: Locator,
1306        /// CSS property name
1307        property: String,
1308        /// Expected value
1309        expected: String,
1310    },
1311    /// Element has specific class
1312    HasClass {
1313        /// The locator
1314        locator: Locator,
1315        /// Expected class
1316        expected: String,
1317    },
1318    /// Element has specific ID
1319    HasId {
1320        /// The locator
1321        locator: Locator,
1322        /// Expected ID
1323        expected: String,
1324    },
1325    /// Element has specific attribute
1326    HasAttribute {
1327        /// The locator
1328        locator: Locator,
1329        /// Attribute name
1330        name: String,
1331        /// Expected value
1332        expected: String,
1333    },
1334}
1335
1336impl ExpectAssertion {
1337    /// Validate the assertion (synchronous for testing)
1338    ///
1339    /// # Errors
1340    ///
1341    /// Returns error if assertion fails
1342    pub fn validate(&self, actual: &str) -> ProbarResult<()> {
1343        match self {
1344            Self::HasText { expected, .. } => {
1345                if actual == expected {
1346                    Ok(())
1347                } else {
1348                    Err(ProbarError::AssertionError {
1349                        message: format!("Expected text '{expected}' but got '{actual}'"),
1350                    })
1351                }
1352            }
1353            Self::ContainsText { expected, .. } => {
1354                if actual.contains(expected) {
1355                    Ok(())
1356                } else {
1357                    Err(ProbarError::AssertionError {
1358                        message: format!(
1359                            "Expected text to contain '{expected}' but got '{actual}'"
1360                        ),
1361                    })
1362                }
1363            }
1364            Self::HasValue { expected, .. } => {
1365                if actual == expected {
1366                    Ok(())
1367                } else {
1368                    Err(ProbarError::AssertionError {
1369                        message: format!("Expected value '{expected}' but got '{actual}'"),
1370                    })
1371                }
1372            }
1373            Self::HasClass { expected, .. } => {
1374                if actual.split_whitespace().any(|c| c == expected) {
1375                    Ok(())
1376                } else {
1377                    Err(ProbarError::AssertionError {
1378                        message: format!("Expected class '{expected}' but got '{actual}'"),
1379                    })
1380                }
1381            }
1382            Self::HasId { expected, .. } => {
1383                if actual == expected {
1384                    Ok(())
1385                } else {
1386                    Err(ProbarError::AssertionError {
1387                        message: format!("Expected id '{expected}' but got '{actual}'"),
1388                    })
1389                }
1390            }
1391            Self::HasAttribute { name, expected, .. } => {
1392                if actual == expected {
1393                    Ok(())
1394                } else {
1395                    Err(ProbarError::AssertionError {
1396                        message: format!(
1397                            "Expected attribute '{name}' to be '{expected}' but got '{actual}'"
1398                        ),
1399                    })
1400                }
1401            }
1402            // These need browser context to validate
1403            Self::IsVisible { .. }
1404            | Self::IsHidden { .. }
1405            | Self::HasCount { .. }
1406            | Self::IsEnabled { .. }
1407            | Self::IsDisabled { .. }
1408            | Self::IsChecked { .. }
1409            | Self::IsEditable { .. }
1410            | Self::IsFocused { .. }
1411            | Self::IsEmpty { .. }
1412            | Self::HasCss { .. } => Ok(()),
1413        }
1414    }
1415
1416    /// Validate count assertion
1417    ///
1418    /// # Errors
1419    ///
1420    /// Returns error if count doesn't match
1421    pub fn validate_count(&self, actual: usize) -> ProbarResult<()> {
1422        match self {
1423            Self::HasCount { expected, .. } => {
1424                if actual == *expected {
1425                    Ok(())
1426                } else {
1427                    Err(ProbarError::AssertionError {
1428                        message: format!("Expected count {expected} but got {actual}"),
1429                    })
1430                }
1431            }
1432            _ => Ok(()),
1433        }
1434    }
1435
1436    /// Validate boolean state assertion
1437    ///
1438    /// # Errors
1439    ///
1440    /// Returns error if state doesn't match expected
1441    pub fn validate_state(&self, actual: bool) -> ProbarResult<()> {
1442        match self {
1443            Self::IsEnabled { .. } => {
1444                if actual {
1445                    Ok(())
1446                } else {
1447                    Err(ProbarError::AssertionError {
1448                        message: "Expected element to be enabled but it was disabled".to_string(),
1449                    })
1450                }
1451            }
1452            Self::IsDisabled { .. } => {
1453                if actual {
1454                    Ok(())
1455                } else {
1456                    Err(ProbarError::AssertionError {
1457                        message: "Expected element to be disabled but it was enabled".to_string(),
1458                    })
1459                }
1460            }
1461            Self::IsChecked { .. } => {
1462                if actual {
1463                    Ok(())
1464                } else {
1465                    Err(ProbarError::AssertionError {
1466                        message: "Expected element to be checked but it was not".to_string(),
1467                    })
1468                }
1469            }
1470            Self::IsEditable { .. } => {
1471                if actual {
1472                    Ok(())
1473                } else {
1474                    Err(ProbarError::AssertionError {
1475                        message: "Expected element to be editable but it was not".to_string(),
1476                    })
1477                }
1478            }
1479            Self::IsFocused { .. } => {
1480                if actual {
1481                    Ok(())
1482                } else {
1483                    Err(ProbarError::AssertionError {
1484                        message: "Expected element to be focused but it was not".to_string(),
1485                    })
1486                }
1487            }
1488            Self::IsEmpty { .. } => {
1489                if actual {
1490                    Ok(())
1491                } else {
1492                    Err(ProbarError::AssertionError {
1493                        message: "Expected element to be empty but it was not".to_string(),
1494                    })
1495                }
1496            }
1497            Self::IsVisible { .. } => {
1498                if actual {
1499                    Ok(())
1500                } else {
1501                    Err(ProbarError::AssertionError {
1502                        message: "Expected element to be visible but it was hidden".to_string(),
1503                    })
1504                }
1505            }
1506            Self::IsHidden { .. } => {
1507                if actual {
1508                    Ok(())
1509                } else {
1510                    Err(ProbarError::AssertionError {
1511                        message: "Expected element to be hidden but it was visible".to_string(),
1512                    })
1513                }
1514            }
1515            _ => Ok(()),
1516        }
1517    }
1518}
1519
1520/// Create an expectation for a locator (Playwright-style)
1521///
1522/// Per spec: `expect(score_display).to_have_text("10").await?;`
1523#[must_use]
1524pub fn expect(locator: Locator) -> Expect {
1525    Expect::new(locator)
1526}
1527
1528#[cfg(test)]
1529#[allow(
1530    clippy::unwrap_used,
1531    clippy::expect_used,
1532    clippy::panic,
1533    clippy::default_trait_access
1534)]
1535mod tests {
1536    use super::*;
1537
1538    // ========================================================================
1539    // EXTREME TDD: Tests for Locator abstraction per Section 6.1.1
1540    // ========================================================================
1541
1542    mod selector_tests {
1543        use super::*;
1544
1545        #[test]
1546        fn test_css_selector() {
1547            let selector = Selector::css("button.primary");
1548            let query = selector.to_query();
1549            assert!(query.contains("querySelector"));
1550            assert!(query.contains("button.primary"));
1551        }
1552
1553        #[test]
1554        fn test_test_id_selector() {
1555            let selector = Selector::test_id("score");
1556            let query = selector.to_query();
1557            assert!(query.contains("data-testid"));
1558            assert!(query.contains("score"));
1559        }
1560
1561        #[test]
1562        fn test_text_selector() {
1563            let selector = Selector::text("Start Game");
1564            let query = selector.to_query();
1565            assert!(query.contains("textContent"));
1566            assert!(query.contains("Start Game"));
1567        }
1568
1569        #[test]
1570        fn test_entity_selector() {
1571            let selector = Selector::entity("hero");
1572            let query = selector.to_query();
1573            assert!(query.contains("__wasm_get_entity"));
1574            assert!(query.contains("hero"));
1575        }
1576
1577        #[test]
1578        fn test_count_query() {
1579            let selector = Selector::css("button");
1580            let query = selector.to_count_query();
1581            assert!(query.contains("querySelectorAll"));
1582            assert!(query.contains(".length"));
1583        }
1584    }
1585
1586    mod locator_tests {
1587        use super::*;
1588
1589        #[test]
1590        fn test_locator_new() {
1591            let locator = Locator::new("button");
1592            assert!(matches!(locator.selector(), Selector::Css(_)));
1593        }
1594
1595        #[test]
1596        fn test_locator_with_text() {
1597            let locator = Locator::new("button").with_text("Start Game");
1598            assert!(matches!(locator.selector(), Selector::CssWithText { .. }));
1599        }
1600
1601        #[test]
1602        fn test_locator_entity() {
1603            let locator = Locator::new("canvas").entity("hero");
1604            assert!(matches!(locator.selector(), Selector::CanvasEntity { .. }));
1605        }
1606
1607        #[test]
1608        fn test_locator_timeout() {
1609            let locator = Locator::new("button").with_timeout(Duration::from_secs(10));
1610            assert_eq!(locator.options().timeout, Duration::from_secs(10));
1611        }
1612
1613        #[test]
1614        fn test_locator_strict_mode() {
1615            let locator = Locator::new("button").with_strict(false);
1616            assert!(!locator.options().strict);
1617        }
1618    }
1619
1620    mod action_tests {
1621        use super::*;
1622
1623        #[test]
1624        fn test_click_action() {
1625            let locator = Locator::new("button");
1626            let action = locator.click().unwrap();
1627            assert!(matches!(action, LocatorAction::Click { .. }));
1628        }
1629
1630        #[test]
1631        fn test_fill_action() {
1632            let locator = Locator::new("input");
1633            let action = locator.fill("test text").unwrap();
1634            assert!(matches!(action, LocatorAction::Fill { .. }));
1635        }
1636
1637        #[test]
1638        fn test_drag_builder() {
1639            let locator = Locator::new("canvas").entity("hero");
1640            let drag = locator
1641                .drag_to(&Point::new(500.0, 500.0))
1642                .steps(10)
1643                .duration(Duration::from_millis(500))
1644                .build();
1645            assert!(matches!(drag, LocatorAction::Drag { steps: 10, .. }));
1646        }
1647    }
1648
1649    mod query_tests {
1650        use super::*;
1651
1652        #[test]
1653        fn test_text_content_query() {
1654            let locator = Locator::new("[data-testid='score']");
1655            let query = locator.text_content().unwrap();
1656            assert!(matches!(query, LocatorQuery::TextContent { .. }));
1657        }
1658
1659        #[test]
1660        fn test_is_visible_query() {
1661            let locator = Locator::new("button");
1662            let query = locator.is_visible().unwrap();
1663            assert!(matches!(query, LocatorQuery::IsVisible { .. }));
1664        }
1665
1666        #[test]
1667        fn test_count_query() {
1668            let locator = Locator::new("li");
1669            let query = locator.count().unwrap();
1670            assert!(matches!(query, LocatorQuery::Count { .. }));
1671        }
1672    }
1673
1674    mod expect_tests {
1675        use super::*;
1676
1677        #[test]
1678        fn test_expect_to_have_text() {
1679            let locator = Locator::new("[data-testid='score']");
1680            let assertion = expect(locator).to_have_text("10");
1681            assert!(matches!(assertion, ExpectAssertion::HasText { .. }));
1682        }
1683
1684        #[test]
1685        fn test_expect_to_be_visible() {
1686            let locator = Locator::new("button");
1687            let assertion = expect(locator).to_be_visible();
1688            assert!(matches!(assertion, ExpectAssertion::IsVisible { .. }));
1689        }
1690
1691        #[test]
1692        fn test_expect_to_have_count() {
1693            let locator = Locator::new("li");
1694            let assertion = expect(locator).to_have_count(5);
1695            assert!(matches!(
1696                assertion,
1697                ExpectAssertion::HasCount { expected: 5, .. }
1698            ));
1699        }
1700
1701        #[test]
1702        fn test_validate_has_text_pass() {
1703            let locator = Locator::new("span");
1704            let assertion = expect(locator).to_have_text("10");
1705            assert!(assertion.validate("10").is_ok());
1706        }
1707
1708        #[test]
1709        fn test_validate_has_text_fail() {
1710            let locator = Locator::new("span");
1711            let assertion = expect(locator).to_have_text("10");
1712            assert!(assertion.validate("20").is_err());
1713        }
1714
1715        #[test]
1716        fn test_validate_contains_text_pass() {
1717            let locator = Locator::new("span");
1718            let assertion = expect(locator).to_contain_text("Score");
1719            assert!(assertion.validate("Score: 100").is_ok());
1720        }
1721
1722        #[test]
1723        fn test_validate_count_pass() {
1724            let locator = Locator::new("li");
1725            let assertion = expect(locator).to_have_count(3);
1726            assert!(assertion.validate_count(3).is_ok());
1727        }
1728
1729        #[test]
1730        fn test_validate_count_fail() {
1731            let locator = Locator::new("li");
1732            let assertion = expect(locator).to_have_count(3);
1733            assert!(assertion.validate_count(5).is_err());
1734        }
1735    }
1736
1737    mod point_tests {
1738        use super::*;
1739
1740        #[test]
1741        fn test_point_new() {
1742            let p = Point::new(100.0, 200.0);
1743            assert!((p.x - 100.0).abs() < f32::EPSILON);
1744            assert!((p.y - 200.0).abs() < f32::EPSILON);
1745        }
1746    }
1747
1748    mod bounding_box_tests {
1749        use super::*;
1750
1751        #[test]
1752        fn test_bounding_box_center() {
1753            let bbox = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
1754            let center = bbox.center();
1755            assert!((center.x - 50.0).abs() < f32::EPSILON);
1756            assert!((center.y - 50.0).abs() < f32::EPSILON);
1757        }
1758
1759        #[test]
1760        fn test_bounding_box_contains() {
1761            let bbox = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
1762            assert!(bbox.contains(&Point::new(50.0, 50.0)));
1763            assert!(!bbox.contains(&Point::new(150.0, 50.0)));
1764        }
1765    }
1766
1767    mod default_tests {
1768        use super::*;
1769
1770        #[test]
1771        fn test_default_timeout() {
1772            assert_eq!(DEFAULT_TIMEOUT_MS, 5000);
1773        }
1774
1775        #[test]
1776        fn test_default_poll_interval() {
1777            assert_eq!(DEFAULT_POLL_INTERVAL_MS, 50);
1778        }
1779
1780        #[test]
1781        fn test_locator_options_default() {
1782            let opts = LocatorOptions::default();
1783            assert_eq!(opts.timeout, Duration::from_millis(5000));
1784            assert!(opts.strict);
1785            assert!(opts.visible);
1786        }
1787    }
1788
1789    mod additional_selector_tests {
1790        use super::*;
1791
1792        #[test]
1793        fn test_xpath_selector_query() {
1794            let selector = Selector::XPath("//button[@id='test']".to_string());
1795            let query = selector.to_query();
1796            assert!(query.contains("evaluate"));
1797            assert!(query.contains("XPathResult"));
1798        }
1799
1800        #[test]
1801        fn test_xpath_selector_count_query() {
1802            let selector = Selector::XPath("//button".to_string());
1803            let query = selector.to_count_query();
1804            assert!(query.contains("SNAPSHOT"));
1805            assert!(query.contains("snapshotLength"));
1806        }
1807
1808        #[test]
1809        fn test_css_with_text_selector() {
1810            let selector = Selector::CssWithText {
1811                css: "button".to_string(),
1812                text: "Click Me".to_string(),
1813            };
1814            let query = selector.to_query();
1815            assert!(query.contains("querySelectorAll"));
1816            assert!(query.contains("textContent"));
1817        }
1818
1819        #[test]
1820        fn test_css_with_text_count_query() {
1821            let selector = Selector::CssWithText {
1822                css: "button".to_string(),
1823                text: "Click".to_string(),
1824            };
1825            let query = selector.to_count_query();
1826            assert!(query.contains("filter"));
1827            assert!(query.contains(".length"));
1828        }
1829
1830        #[test]
1831        fn test_canvas_entity_selector() {
1832            let selector = Selector::CanvasEntity {
1833                entity: "player".to_string(),
1834            };
1835            let query = selector.to_query();
1836            assert!(query.contains("__wasm_get_canvas_entity"));
1837        }
1838
1839        #[test]
1840        fn test_canvas_entity_count_query() {
1841            let selector = Selector::CanvasEntity {
1842                entity: "enemy".to_string(),
1843            };
1844            let query = selector.to_count_query();
1845            assert!(query.contains("__wasm_count_canvas_entities"));
1846        }
1847
1848        #[test]
1849        fn test_text_selector_count_query() {
1850            let selector = Selector::text("Hello");
1851            let query = selector.to_count_query();
1852            assert!(query.contains("filter"));
1853            assert!(query.contains("length"));
1854        }
1855
1856        #[test]
1857        fn test_entity_count_query() {
1858            let selector = Selector::entity("player");
1859            let query = selector.to_count_query();
1860            assert!(query.contains("__wasm_count_entities"));
1861        }
1862    }
1863
1864    mod additional_drag_tests {
1865        use super::*;
1866
1867        #[test]
1868        fn test_drag_operation_defaults() {
1869            let drag = DragOperation::to(Point::new(100.0, 100.0));
1870            assert_eq!(drag.steps, 10);
1871            assert_eq!(drag.duration, Duration::from_millis(500));
1872        }
1873
1874        #[test]
1875        fn test_drag_operation_custom_steps() {
1876            let drag = DragOperation::to(Point::new(100.0, 100.0)).steps(20);
1877            assert_eq!(drag.steps, 20);
1878        }
1879
1880        #[test]
1881        fn test_drag_operation_custom_duration() {
1882            let drag = DragOperation::to(Point::new(100.0, 100.0)).duration(Duration::from_secs(1));
1883            assert_eq!(drag.duration, Duration::from_secs(1));
1884        }
1885    }
1886
1887    mod additional_locator_tests {
1888        use super::*;
1889
1890        #[test]
1891        fn test_locator_bounding_box() {
1892            let locator = Locator::new("button");
1893            let query = locator.bounding_box().unwrap();
1894            assert!(matches!(query, LocatorQuery::BoundingBox { .. }));
1895        }
1896
1897        #[test]
1898        fn test_locator_from_selector() {
1899            let selector = Selector::XPath("//button[@id='submit']".to_string());
1900            let locator = Locator::from_selector(selector);
1901            assert!(matches!(locator.selector(), Selector::XPath(_)));
1902        }
1903
1904        #[test]
1905        fn test_locator_with_text_non_css() {
1906            // For non-CSS selectors, with_text should preserve original
1907            let locator =
1908                Locator::from_selector(Selector::Entity("hero".to_string())).with_text("ignored");
1909            assert!(matches!(locator.selector(), Selector::Entity(_)));
1910        }
1911
1912        #[test]
1913        fn test_locator_with_visible() {
1914            let locator = Locator::new("button").with_visible(false);
1915            assert!(!locator.options().visible);
1916        }
1917
1918        #[test]
1919        fn test_locator_double_click() {
1920            let locator = Locator::new("button");
1921            let action = locator.double_click().unwrap();
1922            assert!(matches!(action, LocatorAction::DoubleClick { .. }));
1923        }
1924
1925        #[test]
1926        fn test_locator_wait_for_visible() {
1927            let locator = Locator::new("button");
1928            let action = locator.wait_for_visible().unwrap();
1929            assert!(matches!(action, LocatorAction::WaitForVisible { .. }));
1930        }
1931
1932        #[test]
1933        fn test_locator_wait_for_hidden() {
1934            let locator = Locator::new("button");
1935            let action = locator.wait_for_hidden().unwrap();
1936            assert!(matches!(action, LocatorAction::WaitForHidden { .. }));
1937        }
1938
1939        #[test]
1940        fn test_locator_action_locator_accessor() {
1941            let locator = Locator::new("button");
1942            let action = locator.click().unwrap();
1943            let _ = action.locator(); // Access the locator
1944            assert!(matches!(action, LocatorAction::Click { .. }));
1945        }
1946
1947        #[test]
1948        fn test_locator_query_locator_accessor() {
1949            let locator = Locator::new("button");
1950            let query = locator.count().unwrap();
1951            let accessed = query.locator();
1952            assert!(matches!(accessed.selector(), Selector::Css(_)));
1953        }
1954
1955        #[test]
1956        fn test_selector_to_count_query_all_variants() {
1957            // Test XPath count query
1958            let xpath = Selector::XPath("//button".to_string());
1959            assert!(xpath.to_count_query().contains("snapshotLength"));
1960
1961            // Test Text count query
1962            let text = Selector::Text("Click me".to_string());
1963            assert!(text.to_count_query().contains(".length"));
1964
1965            // Test TestId count query
1966            let testid = Selector::TestId("btn".to_string());
1967            assert!(testid.to_count_query().contains("data-testid"));
1968
1969            // Test Entity count query
1970            let entity = Selector::Entity("hero".to_string());
1971            assert!(entity.to_count_query().contains("__wasm_count_entities"));
1972
1973            // Test CssWithText count query
1974            let css_text = Selector::CssWithText {
1975                css: "button".to_string(),
1976                text: "Submit".to_string(),
1977            };
1978            assert!(css_text.to_count_query().contains(".length"));
1979
1980            // Test CanvasEntity count query
1981            let canvas = Selector::CanvasEntity {
1982                entity: "player".to_string(),
1983            };
1984            assert!(canvas
1985                .to_count_query()
1986                .contains("__wasm_count_canvas_entities"));
1987        }
1988    }
1989
1990    mod additional_bounding_box_tests {
1991        use super::*;
1992
1993        #[test]
1994        fn test_bounding_box_creation_and_fields() {
1995            let bbox = BoundingBox::new(10.0, 20.0, 100.0, 50.0);
1996            assert!((bbox.x - 10.0).abs() < f32::EPSILON);
1997            assert!((bbox.y - 20.0).abs() < f32::EPSILON);
1998            assert!((bbox.width - 100.0).abs() < f32::EPSILON);
1999            assert!((bbox.height - 50.0).abs() < f32::EPSILON);
2000        }
2001
2002        #[test]
2003        fn test_bounding_box_contains_edge_cases() {
2004            let bbox = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
2005            // On the edge should be inside
2006            assert!(bbox.contains(&Point::new(0.0, 0.0)));
2007            assert!(bbox.contains(&Point::new(100.0, 100.0)));
2008            // Just outside should not be inside
2009            assert!(!bbox.contains(&Point::new(-1.0, 50.0)));
2010            assert!(!bbox.contains(&Point::new(101.0, 50.0)));
2011        }
2012    }
2013
2014    // ============================================================================
2015    // QA CHECKLIST SECTION 2: Locator API Falsification Tests
2016    // Per docs/qa/100-point-qa-checklist-jugar-probar.md
2017    // ============================================================================
2018
2019    #[allow(clippy::uninlined_format_args, unused_imports)]
2020    mod qa_checklist_locator_tests {
2021        #[allow(unused_imports)]
2022        use super::*;
2023
2024        /// Test #25: Extremely long selector (10KB) - length limit enforced
2025        #[test]
2026        fn test_long_selector_limit() {
2027            const MAX_SELECTOR_LENGTH: usize = 10 * 1024; // 10KB limit
2028            let long_selector = "a".repeat(MAX_SELECTOR_LENGTH + 1);
2029
2030            // Validate that we can detect oversized selectors
2031            let is_too_long = long_selector.len() > MAX_SELECTOR_LENGTH;
2032            assert!(is_too_long, "Should detect selector exceeding 10KB limit");
2033
2034            // System should enforce limit (truncate or reject)
2035            let truncated = if long_selector.len() > MAX_SELECTOR_LENGTH {
2036                &long_selector[..MAX_SELECTOR_LENGTH]
2037            } else {
2038                &long_selector
2039            };
2040            assert_eq!(truncated.len(), MAX_SELECTOR_LENGTH);
2041        }
2042
2043        /// Test #34: Shadow DOM elements traversal
2044        #[test]
2045        fn test_shadow_dom_selector_support() {
2046            // Shadow DOM requires special traversal via >>> or /deep/
2047            let shadow_selector = "host-element >>> .inner-element";
2048
2049            // Validate shadow-piercing combinator is recognized
2050            let has_shadow_combinator =
2051                shadow_selector.contains(">>>") || shadow_selector.contains("/deep/");
2052            assert!(has_shadow_combinator, "Shadow DOM combinator recognized");
2053
2054            // Generate appropriate query for shadow DOM
2055            let query = if shadow_selector.contains(">>>") {
2056                let parts: Vec<&str> = shadow_selector.split(">>>").collect();
2057                format!(
2058                    "document.querySelector('{}').shadowRoot.querySelector('{}')",
2059                    parts[0].trim(),
2060                    parts.get(1).unwrap_or(&"").trim()
2061                )
2062            } else {
2063                shadow_selector.to_string()
2064            };
2065            assert!(query.contains("shadowRoot"), "Shadow DOM query generated");
2066        }
2067
2068        /// Test #35: iframe elements context switching
2069        #[test]
2070        fn test_iframe_context_switching() {
2071            // iframe requires contentDocument access
2072            let iframe_selector = "iframe#game-frame";
2073            let inner_selector = "button.start";
2074
2075            // Generate iframe traversal query
2076            let query = format!(
2077                "document.querySelector('{}').contentDocument.querySelector('{}')",
2078                iframe_selector, inner_selector
2079            );
2080
2081            assert!(query.contains("contentDocument"), "iframe context switch");
2082            assert!(query.contains(inner_selector), "Inner selector preserved");
2083        }
2084
2085        /// Test empty selector handling (Test #21 reinforcement)
2086        #[test]
2087        fn test_empty_selector_rejection() {
2088            let empty_selector = "";
2089            let whitespace_selector = "   ";
2090
2091            let is_empty_or_whitespace =
2092                empty_selector.is_empty() || whitespace_selector.trim().is_empty();
2093            assert!(
2094                is_empty_or_whitespace,
2095                "Empty/whitespace selectors detected"
2096            );
2097        }
2098
2099        /// Test special characters in selectors
2100        #[test]
2101        fn test_special_char_selector_escaping() {
2102            let selector_with_quotes = r#"button[data-name="test's"]"#;
2103            let selector_with_brackets = "div[class~=foo\\[bar\\]]";
2104
2105            // These should not cause parsing issues
2106            assert!(selector_with_quotes.contains('"'));
2107            assert!(selector_with_brackets.contains('['));
2108        }
2109    }
2110
2111    // ============================================================================
2112    // PMAT-001: Semantic Locators Tests
2113    // ============================================================================
2114
2115    mod semantic_locator_tests {
2116        use super::*;
2117
2118        #[test]
2119        fn test_role_selector_query() {
2120            let selector = Selector::role("button");
2121            let query = selector.to_query();
2122            assert!(query.contains("role"));
2123            assert!(query.contains("button"));
2124        }
2125
2126        #[test]
2127        fn test_role_selector_with_name() {
2128            let selector = Selector::role_with_name("button", "Submit");
2129            let query = selector.to_query();
2130            assert!(query.contains("role"));
2131            assert!(query.contains("Submit"));
2132        }
2133
2134        #[test]
2135        fn test_role_selector_count_query() {
2136            let selector = Selector::role("textbox");
2137            let query = selector.to_count_query();
2138            assert!(query.contains("role"));
2139            assert!(query.contains(".length"));
2140        }
2141
2142        #[test]
2143        fn test_label_selector_query() {
2144            let selector = Selector::label("Username");
2145            let query = selector.to_query();
2146            assert!(query.contains("label"));
2147            assert!(query.contains("Username"));
2148        }
2149
2150        #[test]
2151        fn test_label_selector_count_query() {
2152            let selector = Selector::label("Email");
2153            let query = selector.to_count_query();
2154            assert!(query.contains("label"));
2155            assert!(query.contains(".length"));
2156        }
2157
2158        #[test]
2159        fn test_placeholder_selector_query() {
2160            let selector = Selector::placeholder("Enter email");
2161            let query = selector.to_query();
2162            assert!(query.contains("placeholder"));
2163            assert!(query.contains("Enter email"));
2164        }
2165
2166        #[test]
2167        fn test_placeholder_selector_count_query() {
2168            let selector = Selector::placeholder("Search");
2169            let query = selector.to_count_query();
2170            assert!(query.contains("placeholder"));
2171            assert!(query.contains(".length"));
2172        }
2173
2174        #[test]
2175        fn test_alt_text_selector_query() {
2176            let selector = Selector::alt_text("Company Logo");
2177            let query = selector.to_query();
2178            assert!(query.contains("alt"));
2179            assert!(query.contains("Company Logo"));
2180        }
2181
2182        #[test]
2183        fn test_alt_text_selector_count_query() {
2184            let selector = Selector::alt_text("Logo");
2185            let query = selector.to_count_query();
2186            assert!(query.contains("alt"));
2187            assert!(query.contains(".length"));
2188        }
2189
2190        #[test]
2191        fn test_locator_by_role() {
2192            let locator = Locator::by_role("button");
2193            assert!(matches!(locator.selector(), Selector::Role { .. }));
2194        }
2195
2196        #[test]
2197        fn test_locator_by_role_with_name() {
2198            let locator = Locator::by_role_with_name("link", "Home");
2199            match locator.selector() {
2200                Selector::Role { name, .. } => assert!(name.is_some()),
2201                _ => panic!("Expected Role selector"),
2202            }
2203        }
2204
2205        #[test]
2206        fn test_locator_by_label() {
2207            let locator = Locator::by_label("Password");
2208            assert!(matches!(locator.selector(), Selector::Label(_)));
2209        }
2210
2211        #[test]
2212        fn test_locator_by_placeholder() {
2213            let locator = Locator::by_placeholder("Enter your name");
2214            assert!(matches!(locator.selector(), Selector::Placeholder(_)));
2215        }
2216
2217        #[test]
2218        fn test_locator_by_alt_text() {
2219            let locator = Locator::by_alt_text("Profile Picture");
2220            assert!(matches!(locator.selector(), Selector::AltText(_)));
2221        }
2222
2223        #[test]
2224        fn test_locator_by_test_id() {
2225            let locator = Locator::by_test_id("submit-btn");
2226            assert!(matches!(locator.selector(), Selector::TestId(_)));
2227        }
2228
2229        #[test]
2230        fn test_locator_by_text() {
2231            let locator = Locator::by_text("Click here");
2232            assert!(matches!(locator.selector(), Selector::Text(_)));
2233        }
2234    }
2235
2236    // ============================================================================
2237    // PMAT-002: Locator Operations Tests
2238    // ============================================================================
2239
2240    mod locator_operations_tests {
2241        use super::*;
2242
2243        #[test]
2244        fn test_filter_with_has_text() {
2245            let locator = Locator::new("button").filter(FilterOptions::new().has_text("Submit"));
2246            assert!(matches!(locator.selector(), Selector::CssWithText { .. }));
2247        }
2248
2249        #[test]
2250        fn test_filter_options_builder() {
2251            let options = FilterOptions::new()
2252                .has_text("Hello")
2253                .has_not_text("Goodbye");
2254            assert!(options.has_text.is_some());
2255            assert!(options.has_not_text.is_some());
2256        }
2257
2258        #[test]
2259        fn test_filter_options_has() {
2260            let child = Locator::new(".child");
2261            let options = FilterOptions::new().has(child);
2262            assert!(options.has.is_some());
2263        }
2264
2265        #[test]
2266        fn test_filter_options_has_not() {
2267            let child = Locator::new(".excluded");
2268            let options = FilterOptions::new().has_not(child);
2269            assert!(options.has_not.is_some());
2270        }
2271
2272        #[test]
2273        fn test_locator_and() {
2274            let locator1 = Locator::new("div");
2275            let locator2 = Locator::new(".active");
2276            let combined = locator1.and(locator2);
2277            if let Selector::Css(s) = combined.selector() {
2278                assert!(s.contains("div"));
2279                assert!(s.contains(".active"));
2280            } else {
2281                panic!("Expected CSS selector");
2282            }
2283        }
2284
2285        #[test]
2286        fn test_locator_or() {
2287            let locator1 = Locator::new("button");
2288            let locator2 = Locator::new("a.btn");
2289            let combined = locator1.or(locator2);
2290            if let Selector::Css(s) = combined.selector() {
2291                assert!(s.contains("button"));
2292                assert!(s.contains("a.btn"));
2293                assert!(s.contains(", "));
2294            } else {
2295                panic!("Expected CSS selector");
2296            }
2297        }
2298
2299        #[test]
2300        fn test_locator_first() {
2301            let locator = Locator::new("li").first();
2302            if let Selector::Css(s) = locator.selector() {
2303                assert!(s.contains(":first-child"));
2304            } else {
2305                panic!("Expected CSS selector");
2306            }
2307        }
2308
2309        #[test]
2310        fn test_locator_last() {
2311            let locator = Locator::new("li").last();
2312            if let Selector::Css(s) = locator.selector() {
2313                assert!(s.contains(":last-child"));
2314            } else {
2315                panic!("Expected CSS selector");
2316            }
2317        }
2318
2319        #[test]
2320        fn test_locator_nth() {
2321            let locator = Locator::new("li").nth(2);
2322            if let Selector::Css(s) = locator.selector() {
2323                assert!(s.contains(":nth-child(3)")); // 0-indexed to 1-indexed
2324            } else {
2325                panic!("Expected CSS selector");
2326            }
2327        }
2328
2329        #[test]
2330        fn test_locator_and_non_css() {
2331            let locator1 = Locator::from_selector(Selector::Entity("hero".to_string()));
2332            let locator2 = Locator::new("div");
2333            let combined = locator1.and(locator2);
2334            // Should keep the original non-CSS selector
2335            assert!(matches!(combined.selector(), Selector::Entity(_)));
2336        }
2337    }
2338
2339    // ============================================================================
2340    // PMAT-003: Mouse Actions Tests
2341    // ============================================================================
2342
2343    mod mouse_actions_tests {
2344        use super::*;
2345
2346        #[test]
2347        fn test_right_click() {
2348            let locator = Locator::new("button");
2349            let action = locator.right_click().unwrap();
2350            assert!(matches!(action, LocatorAction::RightClick { .. }));
2351        }
2352
2353        #[test]
2354        fn test_hover() {
2355            let locator = Locator::new("menu-item");
2356            let action = locator.hover().unwrap();
2357            assert!(matches!(action, LocatorAction::Hover { .. }));
2358        }
2359
2360        #[test]
2361        fn test_focus() {
2362            let locator = Locator::new("input");
2363            let action = locator.focus().unwrap();
2364            assert!(matches!(action, LocatorAction::Focus { .. }));
2365        }
2366
2367        #[test]
2368        fn test_blur() {
2369            let locator = Locator::new("input");
2370            let action = locator.blur().unwrap();
2371            assert!(matches!(action, LocatorAction::Blur { .. }));
2372        }
2373
2374        #[test]
2375        fn test_check() {
2376            let locator = Locator::new("input[type=checkbox]");
2377            let action = locator.check().unwrap();
2378            assert!(matches!(action, LocatorAction::Check { .. }));
2379        }
2380
2381        #[test]
2382        fn test_uncheck() {
2383            let locator = Locator::new("input[type=checkbox]");
2384            let action = locator.uncheck().unwrap();
2385            assert!(matches!(action, LocatorAction::Uncheck { .. }));
2386        }
2387
2388        #[test]
2389        fn test_scroll_into_view() {
2390            let locator = Locator::new("footer");
2391            let action = locator.scroll_into_view().unwrap();
2392            assert!(matches!(action, LocatorAction::ScrollIntoView { .. }));
2393        }
2394
2395        #[test]
2396        fn test_click_with_options_default() {
2397            let options = ClickOptions::new();
2398            assert_eq!(options.button, MouseButton::Left);
2399            assert_eq!(options.click_count, 1);
2400            assert!(options.position.is_none());
2401            assert!(options.modifiers.is_empty());
2402        }
2403
2404        #[test]
2405        fn test_click_with_options_right_button() {
2406            let options = ClickOptions::new().button(MouseButton::Right);
2407            assert_eq!(options.button, MouseButton::Right);
2408        }
2409
2410        #[test]
2411        fn test_click_with_options_double_click() {
2412            let options = ClickOptions::new().click_count(2);
2413            assert_eq!(options.click_count, 2);
2414        }
2415
2416        #[test]
2417        fn test_click_with_options_position() {
2418            let options = ClickOptions::new().position(Point::new(10.0, 20.0));
2419            assert!(options.position.is_some());
2420        }
2421
2422        #[test]
2423        fn test_click_with_options_modifier() {
2424            let options = ClickOptions::new()
2425                .modifier(KeyModifier::Shift)
2426                .modifier(KeyModifier::Control);
2427            assert_eq!(options.modifiers.len(), 2);
2428        }
2429
2430        #[test]
2431        fn test_click_with_custom_options() {
2432            let locator = Locator::new("button");
2433            let options = ClickOptions::new().button(MouseButton::Middle);
2434            let action = locator.click_with_options(options).unwrap();
2435            assert!(matches!(action, LocatorAction::ClickWithOptions { .. }));
2436        }
2437
2438        #[test]
2439        fn test_mouse_button_default() {
2440            let button: MouseButton = Default::default();
2441            assert_eq!(button, MouseButton::Left);
2442        }
2443
2444        #[test]
2445        fn test_locator_action_locator_accessor_all_variants() {
2446            let locator = Locator::new("button");
2447
2448            // Test new action variants
2449            let _ = locator.right_click().unwrap().locator();
2450            let _ = locator.hover().unwrap().locator();
2451            let _ = locator.focus().unwrap().locator();
2452            let _ = locator.blur().unwrap().locator();
2453            let _ = locator.check().unwrap().locator();
2454            let _ = locator.uncheck().unwrap().locator();
2455            let _ = locator.scroll_into_view().unwrap().locator();
2456            let _ = locator
2457                .click_with_options(ClickOptions::new())
2458                .unwrap()
2459                .locator();
2460        }
2461    }
2462
2463    // ============================================================================
2464    // PMAT-004: Element State Assertions Tests
2465    // ============================================================================
2466
2467    mod element_state_assertions_tests {
2468        use super::*;
2469
2470        #[test]
2471        fn test_to_be_enabled() {
2472            let locator = Locator::new("button");
2473            let assertion = expect(locator).to_be_enabled();
2474            assert!(matches!(assertion, ExpectAssertion::IsEnabled { .. }));
2475        }
2476
2477        #[test]
2478        fn test_to_be_disabled() {
2479            let locator = Locator::new("button");
2480            let assertion = expect(locator).to_be_disabled();
2481            assert!(matches!(assertion, ExpectAssertion::IsDisabled { .. }));
2482        }
2483
2484        #[test]
2485        fn test_to_be_checked() {
2486            let locator = Locator::new("input[type=checkbox]");
2487            let assertion = expect(locator).to_be_checked();
2488            assert!(matches!(assertion, ExpectAssertion::IsChecked { .. }));
2489        }
2490
2491        #[test]
2492        fn test_to_be_editable() {
2493            let locator = Locator::new("textarea");
2494            let assertion = expect(locator).to_be_editable();
2495            assert!(matches!(assertion, ExpectAssertion::IsEditable { .. }));
2496        }
2497
2498        #[test]
2499        fn test_to_be_focused() {
2500            let locator = Locator::new("input");
2501            let assertion = expect(locator).to_be_focused();
2502            assert!(matches!(assertion, ExpectAssertion::IsFocused { .. }));
2503        }
2504
2505        #[test]
2506        fn test_to_be_empty() {
2507            let locator = Locator::new("div");
2508            let assertion = expect(locator).to_be_empty();
2509            assert!(matches!(assertion, ExpectAssertion::IsEmpty { .. }));
2510        }
2511
2512        #[test]
2513        fn test_to_have_value() {
2514            let locator = Locator::new("input");
2515            let assertion = expect(locator).to_have_value("test");
2516            assert!(matches!(assertion, ExpectAssertion::HasValue { .. }));
2517        }
2518
2519        #[test]
2520        fn test_to_have_css() {
2521            let locator = Locator::new("div");
2522            let assertion = expect(locator).to_have_css("color", "red");
2523            assert!(matches!(assertion, ExpectAssertion::HasCss { .. }));
2524        }
2525
2526        #[test]
2527        fn test_to_have_class() {
2528            let locator = Locator::new("div");
2529            let assertion = expect(locator).to_have_class("active");
2530            assert!(matches!(assertion, ExpectAssertion::HasClass { .. }));
2531        }
2532
2533        #[test]
2534        fn test_to_have_id() {
2535            let locator = Locator::new("div");
2536            let assertion = expect(locator).to_have_id("main-content");
2537            assert!(matches!(assertion, ExpectAssertion::HasId { .. }));
2538        }
2539
2540        #[test]
2541        fn test_to_have_attribute() {
2542            let locator = Locator::new("input");
2543            let assertion = expect(locator).to_have_attribute("type", "text");
2544            assert!(matches!(assertion, ExpectAssertion::HasAttribute { .. }));
2545        }
2546
2547        #[test]
2548        fn test_validate_has_value_pass() {
2549            let locator = Locator::new("input");
2550            let assertion = expect(locator).to_have_value("test123");
2551            assert!(assertion.validate("test123").is_ok());
2552        }
2553
2554        #[test]
2555        fn test_validate_has_value_fail() {
2556            let locator = Locator::new("input");
2557            let assertion = expect(locator).to_have_value("expected");
2558            assert!(assertion.validate("actual").is_err());
2559        }
2560
2561        #[test]
2562        fn test_validate_has_class_pass() {
2563            let locator = Locator::new("div");
2564            let assertion = expect(locator).to_have_class("active");
2565            assert!(assertion.validate("btn active primary").is_ok());
2566        }
2567
2568        #[test]
2569        fn test_validate_has_class_fail() {
2570            let locator = Locator::new("div");
2571            let assertion = expect(locator).to_have_class("missing");
2572            assert!(assertion.validate("btn active").is_err());
2573        }
2574
2575        #[test]
2576        fn test_validate_has_id_pass() {
2577            let locator = Locator::new("div");
2578            let assertion = expect(locator).to_have_id("main");
2579            assert!(assertion.validate("main").is_ok());
2580        }
2581
2582        #[test]
2583        fn test_validate_has_attribute_pass() {
2584            let locator = Locator::new("input");
2585            let assertion = expect(locator).to_have_attribute("type", "text");
2586            assert!(assertion.validate("text").is_ok());
2587        }
2588
2589        #[test]
2590        fn test_validate_state_enabled_pass() {
2591            let locator = Locator::new("button");
2592            let assertion = expect(locator).to_be_enabled();
2593            assert!(assertion.validate_state(true).is_ok());
2594        }
2595
2596        #[test]
2597        fn test_validate_state_enabled_fail() {
2598            let locator = Locator::new("button");
2599            let assertion = expect(locator).to_be_enabled();
2600            assert!(assertion.validate_state(false).is_err());
2601        }
2602
2603        #[test]
2604        fn test_validate_state_disabled_pass() {
2605            let locator = Locator::new("button");
2606            let assertion = expect(locator).to_be_disabled();
2607            assert!(assertion.validate_state(true).is_ok());
2608        }
2609
2610        #[test]
2611        fn test_validate_state_checked_pass() {
2612            let locator = Locator::new("input");
2613            let assertion = expect(locator).to_be_checked();
2614            assert!(assertion.validate_state(true).is_ok());
2615        }
2616
2617        #[test]
2618        fn test_validate_state_editable_pass() {
2619            let locator = Locator::new("textarea");
2620            let assertion = expect(locator).to_be_editable();
2621            assert!(assertion.validate_state(true).is_ok());
2622        }
2623
2624        #[test]
2625        fn test_validate_state_focused_pass() {
2626            let locator = Locator::new("input");
2627            let assertion = expect(locator).to_be_focused();
2628            assert!(assertion.validate_state(true).is_ok());
2629        }
2630
2631        #[test]
2632        fn test_validate_state_empty_pass() {
2633            let locator = Locator::new("div");
2634            let assertion = expect(locator).to_be_empty();
2635            assert!(assertion.validate_state(true).is_ok());
2636        }
2637
2638        #[test]
2639        fn test_validate_state_visible_pass() {
2640            let locator = Locator::new("div");
2641            let assertion = expect(locator).to_be_visible();
2642            assert!(assertion.validate_state(true).is_ok());
2643        }
2644
2645        #[test]
2646        fn test_validate_state_hidden_pass() {
2647            let locator = Locator::new("div");
2648            let assertion = expect(locator).to_be_hidden();
2649            assert!(assertion.validate_state(true).is_ok());
2650        }
2651    }
2652
2653    // =========================================================================
2654    // Hâ‚€ EXTREME TDD: Auto-Waiting Tests (Spec G.1 P0)
2655    // =========================================================================
2656
2657    mod h0_auto_waiting_tests {
2658        use super::*;
2659
2660        #[test]
2661        fn h0_locator_01_default_timeout_is_5_seconds() {
2662            assert_eq!(DEFAULT_TIMEOUT_MS, 5000);
2663        }
2664
2665        #[test]
2666        fn h0_locator_02_default_poll_interval_is_50ms() {
2667            assert_eq!(DEFAULT_POLL_INTERVAL_MS, 50);
2668        }
2669
2670        #[test]
2671        fn h0_locator_03_locator_options_default_timeout() {
2672            let opts = LocatorOptions::default();
2673            assert_eq!(opts.timeout, Duration::from_millis(DEFAULT_TIMEOUT_MS));
2674        }
2675
2676        #[test]
2677        fn h0_locator_04_locator_options_default_strict_true() {
2678            let opts = LocatorOptions::default();
2679            assert!(opts.strict);
2680        }
2681
2682        #[test]
2683        fn h0_locator_05_locator_options_default_visible_true() {
2684            let opts = LocatorOptions::default();
2685            assert!(opts.visible);
2686        }
2687
2688        #[test]
2689        fn h0_locator_06_with_timeout_custom_value() {
2690            let locator = Locator::new("button").with_timeout(Duration::from_secs(30));
2691            assert_eq!(locator.options().timeout, Duration::from_secs(30));
2692        }
2693
2694        #[test]
2695        fn h0_locator_07_with_strict_false() {
2696            let locator = Locator::new("button").with_strict(false);
2697            assert!(!locator.options().strict);
2698        }
2699
2700        #[test]
2701        fn h0_locator_08_with_visible_false() {
2702            let locator = Locator::new("button").with_visible(false);
2703            assert!(!locator.options().visible);
2704        }
2705
2706        #[test]
2707        fn h0_locator_09_wait_for_visible_action() {
2708            let locator = Locator::new("button");
2709            let action = locator.wait_for_visible().unwrap();
2710            assert!(matches!(action, LocatorAction::WaitForVisible { .. }));
2711        }
2712
2713        #[test]
2714        fn h0_locator_10_wait_for_hidden_action() {
2715            let locator = Locator::new("button");
2716            let action = locator.wait_for_hidden().unwrap();
2717            assert!(matches!(action, LocatorAction::WaitForHidden { .. }));
2718        }
2719    }
2720
2721    // =========================================================================
2722    // Hâ‚€ EXTREME TDD: Semantic Locators (Spec G.1 Playwright Parity)
2723    // =========================================================================
2724
2725    mod h0_semantic_locator_tests {
2726        use super::*;
2727
2728        #[test]
2729        fn h0_locator_11_role_selector_button() {
2730            let selector = Selector::role("button");
2731            assert!(matches!(selector, Selector::Role { role, name: None } if role == "button"));
2732        }
2733
2734        #[test]
2735        fn h0_locator_12_role_selector_with_name() {
2736            let selector = Selector::role_with_name("button", "Submit");
2737            assert!(
2738                matches!(selector, Selector::Role { role, name: Some(n) } if role == "button" && n == "Submit")
2739            );
2740        }
2741
2742        #[test]
2743        fn h0_locator_13_label_selector() {
2744            let selector = Selector::label("Username");
2745            assert!(matches!(selector, Selector::Label(l) if l == "Username"));
2746        }
2747
2748        #[test]
2749        fn h0_locator_14_placeholder_selector() {
2750            let selector = Selector::placeholder("Enter email");
2751            assert!(matches!(selector, Selector::Placeholder(p) if p == "Enter email"));
2752        }
2753
2754        #[test]
2755        fn h0_locator_15_alt_text_selector() {
2756            let selector = Selector::alt_text("Logo image");
2757            assert!(matches!(selector, Selector::AltText(a) if a == "Logo image"));
2758        }
2759
2760        #[test]
2761        fn h0_locator_16_role_to_query() {
2762            let selector = Selector::role("button");
2763            let query = selector.to_query();
2764            assert!(query.contains("role") || query.contains("button"));
2765        }
2766
2767        #[test]
2768        fn h0_locator_17_label_to_query() {
2769            let selector = Selector::label("Email");
2770            let query = selector.to_query();
2771            assert!(query.contains("label") || query.contains("Email"));
2772        }
2773
2774        #[test]
2775        fn h0_locator_18_placeholder_to_query() {
2776            let selector = Selector::placeholder("Search");
2777            let query = selector.to_query();
2778            assert!(query.contains("placeholder") || query.contains("Search"));
2779        }
2780
2781        #[test]
2782        fn h0_locator_19_alt_text_to_query() {
2783            let selector = Selector::alt_text("Company Logo");
2784            let query = selector.to_query();
2785            assert!(query.contains("alt") || query.contains("Company Logo"));
2786        }
2787
2788        #[test]
2789        fn h0_locator_20_css_selector_factory() {
2790            let selector = Selector::css("div.container");
2791            assert!(matches!(selector, Selector::Css(s) if s == "div.container"));
2792        }
2793    }
2794
2795    // =========================================================================
2796    // Hâ‚€ EXTREME TDD: Expect Assertions (Spec G.1 Auto-Retry)
2797    // =========================================================================
2798
2799    mod h0_expect_assertion_tests {
2800        use super::*;
2801
2802        #[test]
2803        fn h0_locator_21_expect_to_have_text() {
2804            let locator = Locator::new("span");
2805            let assertion = expect(locator).to_have_text("Hello");
2806            assert!(matches!(assertion, ExpectAssertion::HasText { .. }));
2807        }
2808
2809        #[test]
2810        fn h0_locator_22_expect_to_contain_text() {
2811            let locator = Locator::new("span");
2812            let assertion = expect(locator).to_contain_text("ell");
2813            assert!(matches!(assertion, ExpectAssertion::ContainsText { .. }));
2814        }
2815
2816        #[test]
2817        fn h0_locator_23_expect_to_have_count() {
2818            let locator = Locator::new("li");
2819            let assertion = expect(locator).to_have_count(5);
2820            assert!(
2821                matches!(assertion, ExpectAssertion::HasCount { expected, .. } if expected == 5)
2822            );
2823        }
2824
2825        #[test]
2826        fn h0_locator_24_expect_to_be_visible() {
2827            let locator = Locator::new("button");
2828            let assertion = expect(locator).to_be_visible();
2829            assert!(matches!(assertion, ExpectAssertion::IsVisible { .. }));
2830        }
2831
2832        #[test]
2833        fn h0_locator_25_expect_to_be_hidden() {
2834            let locator = Locator::new("button");
2835            let assertion = expect(locator).to_be_hidden();
2836            assert!(matches!(assertion, ExpectAssertion::IsHidden { .. }));
2837        }
2838
2839        #[test]
2840        fn h0_locator_26_expect_to_be_enabled() {
2841            let locator = Locator::new("button");
2842            let assertion = expect(locator).to_be_enabled();
2843            assert!(matches!(assertion, ExpectAssertion::IsEnabled { .. }));
2844        }
2845
2846        #[test]
2847        fn h0_locator_27_expect_to_be_disabled() {
2848            let locator = Locator::new("button");
2849            let assertion = expect(locator).to_be_disabled();
2850            assert!(matches!(assertion, ExpectAssertion::IsDisabled { .. }));
2851        }
2852
2853        #[test]
2854        fn h0_locator_28_expect_to_be_checked() {
2855            let locator = Locator::new("input[type=checkbox]");
2856            let assertion = expect(locator).to_be_checked();
2857            assert!(matches!(assertion, ExpectAssertion::IsChecked { .. }));
2858        }
2859
2860        #[test]
2861        fn h0_locator_29_expect_to_have_value() {
2862            let locator = Locator::new("input");
2863            let assertion = expect(locator).to_have_value("test");
2864            assert!(matches!(assertion, ExpectAssertion::HasValue { .. }));
2865        }
2866
2867        #[test]
2868        fn h0_locator_30_expect_to_have_attribute() {
2869            let locator = Locator::new("input");
2870            let assertion = expect(locator).to_have_attribute("type", "email");
2871            assert!(matches!(assertion, ExpectAssertion::HasAttribute { .. }));
2872        }
2873    }
2874
2875    // =========================================================================
2876    // Hâ‚€ EXTREME TDD: Locator Actions (Spec G.1)
2877    // =========================================================================
2878
2879    mod h0_locator_action_tests {
2880        use super::*;
2881
2882        #[test]
2883        fn h0_locator_31_click_action() {
2884            let locator = Locator::new("button");
2885            let action = locator.click().unwrap();
2886            assert!(matches!(action, LocatorAction::Click { .. }));
2887        }
2888
2889        #[test]
2890        fn h0_locator_32_double_click_action() {
2891            let locator = Locator::new("button");
2892            let action = locator.double_click().unwrap();
2893            assert!(matches!(action, LocatorAction::DoubleClick { .. }));
2894        }
2895
2896        #[test]
2897        fn h0_locator_33_fill_action() {
2898            let locator = Locator::new("input");
2899            let action = locator.fill("hello").unwrap();
2900            assert!(matches!(action, LocatorAction::Fill { text, .. } if text == "hello"));
2901        }
2902
2903        #[test]
2904        fn h0_locator_34_hover_action() {
2905            let locator = Locator::new("button");
2906            let action = locator.hover().unwrap();
2907            assert!(matches!(action, LocatorAction::Hover { .. }));
2908        }
2909
2910        #[test]
2911        fn h0_locator_35_focus_action() {
2912            let locator = Locator::new("input");
2913            let action = locator.focus().unwrap();
2914            assert!(matches!(action, LocatorAction::Focus { .. }));
2915        }
2916
2917        #[test]
2918        fn h0_locator_36_drag_to_action() {
2919            let locator = Locator::new("div.draggable");
2920            let action = locator.drag_to(&Point::new(100.0, 200.0)).build();
2921            assert!(matches!(action, LocatorAction::Drag { .. }));
2922        }
2923
2924        #[test]
2925        fn h0_locator_37_drag_steps_custom() {
2926            let locator = Locator::new("div");
2927            let action = locator.drag_to(&Point::new(0.0, 0.0)).steps(25).build();
2928            assert!(matches!(action, LocatorAction::Drag { steps: 25, .. }));
2929        }
2930
2931        #[test]
2932        fn h0_locator_38_drag_duration_custom() {
2933            let locator = Locator::new("div");
2934            let action = locator
2935                .drag_to(&Point::new(0.0, 0.0))
2936                .duration(Duration::from_secs(2))
2937                .build();
2938            assert!(
2939                matches!(action, LocatorAction::Drag { duration, .. } if duration == Duration::from_secs(2))
2940            );
2941        }
2942
2943        #[test]
2944        fn h0_locator_39_text_content_query() {
2945            let locator = Locator::new("span");
2946            let query = locator.text_content().unwrap();
2947            assert!(matches!(query, LocatorQuery::TextContent { .. }));
2948        }
2949
2950        #[test]
2951        fn h0_locator_40_count_query() {
2952            let locator = Locator::new("li");
2953            let query = locator.count().unwrap();
2954            assert!(matches!(query, LocatorQuery::Count { .. }));
2955        }
2956    }
2957
2958    // =========================================================================
2959    // Hâ‚€ EXTREME TDD: BoundingBox and Point (Spec G.1)
2960    // =========================================================================
2961
2962    mod h0_geometry_tests {
2963        use super::*;
2964
2965        #[test]
2966        fn h0_locator_41_point_new() {
2967            let p = Point::new(10.5, 20.5);
2968            assert!((p.x - 10.5).abs() < f32::EPSILON);
2969            assert!((p.y - 20.5).abs() < f32::EPSILON);
2970        }
2971
2972        #[test]
2973        fn h0_locator_42_bounding_box_new() {
2974            let bbox = BoundingBox::new(10.0, 20.0, 100.0, 50.0);
2975            assert!((bbox.x - 10.0).abs() < f32::EPSILON);
2976            assert!((bbox.width - 100.0).abs() < f32::EPSILON);
2977        }
2978
2979        #[test]
2980        fn h0_locator_43_bounding_box_center() {
2981            let bbox = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
2982            let center = bbox.center();
2983            assert!((center.x - 50.0).abs() < f32::EPSILON);
2984            assert!((center.y - 50.0).abs() < f32::EPSILON);
2985        }
2986
2987        #[test]
2988        fn h0_locator_44_bounding_box_contains_inside() {
2989            let bbox = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
2990            assert!(bbox.contains(&Point::new(50.0, 50.0)));
2991        }
2992
2993        #[test]
2994        fn h0_locator_45_bounding_box_contains_outside() {
2995            let bbox = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
2996            assert!(!bbox.contains(&Point::new(150.0, 150.0)));
2997        }
2998
2999        #[test]
3000        fn h0_locator_46_bounding_box_contains_edge() {
3001            let bbox = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
3002            assert!(bbox.contains(&Point::new(0.0, 0.0)));
3003        }
3004
3005        #[test]
3006        fn h0_locator_47_drag_operation_default_steps() {
3007            let drag = DragOperation::to(Point::new(100.0, 100.0));
3008            assert_eq!(drag.steps, 10);
3009        }
3010
3011        #[test]
3012        fn h0_locator_48_drag_operation_default_duration() {
3013            let drag = DragOperation::to(Point::new(100.0, 100.0));
3014            assert_eq!(drag.duration, Duration::from_millis(500));
3015        }
3016
3017        #[test]
3018        fn h0_locator_49_locator_bounding_box_query() {
3019            let locator = Locator::new("div");
3020            let query = locator.bounding_box().unwrap();
3021            assert!(matches!(query, LocatorQuery::BoundingBox { .. }));
3022        }
3023
3024        #[test]
3025        fn h0_locator_50_locator_is_visible_query() {
3026            let locator = Locator::new("div");
3027            let query = locator.is_visible().unwrap();
3028            assert!(matches!(query, LocatorQuery::IsVisible { .. }));
3029        }
3030    }
3031
3032    // =========================================================================
3033    // Additional Coverage Tests: Edge Cases and Failure Paths
3034    // =========================================================================
3035
3036    mod coverage_edge_cases {
3037        use super::*;
3038
3039        // -------------------------------------------------------------------
3040        // validate_state failure cases
3041        // -------------------------------------------------------------------
3042
3043        #[test]
3044        fn test_validate_state_disabled_fail() {
3045            let locator = Locator::new("button");
3046            let assertion = expect(locator).to_be_disabled();
3047            let result = assertion.validate_state(false);
3048            assert!(result.is_err());
3049            let err = result.unwrap_err();
3050            assert!(err.to_string().contains("disabled"));
3051        }
3052
3053        #[test]
3054        fn test_validate_state_checked_fail() {
3055            let locator = Locator::new("input");
3056            let assertion = expect(locator).to_be_checked();
3057            let result = assertion.validate_state(false);
3058            assert!(result.is_err());
3059            let err = result.unwrap_err();
3060            assert!(err.to_string().contains("checked"));
3061        }
3062
3063        #[test]
3064        fn test_validate_state_editable_fail() {
3065            let locator = Locator::new("textarea");
3066            let assertion = expect(locator).to_be_editable();
3067            let result = assertion.validate_state(false);
3068            assert!(result.is_err());
3069            let err = result.unwrap_err();
3070            assert!(err.to_string().contains("editable"));
3071        }
3072
3073        #[test]
3074        fn test_validate_state_focused_fail() {
3075            let locator = Locator::new("input");
3076            let assertion = expect(locator).to_be_focused();
3077            let result = assertion.validate_state(false);
3078            assert!(result.is_err());
3079            let err = result.unwrap_err();
3080            assert!(err.to_string().contains("focused"));
3081        }
3082
3083        #[test]
3084        fn test_validate_state_empty_fail() {
3085            let locator = Locator::new("div");
3086            let assertion = expect(locator).to_be_empty();
3087            let result = assertion.validate_state(false);
3088            assert!(result.is_err());
3089            let err = result.unwrap_err();
3090            assert!(err.to_string().contains("empty"));
3091        }
3092
3093        #[test]
3094        fn test_validate_state_visible_fail() {
3095            let locator = Locator::new("div");
3096            let assertion = expect(locator).to_be_visible();
3097            let result = assertion.validate_state(false);
3098            assert!(result.is_err());
3099            let err = result.unwrap_err();
3100            assert!(err.to_string().contains("visible"));
3101        }
3102
3103        #[test]
3104        fn test_validate_state_hidden_fail() {
3105            let locator = Locator::new("div");
3106            let assertion = expect(locator).to_be_hidden();
3107            let result = assertion.validate_state(false);
3108            assert!(result.is_err());
3109            let err = result.unwrap_err();
3110            assert!(err.to_string().contains("hidden"));
3111        }
3112
3113        // -------------------------------------------------------------------
3114        // validate for non-state assertions with validate_state
3115        // -------------------------------------------------------------------
3116
3117        #[test]
3118        fn test_validate_state_non_state_assertion() {
3119            let locator = Locator::new("span");
3120            let assertion = expect(locator).to_have_text("test");
3121            // Non-state assertions should return Ok
3122            assert!(assertion.validate_state(true).is_ok());
3123            assert!(assertion.validate_state(false).is_ok());
3124        }
3125
3126        // -------------------------------------------------------------------
3127        // validate_count for non-count assertions
3128        // -------------------------------------------------------------------
3129
3130        #[test]
3131        fn test_validate_count_non_count_assertion() {
3132            let locator = Locator::new("span");
3133            let assertion = expect(locator).to_have_text("test");
3134            // Non-count assertions should return Ok
3135            assert!(assertion.validate_count(0).is_ok());
3136            assert!(assertion.validate_count(100).is_ok());
3137        }
3138
3139        // -------------------------------------------------------------------
3140        // validate contains_text failure
3141        // -------------------------------------------------------------------
3142
3143        #[test]
3144        fn test_validate_contains_text_fail() {
3145            let locator = Locator::new("span");
3146            let assertion = expect(locator).to_contain_text("needle");
3147            let result = assertion.validate("haystack without the word");
3148            assert!(result.is_err());
3149            let err = result.unwrap_err();
3150            assert!(err.to_string().contains("needle"));
3151        }
3152
3153        // -------------------------------------------------------------------
3154        // validate has_id failure
3155        // -------------------------------------------------------------------
3156
3157        #[test]
3158        fn test_validate_has_id_fail() {
3159            let locator = Locator::new("div");
3160            let assertion = expect(locator).to_have_id("expected-id");
3161            let result = assertion.validate("actual-id");
3162            assert!(result.is_err());
3163            let err = result.unwrap_err();
3164            assert!(err.to_string().contains("expected-id"));
3165        }
3166
3167        // -------------------------------------------------------------------
3168        // validate has_attribute failure
3169        // -------------------------------------------------------------------
3170
3171        #[test]
3172        fn test_validate_has_attribute_fail() {
3173            let locator = Locator::new("input");
3174            let assertion = expect(locator).to_have_attribute("type", "email");
3175            let result = assertion.validate("text");
3176            assert!(result.is_err());
3177            let err = result.unwrap_err();
3178            assert!(err.to_string().contains("email"));
3179            assert!(err.to_string().contains("type"));
3180        }
3181
3182        // -------------------------------------------------------------------
3183        // Non-CSS selector operations (and, or, first, last, nth)
3184        // -------------------------------------------------------------------
3185
3186        #[test]
3187        fn test_locator_or_non_css() {
3188            let locator1 = Locator::from_selector(Selector::Entity("hero".to_string()));
3189            let locator2 = Locator::new("div");
3190            let combined = locator1.or(locator2);
3191            // Should keep the original non-CSS selector
3192            assert!(matches!(combined.selector(), Selector::Entity(_)));
3193        }
3194
3195        #[test]
3196        fn test_locator_first_non_css() {
3197            let locator = Locator::from_selector(Selector::Entity("hero".to_string()));
3198            let result = locator.first();
3199            // Should keep the original non-CSS selector
3200            assert!(matches!(result.selector(), Selector::Entity(_)));
3201        }
3202
3203        #[test]
3204        fn test_locator_last_non_css() {
3205            let locator = Locator::from_selector(Selector::Entity("hero".to_string()));
3206            let result = locator.last();
3207            // Should keep the original non-CSS selector
3208            assert!(matches!(result.selector(), Selector::Entity(_)));
3209        }
3210
3211        #[test]
3212        fn test_locator_nth_non_css() {
3213            let locator = Locator::from_selector(Selector::Entity("hero".to_string()));
3214            let result = locator.nth(5);
3215            // Should keep the original non-CSS selector
3216            assert!(matches!(result.selector(), Selector::Entity(_)));
3217        }
3218
3219        // -------------------------------------------------------------------
3220        // Role selector with name - count query
3221        // -------------------------------------------------------------------
3222
3223        #[test]
3224        fn test_role_with_name_count_query() {
3225            let selector = Selector::role_with_name("button", "Submit");
3226            let query = selector.to_count_query();
3227            assert!(query.contains("role"));
3228            assert!(query.contains("Submit"));
3229            assert!(query.contains(".length"));
3230        }
3231
3232        // -------------------------------------------------------------------
3233        // Filter options without has_text
3234        // -------------------------------------------------------------------
3235
3236        #[test]
3237        fn test_filter_without_has_text() {
3238            let child = Locator::new(".child");
3239            let locator = Locator::new("div").filter(FilterOptions::new().has(child));
3240            // Without has_text, selector should remain unchanged
3241            assert!(matches!(locator.selector(), Selector::Css(_)));
3242        }
3243
3244        // -------------------------------------------------------------------
3245        // ClickOptions Default trait
3246        // -------------------------------------------------------------------
3247
3248        #[test]
3249        fn test_click_options_default_trait() {
3250            let options: ClickOptions = Default::default();
3251            assert_eq!(options.button, MouseButton::Left);
3252            assert_eq!(options.click_count, 0); // Default is 0, new() sets it to 1
3253        }
3254
3255        // -------------------------------------------------------------------
3256        // FilterOptions Default trait
3257        // -------------------------------------------------------------------
3258
3259        #[test]
3260        fn test_filter_options_default_trait() {
3261            let options: FilterOptions = Default::default();
3262            assert!(options.has.is_none());
3263            assert!(options.has_text.is_none());
3264            assert!(options.has_not.is_none());
3265            assert!(options.has_not_text.is_none());
3266        }
3267
3268        // -------------------------------------------------------------------
3269        // Point serialization (covered by derive)
3270        // -------------------------------------------------------------------
3271
3272        #[test]
3273        fn test_point_clone() {
3274            let p1 = Point::new(1.0, 2.0);
3275            let p2 = p1;
3276            assert!((p2.x - 1.0).abs() < f32::EPSILON);
3277            assert!((p2.y - 2.0).abs() < f32::EPSILON);
3278        }
3279
3280        #[test]
3281        fn test_point_partial_eq() {
3282            let p1 = Point::new(1.0, 2.0);
3283            let p2 = Point::new(1.0, 2.0);
3284            let p3 = Point::new(3.0, 4.0);
3285            assert_eq!(p1, p2);
3286            assert_ne!(p1, p3);
3287        }
3288
3289        // -------------------------------------------------------------------
3290        // BoundingBox serialization (covered by derive)
3291        // -------------------------------------------------------------------
3292
3293        #[test]
3294        fn test_bounding_box_clone() {
3295            let b1 = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
3296            let b2 = b1;
3297            assert!((b2.width - 100.0).abs() < f32::EPSILON);
3298        }
3299
3300        #[test]
3301        fn test_bounding_box_partial_eq() {
3302            let b1 = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
3303            let b2 = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
3304            let b3 = BoundingBox::new(1.0, 1.0, 100.0, 100.0);
3305            assert_eq!(b1, b2);
3306            assert_ne!(b1, b3);
3307        }
3308
3309        // -------------------------------------------------------------------
3310        // Selector equality
3311        // -------------------------------------------------------------------
3312
3313        #[test]
3314        fn test_selector_equality() {
3315            let s1 = Selector::css("button");
3316            let s2 = Selector::css("button");
3317            let s3 = Selector::css("div");
3318            assert_eq!(s1, s2);
3319            assert_ne!(s1, s3);
3320        }
3321
3322        #[test]
3323        fn test_selector_equality_css_with_text() {
3324            let s1 = Selector::CssWithText {
3325                css: "button".to_string(),
3326                text: "Click".to_string(),
3327            };
3328            let s2 = Selector::CssWithText {
3329                css: "button".to_string(),
3330                text: "Click".to_string(),
3331            };
3332            assert_eq!(s1, s2);
3333        }
3334
3335        #[test]
3336        fn test_selector_equality_role() {
3337            let s1 = Selector::Role {
3338                role: "button".to_string(),
3339                name: Some("Submit".to_string()),
3340            };
3341            let s2 = Selector::Role {
3342                role: "button".to_string(),
3343                name: Some("Submit".to_string()),
3344            };
3345            assert_eq!(s1, s2);
3346        }
3347
3348        // -------------------------------------------------------------------
3349        // DragBuilder chaining
3350        // -------------------------------------------------------------------
3351
3352        #[test]
3353        fn test_drag_builder_full_chain() {
3354            let locator = Locator::new("div");
3355            let action = locator
3356                .drag_to(&Point::new(100.0, 200.0))
3357                .steps(15)
3358                .duration(Duration::from_millis(750))
3359                .build();
3360
3361            match action {
3362                LocatorAction::Drag {
3363                    target,
3364                    steps,
3365                    duration,
3366                    ..
3367                } => {
3368                    assert!((target.x - 100.0).abs() < f32::EPSILON);
3369                    assert!((target.y - 200.0).abs() < f32::EPSILON);
3370                    assert_eq!(steps, 15);
3371                    assert_eq!(duration, Duration::from_millis(750));
3372                }
3373                _ => panic!("Expected Drag action"),
3374            }
3375        }
3376
3377        // -------------------------------------------------------------------
3378        // LocatorOptions fields
3379        // -------------------------------------------------------------------
3380
3381        #[test]
3382        fn test_locator_options_poll_interval() {
3383            let opts = LocatorOptions::default();
3384            assert_eq!(
3385                opts.poll_interval,
3386                Duration::from_millis(DEFAULT_POLL_INTERVAL_MS)
3387            );
3388        }
3389
3390        // -------------------------------------------------------------------
3391        // KeyModifier variants
3392        // -------------------------------------------------------------------
3393
3394        #[test]
3395        fn test_key_modifier_variants() {
3396            let modifiers = vec![
3397                KeyModifier::Alt,
3398                KeyModifier::Control,
3399                KeyModifier::Meta,
3400                KeyModifier::Shift,
3401            ];
3402            assert_eq!(modifiers.len(), 4);
3403
3404            // Test equality
3405            assert_eq!(KeyModifier::Alt, KeyModifier::Alt);
3406            assert_ne!(KeyModifier::Alt, KeyModifier::Control);
3407        }
3408
3409        // -------------------------------------------------------------------
3410        // MouseButton variants
3411        // -------------------------------------------------------------------
3412
3413        #[test]
3414        fn test_mouse_button_variants() {
3415            let buttons = vec![MouseButton::Left, MouseButton::Right, MouseButton::Middle];
3416            assert_eq!(buttons.len(), 3);
3417
3418            assert_eq!(MouseButton::Left, MouseButton::Left);
3419            assert_ne!(MouseButton::Left, MouseButton::Right);
3420        }
3421
3422        // -------------------------------------------------------------------
3423        // LocatorAction locator accessor for Drag variant
3424        // -------------------------------------------------------------------
3425
3426        #[test]
3427        fn test_locator_action_drag_locator_accessor() {
3428            let locator = Locator::new("div.draggable");
3429            let action = locator.drag_to(&Point::new(0.0, 0.0)).build();
3430            let accessed = action.locator();
3431            assert!(matches!(accessed.selector(), Selector::Css(_)));
3432        }
3433
3434        // -------------------------------------------------------------------
3435        // LocatorAction locator accessor for Fill variant
3436        // -------------------------------------------------------------------
3437
3438        #[test]
3439        fn test_locator_action_fill_locator_accessor() {
3440            let locator = Locator::new("input");
3441            let action = locator.fill("test").unwrap();
3442            let accessed = action.locator();
3443            assert!(matches!(accessed.selector(), Selector::Css(_)));
3444        }
3445
3446        // -------------------------------------------------------------------
3447        // validate for browser-context assertions
3448        // -------------------------------------------------------------------
3449
3450        #[test]
3451        fn test_validate_browser_context_assertions() {
3452            let locator = Locator::new("div");
3453
3454            // IsVisible - returns Ok for browser context
3455            let assertion = expect(locator.clone()).to_be_visible();
3456            assert!(assertion.validate("any").is_ok());
3457
3458            // IsHidden
3459            let assertion = expect(locator.clone()).to_be_hidden();
3460            assert!(assertion.validate("any").is_ok());
3461
3462            // HasCount
3463            let assertion = expect(locator.clone()).to_have_count(5);
3464            assert!(assertion.validate("any").is_ok());
3465
3466            // IsEnabled
3467            let assertion = expect(locator.clone()).to_be_enabled();
3468            assert!(assertion.validate("any").is_ok());
3469
3470            // IsDisabled
3471            let assertion = expect(locator.clone()).to_be_disabled();
3472            assert!(assertion.validate("any").is_ok());
3473
3474            // IsChecked
3475            let assertion = expect(locator.clone()).to_be_checked();
3476            assert!(assertion.validate("any").is_ok());
3477
3478            // IsEditable
3479            let assertion = expect(locator.clone()).to_be_editable();
3480            assert!(assertion.validate("any").is_ok());
3481
3482            // IsFocused
3483            let assertion = expect(locator.clone()).to_be_focused();
3484            assert!(assertion.validate("any").is_ok());
3485
3486            // IsEmpty
3487            let assertion = expect(locator.clone()).to_be_empty();
3488            assert!(assertion.validate("any").is_ok());
3489
3490            // HasCss
3491            let assertion = expect(locator).to_have_css("color", "red");
3492            assert!(assertion.validate("any").is_ok());
3493        }
3494
3495        // -------------------------------------------------------------------
3496        // Debug implementations (covered by derive)
3497        // -------------------------------------------------------------------
3498
3499        #[test]
3500        fn test_debug_implementations() {
3501            let point = Point::new(1.0, 2.0);
3502            let debug_str = format!("{:?}", point);
3503            assert!(debug_str.contains("Point"));
3504
3505            let bbox = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
3506            let debug_str = format!("{:?}", bbox);
3507            assert!(debug_str.contains("BoundingBox"));
3508
3509            let selector = Selector::css("div");
3510            let debug_str = format!("{:?}", selector);
3511            assert!(debug_str.contains("Css"));
3512
3513            let locator = Locator::new("button");
3514            let debug_str = format!("{:?}", locator);
3515            assert!(debug_str.contains("Locator"));
3516
3517            let options = LocatorOptions::default();
3518            let debug_str = format!("{:?}", options);
3519            assert!(debug_str.contains("LocatorOptions"));
3520
3521            let filter = FilterOptions::new();
3522            let debug_str = format!("{:?}", filter);
3523            assert!(debug_str.contains("FilterOptions"));
3524
3525            let click_opts = ClickOptions::new();
3526            let debug_str = format!("{:?}", click_opts);
3527            assert!(debug_str.contains("ClickOptions"));
3528
3529            let drag_op = DragOperation::to(Point::new(0.0, 0.0));
3530            let debug_str = format!("{:?}", drag_op);
3531            assert!(debug_str.contains("DragOperation"));
3532
3533            let drag_builder = Locator::new("div").drag_to(&Point::new(0.0, 0.0));
3534            let debug_str = format!("{:?}", drag_builder);
3535            assert!(debug_str.contains("DragBuilder"));
3536
3537            let action = Locator::new("button").click().unwrap();
3538            let debug_str = format!("{:?}", action);
3539            assert!(debug_str.contains("Click"));
3540
3541            let query = Locator::new("span").text_content().unwrap();
3542            let debug_str = format!("{:?}", query);
3543            assert!(debug_str.contains("TextContent"));
3544
3545            let exp = Expect::new(Locator::new("div"));
3546            let debug_str = format!("{:?}", exp);
3547            assert!(debug_str.contains("Expect"));
3548
3549            let assertion = expect(Locator::new("div")).to_have_text("test");
3550            let debug_str = format!("{:?}", assertion);
3551            assert!(debug_str.contains("HasText"));
3552        }
3553
3554        // -------------------------------------------------------------------
3555        // Clone implementations
3556        // -------------------------------------------------------------------
3557
3558        #[test]
3559        fn test_clone_implementations() {
3560            let locator = Locator::new("button");
3561            let cloned = locator;
3562            assert!(matches!(cloned.selector(), Selector::Css(_)));
3563
3564            let options = LocatorOptions::default();
3565            let cloned = options;
3566            assert!(cloned.strict);
3567
3568            let filter = FilterOptions::new().has_text("test");
3569            let cloned = filter;
3570            assert!(cloned.has_text.is_some());
3571
3572            let click_opts = ClickOptions::new().button(MouseButton::Right);
3573            let cloned = click_opts;
3574            assert_eq!(cloned.button, MouseButton::Right);
3575
3576            let drag_op = DragOperation::to(Point::new(1.0, 2.0)).steps(5);
3577            let cloned = drag_op;
3578            assert_eq!(cloned.steps, 5);
3579
3580            let drag_builder = Locator::new("div").drag_to(&Point::new(3.0, 4.0)).steps(7);
3581            let cloned = drag_builder;
3582            let action = cloned.build();
3583            assert!(matches!(action, LocatorAction::Drag { steps: 7, .. }));
3584
3585            let action = Locator::new("button").hover().unwrap();
3586            let cloned = action;
3587            assert!(matches!(cloned, LocatorAction::Hover { .. }));
3588
3589            let query = Locator::new("span").count().unwrap();
3590            let cloned = query;
3591            assert!(matches!(cloned, LocatorQuery::Count { .. }));
3592
3593            let exp = Expect::new(Locator::new("div"));
3594            let cloned = exp;
3595            let _ = cloned.to_be_visible();
3596
3597            let assertion = expect(Locator::new("div")).to_have_count(3);
3598            let cloned = assertion;
3599            assert!(matches!(cloned, ExpectAssertion::HasCount { .. }));
3600        }
3601
3602        // -------------------------------------------------------------------
3603        // Selector to_query edge cases
3604        // -------------------------------------------------------------------
3605
3606        #[test]
3607        fn test_selector_to_query_special_chars() {
3608            // Test CSS selector with special characters
3609            let selector = Selector::css(r#"div[data-value="test's value"]"#);
3610            let query = selector.to_query();
3611            assert!(query.contains("querySelector"));
3612
3613            // Test XPath with special characters
3614            let selector =
3615                Selector::XPath(r#"//button[contains(text(), "Click here")]"#.to_string());
3616            let query = selector.to_query();
3617            assert!(query.contains("evaluate"));
3618
3619            // Test TestId with special characters
3620            let selector = Selector::test_id("my-test-id_123");
3621            let query = selector.to_query();
3622            assert!(query.contains("data-testid"));
3623        }
3624
3625        // -------------------------------------------------------------------
3626        // BoundingBox contains edge cases
3627        // -------------------------------------------------------------------
3628
3629        #[test]
3630        fn test_bounding_box_contains_all_edges() {
3631            let bbox = BoundingBox::new(10.0, 20.0, 100.0, 50.0);
3632
3633            // Test all four corners
3634            assert!(bbox.contains(&Point::new(10.0, 20.0))); // top-left
3635            assert!(bbox.contains(&Point::new(110.0, 20.0))); // top-right
3636            assert!(bbox.contains(&Point::new(10.0, 70.0))); // bottom-left
3637            assert!(bbox.contains(&Point::new(110.0, 70.0))); // bottom-right
3638
3639            // Test just outside each edge
3640            assert!(!bbox.contains(&Point::new(9.9, 45.0))); // left
3641            assert!(!bbox.contains(&Point::new(110.1, 45.0))); // right
3642            assert!(!bbox.contains(&Point::new(55.0, 19.9))); // top
3643            assert!(!bbox.contains(&Point::new(55.0, 70.1))); // bottom
3644        }
3645
3646        // -------------------------------------------------------------------
3647        // BoundingBox center with offset
3648        // -------------------------------------------------------------------
3649
3650        #[test]
3651        fn test_bounding_box_center_with_offset() {
3652            let bbox = BoundingBox::new(10.0, 20.0, 100.0, 50.0);
3653            let center = bbox.center();
3654            assert!((center.x - 60.0).abs() < f32::EPSILON); // 10 + 100/2
3655            assert!((center.y - 45.0).abs() < f32::EPSILON); // 20 + 50/2
3656        }
3657
3658        // -------------------------------------------------------------------
3659        // Locator chaining
3660        // -------------------------------------------------------------------
3661
3662        #[test]
3663        fn test_locator_chaining_all_options() {
3664            let locator = Locator::new("button")
3665                .with_text("Click")
3666                .with_timeout(Duration::from_secs(10))
3667                .with_strict(false)
3668                .with_visible(false);
3669
3670            assert!(!locator.options().strict);
3671            assert!(!locator.options().visible);
3672            assert_eq!(locator.options().timeout, Duration::from_secs(10));
3673            assert!(matches!(locator.selector(), Selector::CssWithText { .. }));
3674        }
3675
3676        // -------------------------------------------------------------------
3677        // ClickOptions chaining
3678        // -------------------------------------------------------------------
3679
3680        #[test]
3681        fn test_click_options_full_chain() {
3682            let options = ClickOptions::new()
3683                .button(MouseButton::Middle)
3684                .click_count(3)
3685                .position(Point::new(5.0, 10.0))
3686                .modifier(KeyModifier::Shift)
3687                .modifier(KeyModifier::Alt)
3688                .modifier(KeyModifier::Control)
3689                .modifier(KeyModifier::Meta);
3690
3691            assert_eq!(options.button, MouseButton::Middle);
3692            assert_eq!(options.click_count, 3);
3693            assert!(options.position.is_some());
3694            let pos = options.position.unwrap();
3695            assert!((pos.x - 5.0).abs() < f32::EPSILON);
3696            assert!((pos.y - 10.0).abs() < f32::EPSILON);
3697            assert_eq!(options.modifiers.len(), 4);
3698        }
3699    }
3700}