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