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