Skip to main content

playwright_rs/protocol/
locator.rs

1//! Locator — lazy element selector with auto-waiting.
2//!
3//! Locators are the central piece of Playwright's auto-waiting and retry
4//! semantics. They represent *a way to find element(s)* at any given
5//! moment — not an element handle. Each action re-queries.
6//!
7//! Key characteristics:
8//! - Lazy: don't execute until an action is performed
9//! - Retryable: auto-wait for elements to match actionability checks
10//! - Chainable: can create sub-locators via `first()`, `last()`,
11//!   `nth()`, `locator()`, `filter()`
12//!
13//! Architecture:
14//! - Locator is **not** a ChannelOwner; it's a lightweight wrapper
15//! - Stores a selector string + reference to its Frame + parent Page
16//! - Delegates all operations to Frame with `strict=true`
17//!
18//! # Example
19//!
20//! ```ignore
21//! use playwright_rs::Playwright;
22//!
23//! #[tokio::main]
24//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
25//!     let pw = Playwright::launch().await?;
26//!     let browser = pw.chromium().launch().await?;
27//!     let page = browser.new_page().await?;
28//!
29//!     page.set_content(
30//!         r#"<button data-testid="submit" role="button">Submit</button>
31//!            <ul><li class="item">A</li><li class="item">B</li></ul>"#,
32//!         None,
33//!     ).await?;
34//!
35//!     // Basic locator + action
36//!     page.locator("button").await.click(None).await?;
37//!
38//!     // Robust locator from a fragile starting point: normalize() asks
39//!     // Playwright for the canonical equivalent (test-id / role / text).
40//!     let stable = page
41//!         .locator("body button:nth-child(1)")
42//!         .await
43//!         .normalize()
44//!         .await?;
45//!     assert!(!stable.selector().is_empty());
46//!
47//!     // Chain primitives: filter, count, nth
48//!     let items = page.locator(".item").await;
49//!     assert_eq!(items.count().await?, 2);
50//!     items.nth(0).click(None).await?;
51//!
52//!     browser.close().await?;
53//!     Ok(())
54//! }
55//! ```
56//!
57//! See: <https://playwright.dev/docs/api/class-locator>
58
59use crate::error::Result;
60use crate::protocol::Frame;
61use serde::Deserialize;
62
63/// Trait for action option structs that have an optional timeout field.
64/// Used by `Locator::with_timeout` to inject the page's default timeout.
65pub(crate) trait HasTimeout {
66    fn timeout_ref(&self) -> &Option<f64>;
67    fn timeout_ref_mut(&mut self) -> &mut Option<f64>;
68}
69
70macro_rules! impl_has_timeout {
71    ($($ty:ty),+ $(,)?) => {
72        $(impl HasTimeout for $ty {
73            fn timeout_ref(&self) -> &Option<f64> { &self.timeout }
74            fn timeout_ref_mut(&mut self) -> &mut Option<f64> { &mut self.timeout }
75        })+
76    };
77}
78
79impl_has_timeout!(
80    crate::protocol::ClickOptions,
81    crate::protocol::FillOptions,
82    crate::protocol::PressOptions,
83    crate::protocol::CheckOptions,
84    crate::protocol::HoverOptions,
85    crate::protocol::SelectOptions,
86    crate::protocol::ScreenshotOptions,
87    crate::protocol::TapOptions,
88    crate::protocol::DragToOptions,
89    crate::protocol::WaitForOptions,
90);
91use std::sync::Arc;
92
93/// The bounding box of an element in pixels.
94///
95/// All values are measured relative to the top-left corner of the page.
96///
97/// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
98#[derive(Debug, Clone, PartialEq, Deserialize)]
99pub struct BoundingBox {
100    /// The x coordinate of the top-left corner of the element in pixels.
101    pub x: f64,
102    /// The y coordinate of the top-left corner of the element in pixels.
103    pub y: f64,
104    /// The width of the element in pixels.
105    pub width: f64,
106    /// The height of the element in pixels.
107    pub height: f64,
108}
109
110/// Escapes text for use in Playwright's internal selector engine.
111///
112/// JSON-stringifies the text and appends `i` (case-insensitive) or `s` (strict/exact).
113/// Matches the `escapeForTextSelector`/`escapeForAttributeSelector` in Playwright TypeScript.
114fn escape_for_selector(text: &str, exact: bool) -> String {
115    let suffix = if exact { "s" } else { "i" };
116    let escaped = serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text));
117    format!("{}{}", escaped, suffix)
118}
119
120/// Builds the internal selector string for `get_by_text`.
121///
122/// - `exact=false` → `internal:text="text"i` (case-insensitive substring)
123/// - `exact=true` → `internal:text="text"s` (case-sensitive exact)
124pub(crate) fn get_by_text_selector(text: &str, exact: bool) -> String {
125    format!("internal:text={}", escape_for_selector(text, exact))
126}
127
128/// Builds the internal selector string for `get_by_label`.
129///
130/// - `exact=false` → `internal:label="text"i`
131/// - `exact=true` → `internal:label="text"s`
132pub(crate) fn get_by_label_selector(text: &str, exact: bool) -> String {
133    format!("internal:label={}", escape_for_selector(text, exact))
134}
135
136/// Builds the internal selector string for `get_by_placeholder`.
137///
138/// - `exact=false` → `internal:attr=[placeholder="text"i]`
139/// - `exact=true` → `internal:attr=[placeholder="text"s]`
140pub(crate) fn get_by_placeholder_selector(text: &str, exact: bool) -> String {
141    format!(
142        "internal:attr=[placeholder={}]",
143        escape_for_selector(text, exact)
144    )
145}
146
147/// Builds the internal selector string for `get_by_alt_text`.
148///
149/// - `exact=false` → `internal:attr=[alt="text"i]`
150/// - `exact=true` → `internal:attr=[alt="text"s]`
151pub(crate) fn get_by_alt_text_selector(text: &str, exact: bool) -> String {
152    format!("internal:attr=[alt={}]", escape_for_selector(text, exact))
153}
154
155/// Builds the internal selector string for `get_by_title`.
156///
157/// - `exact=false` → `internal:attr=[title="text"i]`
158/// - `exact=true` → `internal:attr=[title="text"s]`
159pub(crate) fn get_by_title_selector(text: &str, exact: bool) -> String {
160    format!("internal:attr=[title={}]", escape_for_selector(text, exact))
161}
162
163/// Builds the internal selector string for `get_by_test_id`.
164///
165/// Uses `data-testid` attribute by default (matching Playwright's default).
166/// Always uses exact matching (`s` suffix).
167pub(crate) fn get_by_test_id_selector(test_id: &str) -> String {
168    get_by_test_id_selector_with_attr(test_id, "data-testid")
169}
170
171/// Builds the internal selector string for `get_by_test_id` with a custom attribute.
172///
173/// Used when `playwright.selectors().set_test_id_attribute()` has been called.
174pub(crate) fn get_by_test_id_selector_with_attr(test_id: &str, attribute: &str) -> String {
175    format!(
176        "internal:testid=[{}={}]",
177        attribute,
178        escape_for_selector(test_id, true)
179    )
180}
181
182/// Escapes text for use in Playwright's attribute role selector.
183///
184/// Unlike `escape_for_selector` (which uses JSON encoding), this only escapes
185/// backslashes and double quotes, matching Playwright's `escapeForAttributeSelector`.
186fn escape_for_attribute_selector(text: &str, exact: bool) -> String {
187    let suffix = if exact { "s" } else { "i" };
188    let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
189    format!("\"{}\"{}", escaped, suffix)
190}
191
192/// Builds the internal selector string for `get_by_role`.
193///
194/// Format: `internal:role=<role>[prop1=val1][prop2=val2]...`
195///
196/// Properties are appended in Playwright's required order:
197/// checked, disabled, selected, expanded, include-hidden, level, name, pressed.
198pub(crate) fn get_by_role_selector(role: AriaRole, options: Option<GetByRoleOptions>) -> String {
199    let mut selector = format!("internal:role={}", role.as_str());
200
201    if let Some(opts) = options {
202        if let Some(checked) = opts.checked {
203            selector.push_str(&format!("[checked={}]", checked));
204        }
205        if let Some(disabled) = opts.disabled {
206            selector.push_str(&format!("[disabled={}]", disabled));
207        }
208        if let Some(selected) = opts.selected {
209            selector.push_str(&format!("[selected={}]", selected));
210        }
211        if let Some(expanded) = opts.expanded {
212            selector.push_str(&format!("[expanded={}]", expanded));
213        }
214        if let Some(include_hidden) = opts.include_hidden {
215            selector.push_str(&format!("[include-hidden={}]", include_hidden));
216        }
217        if let Some(level) = opts.level {
218            selector.push_str(&format!("[level={}]", level));
219        }
220        if let Some(name) = &opts.name {
221            let exact = opts.exact.unwrap_or(false);
222            selector.push_str(&format!(
223                "[name={}]",
224                escape_for_attribute_selector(name, exact)
225            ));
226        }
227        if let Some(pressed) = opts.pressed {
228            selector.push_str(&format!("[pressed={}]", pressed));
229        }
230    }
231
232    selector
233}
234
235/// ARIA roles for `get_by_role()` locator.
236///
237/// Represents WAI-ARIA roles used to locate elements by their accessibility role.
238/// Matches Playwright's `AriaRole` enum across all language bindings.
239///
240/// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum AriaRole {
243    Alert,
244    Alertdialog,
245    Application,
246    Article,
247    Banner,
248    Blockquote,
249    Button,
250    Caption,
251    Cell,
252    Checkbox,
253    Code,
254    Columnheader,
255    Combobox,
256    Complementary,
257    Contentinfo,
258    Definition,
259    Deletion,
260    Dialog,
261    Directory,
262    Document,
263    Emphasis,
264    Feed,
265    Figure,
266    Form,
267    Generic,
268    Grid,
269    Gridcell,
270    Group,
271    Heading,
272    Img,
273    Insertion,
274    Link,
275    List,
276    Listbox,
277    Listitem,
278    Log,
279    Main,
280    Marquee,
281    Math,
282    Meter,
283    Menu,
284    Menubar,
285    Menuitem,
286    Menuitemcheckbox,
287    Menuitemradio,
288    Navigation,
289    None,
290    Note,
291    Option,
292    Paragraph,
293    Presentation,
294    Progressbar,
295    Radio,
296    Radiogroup,
297    Region,
298    Row,
299    Rowgroup,
300    Rowheader,
301    Scrollbar,
302    Search,
303    Searchbox,
304    Separator,
305    Slider,
306    Spinbutton,
307    Status,
308    Strong,
309    Subscript,
310    Superscript,
311    Switch,
312    Tab,
313    Table,
314    Tablist,
315    Tabpanel,
316    Term,
317    Textbox,
318    Time,
319    Timer,
320    Toolbar,
321    Tooltip,
322    Tree,
323    Treegrid,
324    Treeitem,
325}
326
327impl AriaRole {
328    /// Returns the lowercase string representation used in selectors.
329    pub fn as_str(&self) -> &'static str {
330        match self {
331            Self::Alert => "alert",
332            Self::Alertdialog => "alertdialog",
333            Self::Application => "application",
334            Self::Article => "article",
335            Self::Banner => "banner",
336            Self::Blockquote => "blockquote",
337            Self::Button => "button",
338            Self::Caption => "caption",
339            Self::Cell => "cell",
340            Self::Checkbox => "checkbox",
341            Self::Code => "code",
342            Self::Columnheader => "columnheader",
343            Self::Combobox => "combobox",
344            Self::Complementary => "complementary",
345            Self::Contentinfo => "contentinfo",
346            Self::Definition => "definition",
347            Self::Deletion => "deletion",
348            Self::Dialog => "dialog",
349            Self::Directory => "directory",
350            Self::Document => "document",
351            Self::Emphasis => "emphasis",
352            Self::Feed => "feed",
353            Self::Figure => "figure",
354            Self::Form => "form",
355            Self::Generic => "generic",
356            Self::Grid => "grid",
357            Self::Gridcell => "gridcell",
358            Self::Group => "group",
359            Self::Heading => "heading",
360            Self::Img => "img",
361            Self::Insertion => "insertion",
362            Self::Link => "link",
363            Self::List => "list",
364            Self::Listbox => "listbox",
365            Self::Listitem => "listitem",
366            Self::Log => "log",
367            Self::Main => "main",
368            Self::Marquee => "marquee",
369            Self::Math => "math",
370            Self::Meter => "meter",
371            Self::Menu => "menu",
372            Self::Menubar => "menubar",
373            Self::Menuitem => "menuitem",
374            Self::Menuitemcheckbox => "menuitemcheckbox",
375            Self::Menuitemradio => "menuitemradio",
376            Self::Navigation => "navigation",
377            Self::None => "none",
378            Self::Note => "note",
379            Self::Option => "option",
380            Self::Paragraph => "paragraph",
381            Self::Presentation => "presentation",
382            Self::Progressbar => "progressbar",
383            Self::Radio => "radio",
384            Self::Radiogroup => "radiogroup",
385            Self::Region => "region",
386            Self::Row => "row",
387            Self::Rowgroup => "rowgroup",
388            Self::Rowheader => "rowheader",
389            Self::Scrollbar => "scrollbar",
390            Self::Search => "search",
391            Self::Searchbox => "searchbox",
392            Self::Separator => "separator",
393            Self::Slider => "slider",
394            Self::Spinbutton => "spinbutton",
395            Self::Status => "status",
396            Self::Strong => "strong",
397            Self::Subscript => "subscript",
398            Self::Superscript => "superscript",
399            Self::Switch => "switch",
400            Self::Tab => "tab",
401            Self::Table => "table",
402            Self::Tablist => "tablist",
403            Self::Tabpanel => "tabpanel",
404            Self::Term => "term",
405            Self::Textbox => "textbox",
406            Self::Time => "time",
407            Self::Timer => "timer",
408            Self::Toolbar => "toolbar",
409            Self::Tooltip => "tooltip",
410            Self::Tree => "tree",
411            Self::Treegrid => "treegrid",
412            Self::Treeitem => "treeitem",
413        }
414    }
415}
416
417/// Options for `get_by_role()` locator.
418///
419/// All fields are optional. When not specified, the property is not included
420/// in the role selector, meaning it matches any value.
421///
422/// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
423#[derive(Debug, Clone, Default)]
424pub struct GetByRoleOptions {
425    /// Whether the element is checked (for checkboxes, radio buttons).
426    pub checked: Option<bool>,
427    /// Whether the element is disabled.
428    pub disabled: Option<bool>,
429    /// Whether the element is selected (for options).
430    pub selected: Option<bool>,
431    /// Whether the element is expanded (for tree items, comboboxes).
432    pub expanded: Option<bool>,
433    /// Whether to include hidden elements.
434    pub include_hidden: Option<bool>,
435    /// The heading level (1-6, for heading role).
436    pub level: Option<u32>,
437    /// The accessible name of the element.
438    pub name: Option<String>,
439    /// Whether `name` matching is exact (case-sensitive, full-string).
440    /// Default is false (case-insensitive substring).
441    pub exact: Option<bool>,
442    /// Whether the element is pressed (for toggle buttons).
443    pub pressed: Option<bool>,
444}
445
446/// Options for [`Locator::filter()`].
447///
448/// Narrows an existing locator according to the specified criteria.
449/// All fields are optional; unset fields are ignored.
450///
451/// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
452#[derive(Debug, Clone, Default)]
453pub struct FilterOptions {
454    /// Matches elements containing the specified text (case-insensitive substring by default).
455    pub has_text: Option<String>,
456    /// Matches elements that do **not** contain the specified text anywhere inside.
457    pub has_not_text: Option<String>,
458    /// Narrows to elements that contain a descendant matching this locator.
459    ///
460    /// The inner locator is queried relative to the outer locator's matched element,
461    /// not the document root.
462    pub has: Option<Locator>,
463    /// Narrows to elements that do **not** contain a descendant matching this locator.
464    pub has_not: Option<Locator>,
465}
466
467/// Locator represents a way to find element(s) on the page at any given moment.
468///
469/// Locators are lazy - they don't execute queries until an action is performed.
470/// This enables auto-waiting and retry-ability for robust test automation.
471///
472/// # Examples
473///
474/// ```ignore
475/// use playwright_rs::protocol::{Playwright, SelectOption};
476///
477/// #[tokio::main]
478/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
479///     let playwright = Playwright::launch().await?;
480///     let browser = playwright.chromium().launch().await?;
481///     let page = browser.new_page().await?;
482///
483///     // Demonstrate set_checked() - checkbox interaction
484///     let _ = page.goto(
485///         "data:text/html,<input type='checkbox' id='cb'>",
486///         None
487///     ).await;
488///     let checkbox = page.locator("#cb").await;
489///     checkbox.set_checked(true, None).await?;
490///     assert!(checkbox.is_checked().await?);
491///     checkbox.set_checked(false, None).await?;
492///     assert!(!checkbox.is_checked().await?);
493///
494///     // Demonstrate select_option() - select by value, label, and index
495///     let _ = page.goto(
496///         "data:text/html,<select id='fruits'>\
497///             <option value='apple'>Apple</option>\
498///             <option value='banana'>Banana</option>\
499///             <option value='cherry'>Cherry</option>\
500///         </select>",
501///         None
502///     ).await;
503///     let select = page.locator("#fruits").await;
504///     select.select_option("banana", None).await?;
505///     assert_eq!(select.input_value(None).await?, "banana");
506///     select.select_option(SelectOption::Label("Apple".to_string()), None).await?;
507///     assert_eq!(select.input_value(None).await?, "apple");
508///     select.select_option(SelectOption::Index(2), None).await?;
509///     assert_eq!(select.input_value(None).await?, "cherry");
510///
511///     // Demonstrate select_option_multiple() - multi-select
512///     let _ = page.goto(
513///         "data:text/html,<select id='colors' multiple>\
514///             <option value='red'>Red</option>\
515///             <option value='green'>Green</option>\
516///             <option value='blue'>Blue</option>\
517///             <option value='yellow'>Yellow</option>\
518///         </select>",
519///         None
520///     ).await;
521///     let multi = page.locator("#colors").await;
522///     let selected = multi.select_option_multiple(&["red", "blue"], None).await?;
523///     assert_eq!(selected.len(), 2);
524///     assert!(selected.contains(&"red".to_string()));
525///     assert!(selected.contains(&"blue".to_string()));
526///
527///     // Demonstrate get_by_text() - find elements by text content
528///     let _ = page.goto(
529///         "data:text/html,<button>Submit</button><button>Submit Order</button>",
530///         None
531///     ).await;
532///     let all_submits = page.get_by_text("Submit", false).await;
533///     assert_eq!(all_submits.count().await?, 2); // case-insensitive substring
534///     let exact_submit = page.get_by_text("Submit", true).await;
535///     assert_eq!(exact_submit.count().await?, 1); // exact match only
536///
537///     // Demonstrate get_by_label, get_by_placeholder, get_by_test_id
538///     let _ = page.goto(
539///         "data:text/html,<label for='email'>Email</label>\
540///             <input id='email' placeholder='you@example.com' data-testid='email-input' />",
541///         None
542///     ).await;
543///     let by_label = page.get_by_label("Email", false).await;
544///     assert_eq!(by_label.count().await?, 1);
545///     let by_placeholder = page.get_by_placeholder("you@example.com", true).await;
546///     assert_eq!(by_placeholder.count().await?, 1);
547///     let by_test_id = page.get_by_test_id("email-input").await;
548///     assert_eq!(by_test_id.count().await?, 1);
549///
550///     // Demonstrate screenshot() - element screenshot
551///     let _ = page.goto(
552///         "data:text/html,<h1 id='title'>Hello World</h1>",
553///         None
554///     ).await;
555///     let heading = page.locator("#title").await;
556///     let screenshot = heading.screenshot(None).await?;
557///     assert!(!screenshot.is_empty());
558///
559///     browser.close().await?;
560///     Ok(())
561/// }
562/// ```
563///
564/// See: <https://playwright.dev/docs/api/class-locator>
565#[derive(Clone)]
566pub struct Locator {
567    frame: Arc<Frame>,
568    selector: String,
569    page: crate::protocol::Page,
570}
571
572impl Locator {
573    /// Creates a new Locator (internal use only)
574    ///
575    /// Use `page.locator()` or `frame.locator()` to create locators in application code.
576    pub(crate) fn new(frame: Arc<Frame>, selector: String, page: crate::protocol::Page) -> Self {
577        Self {
578            frame,
579            selector,
580            page,
581        }
582    }
583
584    /// Returns the selector string for this locator
585    pub fn selector(&self) -> &str {
586        &self.selector
587    }
588
589    /// Returns the underlying frame for this locator (crate-internal use only).
590    pub(crate) fn frame(&self) -> &Arc<Frame> {
591        &self.frame
592    }
593
594    /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) scoped within this locator's subtree.
595    ///
596    /// The `selector` identifies an iframe element within the locator's scope.
597    ///
598    /// See: <https://playwright.dev/docs/api/class-locator#locator-frame-locator>
599    pub fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
600        crate::protocol::FrameLocator::new(
601            Arc::clone(&self.frame),
602            format!("{} >> {}", self.selector, selector),
603            self.page.clone(),
604        )
605    }
606
607    /// Returns the Page this locator belongs to.
608    ///
609    /// Each locator is bound to the page that created it. Chained locators (via
610    /// `first()`, `last()`, `nth()`, `locator()`, `filter()`, etc.) all return
611    /// the same owning page. This matches the behavior of `locator.page` in
612    /// other Playwright language bindings.
613    ///
614    /// # Example
615    ///
616    /// ```ignore
617    /// # use playwright_rs::Playwright;
618    /// # #[tokio::main]
619    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
620    /// let playwright = Playwright::launch().await?;
621    /// let browser = playwright.chromium().launch().await?;
622    /// let page = browser.new_page().await?;
623    /// page.goto("https://example.com", None).await?;
624    ///
625    /// let locator = page.locator("h1").await;
626    /// let locator_page = locator.page()?;
627    /// assert_eq!(locator_page.url(), page.url());
628    /// # Ok(())
629    /// # }
630    /// ```
631    ///
632    /// See: <https://playwright.dev/docs/api/class-locator#locator-page>
633    pub fn page(&self) -> Result<crate::protocol::Page> {
634        Ok(self.page.clone())
635    }
636
637    /// Evaluate a JavaScript expression in the frame context.
638    ///
639    /// Used internally for injecting CSS (e.g., disabling animations) before screenshot assertions.
640    #[cfg(feature = "screenshot-diff")]
641    pub(crate) async fn evaluate_js<T: serde::Serialize>(
642        &self,
643        expression: &str,
644        _arg: Option<T>,
645    ) -> Result<()> {
646        self.frame
647            .frame_evaluate_expression(expression)
648            .await
649            .map_err(|e| self.wrap_error_with_selector(e))
650    }
651
652    /// Creates a locator for the first matching element.
653    ///
654    /// See: <https://playwright.dev/docs/api/class-locator#locator-first>
655    pub fn first(&self) -> Locator {
656        Locator::new(
657            Arc::clone(&self.frame),
658            format!("{} >> nth=0", self.selector),
659            self.page.clone(),
660        )
661    }
662
663    /// Creates a locator for the last matching element.
664    ///
665    /// See: <https://playwright.dev/docs/api/class-locator#locator-last>
666    pub fn last(&self) -> Locator {
667        Locator::new(
668            Arc::clone(&self.frame),
669            format!("{} >> nth=-1", self.selector),
670            self.page.clone(),
671        )
672    }
673
674    /// Creates a locator for the nth matching element (0-indexed).
675    ///
676    /// See: <https://playwright.dev/docs/api/class-locator#locator-nth>
677    pub fn nth(&self, index: i32) -> Locator {
678        Locator::new(
679            Arc::clone(&self.frame),
680            format!("{} >> nth={}", self.selector, index),
681            self.page.clone(),
682        )
683    }
684
685    /// Returns a locator that matches elements containing the given text.
686    ///
687    /// By default, matching is case-insensitive and searches for a substring.
688    /// Set `exact` to `true` for case-sensitive exact matching.
689    ///
690    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-text>
691    pub fn get_by_text(&self, text: &str, exact: bool) -> Locator {
692        self.locator(&get_by_text_selector(text, exact))
693    }
694
695    /// Returns a locator that matches elements by their associated label text.
696    ///
697    /// Targets form controls (`input`, `textarea`, `select`) linked via `<label>`,
698    /// `aria-label`, or `aria-labelledby`.
699    ///
700    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-label>
701    pub fn get_by_label(&self, text: &str, exact: bool) -> Locator {
702        self.locator(&get_by_label_selector(text, exact))
703    }
704
705    /// Returns a locator that matches elements by their placeholder text.
706    ///
707    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-placeholder>
708    pub fn get_by_placeholder(&self, text: &str, exact: bool) -> Locator {
709        self.locator(&get_by_placeholder_selector(text, exact))
710    }
711
712    /// Returns a locator that matches elements by their alt text.
713    ///
714    /// Typically used for `<img>` elements.
715    ///
716    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-alt-text>
717    pub fn get_by_alt_text(&self, text: &str, exact: bool) -> Locator {
718        self.locator(&get_by_alt_text_selector(text, exact))
719    }
720
721    /// Returns a locator that matches elements by their title attribute.
722    ///
723    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-title>
724    pub fn get_by_title(&self, text: &str, exact: bool) -> Locator {
725        self.locator(&get_by_title_selector(text, exact))
726    }
727
728    /// Returns a locator that matches elements by their test ID attribute.
729    ///
730    /// By default, uses the `data-testid` attribute. Call
731    /// `playwright.selectors().set_test_id_attribute()` to change the attribute name.
732    ///
733    /// Always uses exact matching (case-sensitive).
734    ///
735    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-test-id>
736    pub fn get_by_test_id(&self, test_id: &str) -> Locator {
737        use crate::server::channel_owner::ChannelOwner as _;
738        let attr = self.frame.connection().selectors().test_id_attribute();
739        self.locator(&get_by_test_id_selector_with_attr(test_id, &attr))
740    }
741
742    /// Returns a locator that matches elements by their ARIA role.
743    ///
744    /// This is the recommended way to locate elements, as it matches the way
745    /// users and assistive technology perceive the page.
746    ///
747    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-role>
748    pub fn get_by_role(&self, role: AriaRole, options: Option<GetByRoleOptions>) -> Locator {
749        self.locator(&get_by_role_selector(role, options))
750    }
751
752    /// Creates a sub-locator within this locator's subtree.
753    ///
754    /// See: <https://playwright.dev/docs/api/class-locator#locator-locator>
755    pub fn locator(&self, selector: &str) -> Locator {
756        Locator::new(
757            Arc::clone(&self.frame),
758            format!("{} >> {}", self.selector, selector),
759            self.page.clone(),
760        )
761    }
762
763    /// Narrows this locator according to the filter options.
764    ///
765    /// Can be chained to apply multiple filters in sequence.
766    ///
767    /// # Example
768    ///
769    /// ```ignore
770    /// use playwright_rs::{Playwright, FilterOptions};
771    ///
772    /// # #[tokio::main]
773    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
774    /// let playwright = Playwright::launch().await?;
775    /// let browser = playwright.chromium().launch().await?;
776    /// let page = browser.new_page().await?;
777    ///
778    /// // Filter rows to those containing "Apple"
779    /// let rows = page.locator("tr").await;
780    /// let apple_row = rows.filter(FilterOptions {
781    ///     has_text: Some("Apple".to_string()),
782    ///     ..Default::default()
783    /// });
784    /// # browser.close().await?;
785    /// # Ok(())
786    /// # }
787    /// ```
788    ///
789    /// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
790    pub fn filter(&self, options: FilterOptions) -> Locator {
791        let mut selector = self.selector.clone();
792
793        if let Some(text) = &options.has_text {
794            let escaped = escape_for_selector(text, false);
795            selector = format!("{} >> internal:has-text={}", selector, escaped);
796        }
797
798        if let Some(text) = &options.has_not_text {
799            let escaped = escape_for_selector(text, false);
800            selector = format!("{} >> internal:has-not-text={}", selector, escaped);
801        }
802
803        if let Some(locator) = &options.has {
804            let inner = serde_json::to_string(&locator.selector)
805                .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
806            selector = format!("{} >> internal:has={}", selector, inner);
807        }
808
809        if let Some(locator) = &options.has_not {
810            let inner = serde_json::to_string(&locator.selector)
811                .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
812            selector = format!("{} >> internal:has-not={}", selector, inner);
813        }
814
815        Locator::new(Arc::clone(&self.frame), selector, self.page.clone())
816    }
817
818    /// Creates a locator matching elements that satisfy **both** this locator and `locator`.
819    ///
820    /// Note: named `and_` because `and` is a Rust keyword.
821    ///
822    /// # Example
823    ///
824    /// ```ignore
825    /// use playwright_rs::Playwright;
826    ///
827    /// # #[tokio::main]
828    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
829    /// let playwright = Playwright::launch().await?;
830    /// let browser = playwright.chromium().launch().await?;
831    /// let page = browser.new_page().await?;
832    ///
833    /// // Find a button that also has a specific title
834    /// let button = page.locator("button").await;
835    /// let titled = page.locator("[title='Subscribe']").await;
836    /// let subscribe_btn = button.and_(&titled);
837    /// # browser.close().await?;
838    /// # Ok(())
839    /// # }
840    /// ```
841    ///
842    /// See: <https://playwright.dev/docs/api/class-locator#locator-and>
843    pub fn and_(&self, locator: &Locator) -> Locator {
844        let inner = serde_json::to_string(&locator.selector)
845            .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
846        Locator::new(
847            Arc::clone(&self.frame),
848            format!("{} >> internal:and={}", self.selector, inner),
849            self.page.clone(),
850        )
851    }
852
853    /// Creates a locator matching elements that satisfy **either** this locator or `locator`.
854    ///
855    /// Note: named `or_` because `or` is a Rust keyword.
856    ///
857    /// # Example
858    ///
859    /// ```ignore
860    /// use playwright_rs::Playwright;
861    ///
862    /// # #[tokio::main]
863    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
864    /// let playwright = Playwright::launch().await?;
865    /// let browser = playwright.chromium().launch().await?;
866    /// let page = browser.new_page().await?;
867    ///
868    /// // Find any element that is either a button or a link
869    /// let buttons = page.locator("button").await;
870    /// let links = page.locator("a").await;
871    /// let interactive = buttons.or_(&links);
872    /// # browser.close().await?;
873    /// # Ok(())
874    /// # }
875    /// ```
876    ///
877    /// See: <https://playwright.dev/docs/api/class-locator#locator-or>
878    pub fn or_(&self, locator: &Locator) -> Locator {
879        let inner = serde_json::to_string(&locator.selector)
880            .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
881        Locator::new(
882            Arc::clone(&self.frame),
883            format!("{} >> internal:or={}", self.selector, inner),
884            self.page.clone(),
885        )
886    }
887
888    /// Returns the number of elements matching this locator.
889    ///
890    /// See: <https://playwright.dev/docs/api/class-locator#locator-count>
891    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, count = tracing::field::Empty))]
892    pub async fn count(&self) -> Result<usize> {
893        let n = self
894            .frame
895            .locator_count(&self.selector)
896            .await
897            .map_err(|e| self.wrap_error_with_selector(e))?;
898        tracing::Span::current().record("count", n);
899        Ok(n)
900    }
901
902    /// Returns an array of locators, one for each matching element.
903    ///
904    /// Note: `all()` does not wait for elements to match the locator,
905    /// and instead immediately returns whatever is in the DOM.
906    ///
907    /// See: <https://playwright.dev/docs/api/class-locator#locator-all>
908    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
909    pub async fn all(&self) -> Result<Vec<Locator>> {
910        let count = self.count().await?;
911        Ok((0..count).map(|i| self.nth(i as i32)).collect())
912    }
913
914    /// Returns the text content of the element.
915    ///
916    /// See: <https://playwright.dev/docs/api/class-locator#locator-text-content>
917    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
918    pub async fn text_content(&self) -> Result<Option<String>> {
919        self.frame
920            .locator_text_content(&self.selector)
921            .await
922            .map_err(|e| self.wrap_error_with_selector(e))
923    }
924
925    /// Returns the inner text of the element (visible text).
926    ///
927    /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-text>
928    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
929    pub async fn inner_text(&self) -> Result<String> {
930        self.frame
931            .locator_inner_text(&self.selector)
932            .await
933            .map_err(|e| self.wrap_error_with_selector(e))
934    }
935
936    /// Returns the inner HTML of the element.
937    ///
938    /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-html>
939    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
940    pub async fn inner_html(&self) -> Result<String> {
941        self.frame
942            .locator_inner_html(&self.selector)
943            .await
944            .map_err(|e| self.wrap_error_with_selector(e))
945    }
946
947    /// Returns the value of the specified attribute.
948    ///
949    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-attribute>
950    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, name = %name))]
951    pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
952        self.frame
953            .locator_get_attribute(&self.selector, name)
954            .await
955            .map_err(|e| self.wrap_error_with_selector(e))
956    }
957
958    /// Returns whether the element is visible.
959    ///
960    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-visible>
961    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
962    pub async fn is_visible(&self) -> Result<bool> {
963        self.frame
964            .locator_is_visible(&self.selector)
965            .await
966            .map_err(|e| self.wrap_error_with_selector(e))
967    }
968
969    /// Returns whether the element is enabled.
970    ///
971    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-enabled>
972    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
973    pub async fn is_enabled(&self) -> Result<bool> {
974        self.frame
975            .locator_is_enabled(&self.selector)
976            .await
977            .map_err(|e| self.wrap_error_with_selector(e))
978    }
979
980    /// Returns whether the checkbox or radio button is checked.
981    ///
982    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-checked>
983    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
984    pub async fn is_checked(&self) -> Result<bool> {
985        self.frame
986            .locator_is_checked(&self.selector)
987            .await
988            .map_err(|e| self.wrap_error_with_selector(e))
989    }
990
991    /// Returns whether the element is editable.
992    ///
993    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-editable>
994    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
995    pub async fn is_editable(&self) -> Result<bool> {
996        self.frame
997            .locator_is_editable(&self.selector)
998            .await
999            .map_err(|e| self.wrap_error_with_selector(e))
1000    }
1001
1002    /// Returns whether the element is hidden.
1003    ///
1004    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-hidden>
1005    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1006    pub async fn is_hidden(&self) -> Result<bool> {
1007        self.frame
1008            .locator_is_hidden(&self.selector)
1009            .await
1010            .map_err(|e| self.wrap_error_with_selector(e))
1011    }
1012
1013    /// Returns whether the element is disabled.
1014    ///
1015    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-disabled>
1016    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1017    pub async fn is_disabled(&self) -> Result<bool> {
1018        self.frame
1019            .locator_is_disabled(&self.selector)
1020            .await
1021            .map_err(|e| self.wrap_error_with_selector(e))
1022    }
1023
1024    /// Returns whether the element is focused (currently has focus).
1025    ///
1026    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-focused>
1027    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1028    pub async fn is_focused(&self) -> Result<bool> {
1029        self.frame
1030            .locator_is_focused(&self.selector)
1031            .await
1032            .map_err(|e| self.wrap_error_with_selector(e))
1033    }
1034
1035    // Action methods
1036
1037    /// Clicks the element.
1038    ///
1039    /// See: <https://playwright.dev/docs/api/class-locator#locator-click>
1040    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1041    pub async fn click(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
1042        self.frame
1043            .locator_click(&self.selector, Some(self.with_timeout(options)))
1044            .await
1045            .map_err(|e| self.wrap_error_with_selector(e))
1046    }
1047
1048    /// Ensures an options struct has the page's default timeout when none is explicitly set.
1049    fn with_timeout<T: HasTimeout + Default>(&self, options: Option<T>) -> T {
1050        let mut opts = options.unwrap_or_default();
1051        if opts.timeout_ref().is_none() {
1052            *opts.timeout_ref_mut() = Some(self.page.default_timeout_ms());
1053        }
1054        opts
1055    }
1056
1057    /// Wraps an error with selector context for better error messages.
1058    fn wrap_error_with_selector(&self, error: crate::error::Error) -> crate::error::Error {
1059        match &error {
1060            crate::error::Error::ProtocolError(msg) => {
1061                // Add selector context to protocol errors (timeouts, etc.)
1062                crate::error::Error::ProtocolError(format!("{} [selector: {}]", msg, self.selector))
1063            }
1064            crate::error::Error::Timeout(msg) => {
1065                crate::error::Error::Timeout(format!("{} [selector: {}]", msg, self.selector))
1066            }
1067            _ => error, // Other errors pass through unchanged
1068        }
1069    }
1070
1071    /// Double clicks the element.
1072    ///
1073    /// See: <https://playwright.dev/docs/api/class-locator#locator-dblclick>
1074    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1075    pub async fn dblclick(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
1076        self.frame
1077            .locator_dblclick(&self.selector, Some(self.with_timeout(options)))
1078            .await
1079            .map_err(|e| self.wrap_error_with_selector(e))
1080    }
1081
1082    /// Fills the element with text.
1083    ///
1084    /// See: <https://playwright.dev/docs/api/class-locator#locator-fill>
1085    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1086    pub async fn fill(
1087        &self,
1088        text: &str,
1089        options: Option<crate::protocol::FillOptions>,
1090    ) -> Result<()> {
1091        self.frame
1092            .locator_fill(&self.selector, text, Some(self.with_timeout(options)))
1093            .await
1094            .map_err(|e| self.wrap_error_with_selector(e))
1095    }
1096
1097    /// Clears the element's value.
1098    ///
1099    /// See: <https://playwright.dev/docs/api/class-locator#locator-clear>
1100    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1101    pub async fn clear(&self, options: Option<crate::protocol::FillOptions>) -> Result<()> {
1102        self.frame
1103            .locator_clear(&self.selector, Some(self.with_timeout(options)))
1104            .await
1105            .map_err(|e| self.wrap_error_with_selector(e))
1106    }
1107
1108    /// Presses a key on the element.
1109    ///
1110    /// See: <https://playwright.dev/docs/api/class-locator#locator-press>
1111    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1112    pub async fn press(
1113        &self,
1114        key: &str,
1115        options: Option<crate::protocol::PressOptions>,
1116    ) -> Result<()> {
1117        self.frame
1118            .locator_press(&self.selector, key, Some(self.with_timeout(options)))
1119            .await
1120            .map_err(|e| self.wrap_error_with_selector(e))
1121    }
1122
1123    /// Sets focus on the element.
1124    ///
1125    /// Calls the element's `focus()` method. Used to move keyboard focus to a
1126    /// specific element for subsequent keyboard interactions.
1127    ///
1128    /// See: <https://playwright.dev/docs/api/class-locator#locator-focus>
1129    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1130    pub async fn focus(&self) -> Result<()> {
1131        self.frame
1132            .locator_focus(&self.selector)
1133            .await
1134            .map_err(|e| self.wrap_error_with_selector(e))
1135    }
1136
1137    /// Removes focus from the element.
1138    ///
1139    /// Calls the element's `blur()` method. Moves keyboard focus away from the element.
1140    ///
1141    /// See: <https://playwright.dev/docs/api/class-locator#locator-blur>
1142    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1143    pub async fn blur(&self) -> Result<()> {
1144        self.frame
1145            .locator_blur(&self.selector)
1146            .await
1147            .map_err(|e| self.wrap_error_with_selector(e))
1148    }
1149
1150    /// Types `text` into the element character by character, as though it was typed
1151    /// on a real keyboard.
1152    ///
1153    /// Use this method when you need to simulate keystrokes with individual key events
1154    /// (e.g., for autocomplete widgets). For simply setting a field value, prefer
1155    /// [`Locator::fill()`].
1156    ///
1157    /// # Arguments
1158    ///
1159    /// * `text` - Text to type into the element
1160    /// * `options` - Optional [`PressSequentiallyOptions`](crate::protocol::PressSequentiallyOptions) (e.g., `delay` between key presses)
1161    ///
1162    /// See: <https://playwright.dev/docs/api/class-locator#locator-press-sequentially>
1163    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1164    pub async fn press_sequentially(
1165        &self,
1166        text: &str,
1167        options: Option<crate::protocol::PressSequentiallyOptions>,
1168    ) -> Result<()> {
1169        self.frame
1170            .locator_press_sequentially(&self.selector, text, options)
1171            .await
1172            .map_err(|e| self.wrap_error_with_selector(e))
1173    }
1174
1175    /// Returns the `innerText` values of all elements matching this locator.
1176    ///
1177    /// Unlike [`Locator::inner_text()`] (which uses strict mode and requires exactly one match),
1178    /// `all_inner_texts()` returns text from all matching elements.
1179    ///
1180    /// See: <https://playwright.dev/docs/api/class-locator#locator-all-inner-texts>
1181    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1182    pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
1183        self.frame
1184            .locator_all_inner_texts(&self.selector)
1185            .await
1186            .map_err(|e| self.wrap_error_with_selector(e))
1187    }
1188
1189    /// Returns the `textContent` values of all elements matching this locator.
1190    ///
1191    /// Unlike [`Locator::text_content()`] (which uses strict mode and requires exactly one match),
1192    /// `all_text_contents()` returns text from all matching elements.
1193    ///
1194    /// See: <https://playwright.dev/docs/api/class-locator#locator-all-text-contents>
1195    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1196    pub async fn all_text_contents(&self) -> Result<Vec<String>> {
1197        self.frame
1198            .locator_all_text_contents(&self.selector)
1199            .await
1200            .map_err(|e| self.wrap_error_with_selector(e))
1201    }
1202
1203    /// Ensures the checkbox or radio button is checked.
1204    ///
1205    /// This method is idempotent - if already checked, does nothing.
1206    ///
1207    /// See: <https://playwright.dev/docs/api/class-locator#locator-check>
1208    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1209    pub async fn check(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
1210        self.frame
1211            .locator_check(&self.selector, Some(self.with_timeout(options)))
1212            .await
1213            .map_err(|e| self.wrap_error_with_selector(e))
1214    }
1215
1216    /// Ensures the checkbox is unchecked.
1217    ///
1218    /// This method is idempotent - if already unchecked, does nothing.
1219    ///
1220    /// See: <https://playwright.dev/docs/api/class-locator#locator-uncheck>
1221    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1222    pub async fn uncheck(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
1223        self.frame
1224            .locator_uncheck(&self.selector, Some(self.with_timeout(options)))
1225            .await
1226            .map_err(|e| self.wrap_error_with_selector(e))
1227    }
1228
1229    /// Sets the checkbox or radio button to the specified checked state.
1230    ///
1231    /// This is a convenience method that calls `check()` if `checked` is true,
1232    /// or `uncheck()` if `checked` is false.
1233    ///
1234    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-checked>
1235    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1236    pub async fn set_checked(
1237        &self,
1238        checked: bool,
1239        options: Option<crate::protocol::CheckOptions>,
1240    ) -> Result<()> {
1241        if checked {
1242            self.check(options).await
1243        } else {
1244            self.uncheck(options).await
1245        }
1246    }
1247
1248    /// Hovers the mouse over the element.
1249    ///
1250    /// See: <https://playwright.dev/docs/api/class-locator#locator-hover>
1251    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1252    pub async fn hover(&self, options: Option<crate::protocol::HoverOptions>) -> Result<()> {
1253        self.frame
1254            .locator_hover(&self.selector, Some(self.with_timeout(options)))
1255            .await
1256            .map_err(|e| self.wrap_error_with_selector(e))
1257    }
1258
1259    /// Returns the value of the input, textarea, or select element.
1260    ///
1261    /// See: <https://playwright.dev/docs/api/class-locator#locator-input-value>
1262    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1263    pub async fn input_value(&self, _options: Option<()>) -> Result<String> {
1264        self.frame
1265            .locator_input_value(&self.selector)
1266            .await
1267            .map_err(|e| self.wrap_error_with_selector(e))
1268    }
1269
1270    /// Selects one or more options in a select element.
1271    ///
1272    /// Returns an array of option values that have been successfully selected.
1273    ///
1274    /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1275    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1276    pub async fn select_option(
1277        &self,
1278        value: impl Into<crate::protocol::SelectOption>,
1279        options: Option<crate::protocol::SelectOptions>,
1280    ) -> Result<Vec<String>> {
1281        self.frame
1282            .locator_select_option(
1283                &self.selector,
1284                value.into(),
1285                Some(self.with_timeout(options)),
1286            )
1287            .await
1288            .map_err(|e| self.wrap_error_with_selector(e))
1289    }
1290
1291    /// Selects multiple options in a select element.
1292    ///
1293    /// Returns an array of option values that have been successfully selected.
1294    ///
1295    /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1296    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1297    pub async fn select_option_multiple(
1298        &self,
1299        values: &[impl Into<crate::protocol::SelectOption> + Clone],
1300        options: Option<crate::protocol::SelectOptions>,
1301    ) -> Result<Vec<String>> {
1302        let select_options: Vec<crate::protocol::SelectOption> =
1303            values.iter().map(|v| v.clone().into()).collect();
1304        self.frame
1305            .locator_select_option_multiple(
1306                &self.selector,
1307                select_options,
1308                Some(self.with_timeout(options)),
1309            )
1310            .await
1311            .map_err(|e| self.wrap_error_with_selector(e))
1312    }
1313
1314    /// Sets the file path(s) to upload to a file input element.
1315    ///
1316    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1317    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1318    pub async fn set_input_files(
1319        &self,
1320        file: &std::path::PathBuf,
1321        _options: Option<()>,
1322    ) -> Result<()> {
1323        self.frame
1324            .locator_set_input_files(&self.selector, file)
1325            .await
1326            .map_err(|e| self.wrap_error_with_selector(e))
1327    }
1328
1329    /// Sets multiple file paths to upload to a file input element.
1330    ///
1331    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1332    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1333    pub async fn set_input_files_multiple(
1334        &self,
1335        files: &[&std::path::PathBuf],
1336        _options: Option<()>,
1337    ) -> Result<()> {
1338        self.frame
1339            .locator_set_input_files_multiple(&self.selector, files)
1340            .await
1341            .map_err(|e| self.wrap_error_with_selector(e))
1342    }
1343
1344    /// Sets a file to upload using FilePayload (explicit name, mimeType, buffer).
1345    ///
1346    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1347    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1348    pub async fn set_input_files_payload(
1349        &self,
1350        file: crate::protocol::FilePayload,
1351        _options: Option<()>,
1352    ) -> Result<()> {
1353        self.frame
1354            .locator_set_input_files_payload(&self.selector, file)
1355            .await
1356            .map_err(|e| self.wrap_error_with_selector(e))
1357    }
1358
1359    /// Sets multiple files to upload using FilePayload.
1360    ///
1361    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1362    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1363    pub async fn set_input_files_payload_multiple(
1364        &self,
1365        files: &[crate::protocol::FilePayload],
1366        _options: Option<()>,
1367    ) -> Result<()> {
1368        self.frame
1369            .locator_set_input_files_payload_multiple(&self.selector, files)
1370            .await
1371            .map_err(|e| self.wrap_error_with_selector(e))
1372    }
1373
1374    /// Dispatches a DOM event on the element.
1375    ///
1376    /// Unlike clicking or typing, `dispatch_event` directly sends the event without
1377    /// performing any actionability checks. It still waits for the element to be present
1378    /// in the DOM.
1379    ///
1380    /// # Arguments
1381    ///
1382    /// * `type_` - The event type to dispatch, e.g. `"click"`, `"focus"`, `"myevent"`.
1383    /// * `event_init` - Optional event initializer properties (e.g. `{"detail": "value"}` for
1384    ///   `CustomEvent`). Corresponds to the second argument of `new Event(type, init)`.
1385    ///
1386    /// # Errors
1387    ///
1388    /// Returns an error if:
1389    /// - The element is not found within the timeout
1390    /// - The protocol call fails
1391    ///
1392    /// See: <https://playwright.dev/docs/api/class-locator#locator-dispatch-event>
1393    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1394    pub async fn dispatch_event(
1395        &self,
1396        type_: &str,
1397        event_init: Option<serde_json::Value>,
1398    ) -> Result<()> {
1399        self.frame
1400            .locator_dispatch_event(&self.selector, type_, event_init)
1401            .await
1402            .map_err(|e| self.wrap_error_with_selector(e))
1403    }
1404
1405    /// Returns the bounding box of the element, or `None` if the element is not visible.
1406    ///
1407    /// The bounding box is in pixels, relative to the top-left corner of the page.
1408    /// Returns `None` when the element has `display: none` or is otherwise not part of
1409    /// the layout.
1410    ///
1411    /// # Errors
1412    ///
1413    /// Returns an error if:
1414    /// - The element is not found within the timeout
1415    /// - The protocol call fails
1416    ///
1417    /// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
1418    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1419    pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
1420        self.frame
1421            .locator_bounding_box(&self.selector)
1422            .await
1423            .map_err(|e| self.wrap_error_with_selector(e))
1424    }
1425
1426    /// Scrolls the element into view if it is not already visible in the viewport.
1427    ///
1428    /// This is an alias for calling `element.scrollIntoView()` in the browser.
1429    ///
1430    /// # Errors
1431    ///
1432    /// Returns an error if:
1433    /// - The element is not found within the timeout
1434    /// - The protocol call fails
1435    ///
1436    /// See: <https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed>
1437    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1438    pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
1439        self.frame
1440            .locator_scroll_into_view_if_needed(&self.selector)
1441            .await
1442            .map_err(|e| self.wrap_error_with_selector(e))
1443    }
1444
1445    /// Takes a screenshot of the element and returns the image bytes.
1446    ///
1447    /// This method uses strict mode - it will fail if the selector matches multiple elements.
1448    /// Use `first()`, `last()`, or `nth()` to refine the selector to a single element.
1449    ///
1450    /// See: <https://playwright.dev/docs/api/class-locator#locator-screenshot>
1451    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector, bytes_len = tracing::field::Empty))]
1452    pub async fn screenshot(
1453        &self,
1454        options: Option<crate::protocol::ScreenshotOptions>,
1455    ) -> Result<Vec<u8>> {
1456        // Query for the element using strict mode (should return exactly one)
1457        let element = self
1458            .frame
1459            .query_selector(&self.selector)
1460            .await
1461            .map_err(|e| self.wrap_error_with_selector(e))?
1462            .ok_or_else(|| {
1463                crate::error::Error::ElementNotFound(format!(
1464                    "Element not found: {}",
1465                    self.selector
1466                ))
1467            })?;
1468
1469        // Delegate to ElementHandle.screenshot() with default timeout injected
1470        let bytes = element
1471            .screenshot(Some(self.with_timeout(options)))
1472            .await
1473            .map_err(|e| self.wrap_error_with_selector(e))?;
1474        tracing::Span::current().record("bytes_len", bytes.len());
1475        Ok(bytes)
1476    }
1477
1478    /// Performs a touch-tap on the element.
1479    ///
1480    /// This method dispatches a `touchstart` and `touchend` event on the element.
1481    /// For touch support to work, the browser context must be created with
1482    /// `has_touch: true`.
1483    ///
1484    /// # Arguments
1485    ///
1486    /// * `options` - Optional [`TapOptions`](crate::protocol::TapOptions) (force, modifiers, position, timeout, trial)
1487    ///
1488    /// # Errors
1489    ///
1490    /// Returns an error if:
1491    /// - The element is not found within the timeout
1492    /// - Actionability checks fail (unless `force: true`)
1493    /// - The browser context was not created with `has_touch: true`
1494    ///
1495    /// See: <https://playwright.dev/docs/api/class-locator#locator-tap>
1496    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1497    pub async fn tap(&self, options: Option<crate::protocol::TapOptions>) -> Result<()> {
1498        self.frame
1499            .locator_tap(&self.selector, Some(self.with_timeout(options)))
1500            .await
1501            .map_err(|e| self.wrap_error_with_selector(e))
1502    }
1503
1504    /// Drags this element to the `target` element.
1505    ///
1506    /// Both this locator and `target` must resolve to elements in the same frame.
1507    /// Playwright performs a series of mouse events (move, press, move to target, release)
1508    /// to simulate the drag.
1509    ///
1510    /// # Arguments
1511    ///
1512    /// * `target` - The locator of the element to drag onto
1513    /// * `options` - Optional [`DragToOptions`](crate::protocol::DragToOptions) (force, no_wait_after, timeout, trial,
1514    ///   source_position, target_position)
1515    ///
1516    /// # Errors
1517    ///
1518    /// Returns an error if:
1519    /// - Either element is not found within the timeout
1520    /// - Actionability checks fail (unless `force: true`)
1521    /// - The protocol call fails
1522    ///
1523    /// See: <https://playwright.dev/docs/api/class-locator#locator-drag-to>
1524    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1525    pub async fn drag_to(
1526        &self,
1527        target: &Locator,
1528        options: Option<crate::protocol::DragToOptions>,
1529    ) -> Result<()> {
1530        self.frame
1531            .locator_drag_to(
1532                &self.selector,
1533                &target.selector,
1534                Some(self.with_timeout(options)),
1535            )
1536            .await
1537            .map_err(|e| self.wrap_error_with_selector(e))
1538    }
1539
1540    /// Waits until the element satisfies the given state condition.
1541    ///
1542    /// If no state is specified, waits for the element to be `visible` (the default).
1543    ///
1544    /// This method is useful for waiting for lazy-rendered elements or elements that
1545    /// appear/disappear based on user interaction or async data loading.
1546    ///
1547    /// # Arguments
1548    ///
1549    /// * `options` - Optional [`WaitForOptions`](crate::protocol::WaitForOptions) specifying the `state` to wait for
1550    ///   (`Visible`, `Hidden`, `Attached`, or `Detached`) and a `timeout` in milliseconds.
1551    ///
1552    /// # Errors
1553    ///
1554    /// Returns an error if the element does not satisfy the expected state within the timeout.
1555    ///
1556    /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for>
1557    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1558    pub async fn wait_for(&self, options: Option<crate::protocol::WaitForOptions>) -> Result<()> {
1559        self.frame
1560            .locator_wait_for(&self.selector, Some(self.with_timeout(options)))
1561            .await
1562            .map_err(|e| self.wrap_error_with_selector(e))
1563    }
1564
1565    /// Evaluates a JavaScript expression in the scope of the matched element.
1566    ///
1567    /// The element is passed as the first argument to the expression. The expression
1568    /// can be any JavaScript function or expression that returns a JSON-serializable value.
1569    ///
1570    /// # Arguments
1571    ///
1572    /// * `expression` - JavaScript expression or function, e.g. `"(el) => el.textContent"`
1573    /// * `arg` - Optional argument passed as the second argument to the function
1574    ///
1575    /// # Errors
1576    ///
1577    /// Returns an error if:
1578    /// - The element is not found within the timeout
1579    /// - The JavaScript expression throws an error
1580    /// - The return value is not JSON-serializable
1581    ///
1582    /// # Example
1583    ///
1584    /// ```ignore
1585    /// use playwright_rs::Playwright;
1586    ///
1587    /// # #[tokio::main]
1588    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1589    /// let playwright = Playwright::launch().await?;
1590    /// let browser = playwright.chromium().launch().await?;
1591    /// let page = browser.new_page().await?;
1592    /// let _ = page.goto("data:text/html,<h1>Hello</h1>", None).await;
1593    ///
1594    /// let heading = page.locator("h1").await;
1595    /// let text: String = heading.evaluate("(el) => el.textContent", None::<()>).await?;
1596    /// assert_eq!(text, "Hello");
1597    ///
1598    /// // With an argument
1599    /// let result: String = heading
1600    ///     .evaluate("(el, suffix) => el.textContent + suffix", Some("!"))
1601    ///     .await?;
1602    /// assert_eq!(result, "Hello!");
1603    /// # browser.close().await?;
1604    /// # Ok(())
1605    /// # }
1606    /// ```
1607    ///
1608    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate>
1609    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1610    pub async fn evaluate<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1611    where
1612        R: serde::de::DeserializeOwned,
1613        T: serde::Serialize,
1614    {
1615        let raw = self
1616            .frame
1617            .locator_evaluate(&self.selector, expression, arg)
1618            .await
1619            .map_err(|e| self.wrap_error_with_selector(e))?;
1620        serde_json::from_value(raw).map_err(|e| {
1621            crate::error::Error::ProtocolError(format!(
1622                "evaluate result deserialization failed: {}",
1623                e
1624            ))
1625        })
1626    }
1627
1628    /// Evaluates a JavaScript expression in the scope of all elements matching this locator.
1629    ///
1630    /// The array of all matched elements is passed as the first argument to the expression.
1631    /// Unlike [`evaluate()`](Self::evaluate), this does not use strict mode — all matching
1632    /// elements are collected and passed as an array.
1633    ///
1634    /// # Arguments
1635    ///
1636    /// * `expression` - JavaScript function that receives an array of elements
1637    /// * `arg` - Optional argument passed as the second argument to the function
1638    ///
1639    /// # Errors
1640    ///
1641    /// Returns an error if:
1642    /// - The JavaScript expression throws an error
1643    /// - The return value is not JSON-serializable
1644    ///
1645    /// # Example
1646    ///
1647    /// ```ignore
1648    /// use playwright_rs::Playwright;
1649    ///
1650    /// # #[tokio::main]
1651    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1652    /// let playwright = Playwright::launch().await?;
1653    /// let browser = playwright.chromium().launch().await?;
1654    /// let page = browser.new_page().await?;
1655    /// let _ = page.goto(
1656    ///     "data:text/html,<li class='item'>A</li><li class='item'>B</li>",
1657    ///     None
1658    /// ).await;
1659    ///
1660    /// let items = page.locator(".item").await;
1661    /// let texts: Vec<String> = items
1662    ///     .evaluate_all("(elements) => elements.map(e => e.textContent)", None::<()>)
1663    ///     .await?;
1664    /// assert_eq!(texts, vec!["A", "B"]);
1665    /// # browser.close().await?;
1666    /// # Ok(())
1667    /// # }
1668    /// ```
1669    ///
1670    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate-all>
1671    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1672    pub async fn evaluate_all<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1673    where
1674        R: serde::de::DeserializeOwned,
1675        T: serde::Serialize,
1676    {
1677        let raw = self
1678            .frame
1679            .locator_evaluate_all(&self.selector, expression, arg)
1680            .await
1681            .map_err(|e| self.wrap_error_with_selector(e))?;
1682        serde_json::from_value(raw).map_err(|e| {
1683            crate::error::Error::ProtocolError(format!(
1684                "evaluate_all result deserialization failed: {}",
1685                e
1686            ))
1687        })
1688    }
1689
1690    /// Returns the ARIA accessibility tree snapshot as a YAML string.
1691    ///
1692    /// The snapshot describes the accessible roles, names, and properties of the matched
1693    /// element and its descendants. This is useful for writing stable accessibility assertions
1694    /// that are independent of CSS classes or DOM structure.
1695    ///
1696    /// # Errors
1697    ///
1698    /// Returns an error if:
1699    /// - The element is not found within the timeout
1700    /// - The protocol call fails
1701    ///
1702    /// See: <https://playwright.dev/docs/api/class-locator#locator-aria-snapshot>
1703    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, mode = tracing::field::Empty))]
1704    pub async fn aria_snapshot(
1705        &self,
1706        options: Option<crate::protocol::AriaSnapshotOptions>,
1707    ) -> Result<String> {
1708        self.frame
1709            .locator_aria_snapshot(&self.selector, options.as_ref())
1710            .await
1711            .map_err(|e| self.wrap_error_with_selector(e))
1712    }
1713
1714    /// Returns a new locator whose selector has been resolved to a
1715    /// best-practices canonical form — preferring test-ids, then ARIA
1716    /// roles, then accessible text. The resolved locator points at the
1717    /// same element(s) as `self` but uses a more robust selector that
1718    /// is less coupled to CSS classes or DOM structure. Useful as a
1719    /// building block for codegen helpers that want the "most stable
1720    /// selector for this element" primitive.
1721    ///
1722    /// See the module-level example for usage.
1723    ///
1724    /// # Errors
1725    ///
1726    /// Returns an error if:
1727    /// - No element matches the original selector
1728    /// - The protocol call fails
1729    ///
1730    /// See: <https://playwright.dev/docs/api/class-locator#locator-normalize>
1731    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1732    pub async fn normalize(&self) -> Result<Locator> {
1733        let resolved = self
1734            .frame
1735            .frame_resolve_selector(&self.selector)
1736            .await
1737            .map_err(|e| self.wrap_error_with_selector(e))?;
1738        Ok(Locator {
1739            frame: Arc::clone(&self.frame),
1740            selector: resolved,
1741            page: self.page.clone(),
1742        })
1743    }
1744
1745    /// Returns a new Locator with an attached description for traces and error messages.
1746    ///
1747    /// The description does not affect element matching — it is purely informational,
1748    /// appearing in trace viewer labels and error messages to make them more readable.
1749    ///
1750    /// Appends `>> internal:describe="description"` to the selector, matching
1751    /// playwright-python's behavior exactly.
1752    ///
1753    /// See: <https://playwright.dev/docs/api/class-locator#locator-describe>
1754    pub fn describe(&self, description: &str) -> Locator {
1755        let escaped =
1756            serde_json::to_string(description).unwrap_or_else(|_| format!("\"{}\"", description));
1757        Locator::new(
1758            Arc::clone(&self.frame),
1759            format!("{} >> internal:describe={}", self.selector, escaped),
1760            self.page.clone(),
1761        )
1762    }
1763
1764    /// Highlights the matched element in the browser for visual debugging.
1765    ///
1766    /// Draws a colored overlay over the element for a short period. This is a
1767    /// debugging tool and has no effect on test assertions or element state.
1768    ///
1769    /// # Errors
1770    ///
1771    /// Returns an error if:
1772    /// - The element is not found within the timeout
1773    /// - The protocol call fails
1774    ///
1775    /// See: <https://playwright.dev/docs/api/class-locator#locator-highlight>
1776    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1777    pub async fn highlight(&self) -> Result<()> {
1778        self.frame
1779            .locator_highlight(&self.selector)
1780            .await
1781            .map_err(|e| self.wrap_error_with_selector(e))
1782    }
1783
1784    /// Returns a [`FrameLocator`](crate::protocol::FrameLocator) for the content of an
1785    /// `<iframe>` element matched by this locator.
1786    ///
1787    /// This is a client-side operation — it creates a `FrameLocator` scoped to the matched
1788    /// iframe element, allowing you to interact with elements inside the iframe using the
1789    /// standard `FrameLocator` API.
1790    ///
1791    /// Equivalent to `page.frame_locator(selector)`, but starting from an existing `Locator`.
1792    ///
1793    /// See: <https://playwright.dev/docs/api/class-locator#locator-content-frame>
1794    pub fn content_frame(&self) -> crate::protocol::FrameLocator {
1795        crate::protocol::FrameLocator::new(
1796            Arc::clone(&self.frame),
1797            self.selector.clone(),
1798            self.page.clone(),
1799        )
1800    }
1801}
1802
1803impl std::fmt::Debug for Locator {
1804    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1805        f.debug_struct("Locator")
1806            .field("selector", &self.selector)
1807            .finish()
1808    }
1809}
1810
1811#[cfg(test)]
1812mod tests {
1813    use super::*;
1814
1815    #[test]
1816    fn test_escape_for_selector_case_insensitive() {
1817        assert_eq!(escape_for_selector("hello", false), "\"hello\"i");
1818    }
1819
1820    #[test]
1821    fn test_escape_for_selector_exact() {
1822        assert_eq!(escape_for_selector("hello", true), "\"hello\"s");
1823    }
1824
1825    #[test]
1826    fn test_escape_for_selector_with_quotes() {
1827        assert_eq!(
1828            escape_for_selector("say \"hi\"", false),
1829            "\"say \\\"hi\\\"\"i"
1830        );
1831    }
1832
1833    #[test]
1834    fn test_get_by_text_selector_case_insensitive() {
1835        assert_eq!(
1836            get_by_text_selector("Click me", false),
1837            "internal:text=\"Click me\"i"
1838        );
1839    }
1840
1841    #[test]
1842    fn test_get_by_text_selector_exact() {
1843        assert_eq!(
1844            get_by_text_selector("Click me", true),
1845            "internal:text=\"Click me\"s"
1846        );
1847    }
1848
1849    #[test]
1850    fn test_get_by_label_selector() {
1851        assert_eq!(
1852            get_by_label_selector("Email", false),
1853            "internal:label=\"Email\"i"
1854        );
1855    }
1856
1857    #[test]
1858    fn test_get_by_placeholder_selector() {
1859        assert_eq!(
1860            get_by_placeholder_selector("Enter name", false),
1861            "internal:attr=[placeholder=\"Enter name\"i]"
1862        );
1863    }
1864
1865    #[test]
1866    fn test_get_by_alt_text_selector() {
1867        assert_eq!(
1868            get_by_alt_text_selector("Logo", true),
1869            "internal:attr=[alt=\"Logo\"s]"
1870        );
1871    }
1872
1873    #[test]
1874    fn test_get_by_title_selector() {
1875        assert_eq!(
1876            get_by_title_selector("Help", false),
1877            "internal:attr=[title=\"Help\"i]"
1878        );
1879    }
1880
1881    #[test]
1882    fn test_get_by_test_id_selector() {
1883        assert_eq!(
1884            get_by_test_id_selector("submit-btn"),
1885            "internal:testid=[data-testid=\"submit-btn\"s]"
1886        );
1887    }
1888
1889    #[test]
1890    fn test_escape_for_attribute_selector_case_insensitive() {
1891        assert_eq!(
1892            escape_for_attribute_selector("Submit", false),
1893            "\"Submit\"i"
1894        );
1895    }
1896
1897    #[test]
1898    fn test_escape_for_attribute_selector_exact() {
1899        assert_eq!(escape_for_attribute_selector("Submit", true), "\"Submit\"s");
1900    }
1901
1902    #[test]
1903    fn test_escape_for_attribute_selector_escapes_quotes() {
1904        assert_eq!(
1905            escape_for_attribute_selector("Say \"hello\"", false),
1906            "\"Say \\\"hello\\\"\"i"
1907        );
1908    }
1909
1910    #[test]
1911    fn test_escape_for_attribute_selector_escapes_backslashes() {
1912        assert_eq!(
1913            escape_for_attribute_selector("path\\to", true),
1914            "\"path\\\\to\"s"
1915        );
1916    }
1917
1918    #[test]
1919    fn test_get_by_role_selector_role_only() {
1920        assert_eq!(
1921            get_by_role_selector(AriaRole::Button, None),
1922            "internal:role=button"
1923        );
1924    }
1925
1926    #[test]
1927    fn test_get_by_role_selector_with_name() {
1928        let opts = GetByRoleOptions {
1929            name: Some("Submit".to_string()),
1930            ..Default::default()
1931        };
1932        assert_eq!(
1933            get_by_role_selector(AriaRole::Button, Some(opts)),
1934            "internal:role=button[name=\"Submit\"i]"
1935        );
1936    }
1937
1938    #[test]
1939    fn test_get_by_role_selector_with_name_exact() {
1940        let opts = GetByRoleOptions {
1941            name: Some("Submit".to_string()),
1942            exact: Some(true),
1943            ..Default::default()
1944        };
1945        assert_eq!(
1946            get_by_role_selector(AriaRole::Button, Some(opts)),
1947            "internal:role=button[name=\"Submit\"s]"
1948        );
1949    }
1950
1951    #[test]
1952    fn test_get_by_role_selector_with_checked() {
1953        let opts = GetByRoleOptions {
1954            checked: Some(true),
1955            ..Default::default()
1956        };
1957        assert_eq!(
1958            get_by_role_selector(AriaRole::Checkbox, Some(opts)),
1959            "internal:role=checkbox[checked=true]"
1960        );
1961    }
1962
1963    #[test]
1964    fn test_get_by_role_selector_with_level() {
1965        let opts = GetByRoleOptions {
1966            level: Some(2),
1967            ..Default::default()
1968        };
1969        assert_eq!(
1970            get_by_role_selector(AriaRole::Heading, Some(opts)),
1971            "internal:role=heading[level=2]"
1972        );
1973    }
1974
1975    #[test]
1976    fn test_get_by_role_selector_with_disabled() {
1977        let opts = GetByRoleOptions {
1978            disabled: Some(true),
1979            ..Default::default()
1980        };
1981        assert_eq!(
1982            get_by_role_selector(AriaRole::Button, Some(opts)),
1983            "internal:role=button[disabled=true]"
1984        );
1985    }
1986
1987    #[test]
1988    fn test_get_by_role_selector_include_hidden() {
1989        let opts = GetByRoleOptions {
1990            include_hidden: Some(true),
1991            ..Default::default()
1992        };
1993        assert_eq!(
1994            get_by_role_selector(AriaRole::Button, Some(opts)),
1995            "internal:role=button[include-hidden=true]"
1996        );
1997    }
1998
1999    #[test]
2000    fn test_get_by_role_selector_property_order() {
2001        // All properties: checked, disabled, selected, expanded, include-hidden, level, name, pressed
2002        let opts = GetByRoleOptions {
2003            pressed: Some(true),
2004            name: Some("OK".to_string()),
2005            checked: Some(false),
2006            disabled: Some(true),
2007            ..Default::default()
2008        };
2009        assert_eq!(
2010            get_by_role_selector(AriaRole::Button, Some(opts)),
2011            "internal:role=button[checked=false][disabled=true][name=\"OK\"i][pressed=true]"
2012        );
2013    }
2014
2015    #[test]
2016    fn test_get_by_role_selector_name_with_special_chars() {
2017        let opts = GetByRoleOptions {
2018            name: Some("Click \"here\" now".to_string()),
2019            exact: Some(true),
2020            ..Default::default()
2021        };
2022        assert_eq!(
2023            get_by_role_selector(AriaRole::Link, Some(opts)),
2024            "internal:role=link[name=\"Click \\\"here\\\" now\"s]"
2025        );
2026    }
2027
2028    #[test]
2029    fn test_aria_role_as_str() {
2030        assert_eq!(AriaRole::Button.as_str(), "button");
2031        assert_eq!(AriaRole::Heading.as_str(), "heading");
2032        assert_eq!(AriaRole::Link.as_str(), "link");
2033        assert_eq!(AriaRole::Checkbox.as_str(), "checkbox");
2034        assert_eq!(AriaRole::Alert.as_str(), "alert");
2035        assert_eq!(AriaRole::Navigation.as_str(), "navigation");
2036        assert_eq!(AriaRole::Progressbar.as_str(), "progressbar");
2037        assert_eq!(AriaRole::Treeitem.as_str(), "treeitem");
2038    }
2039}