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
521impl HighlightOptions {
522    /// Extra inline CSS applied to the debug highlight overlay.
523    pub fn style(mut self, style: impl Into<String>) -> Self {
524        self.style = Some(style.into());
525        self
526    }
527}
528
529/// Options for [`Locator::filter()`].
530///
531/// Narrows an existing locator according to the specified criteria.
532/// All fields are optional; unset fields are ignored.
533///
534/// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
535#[derive(Debug, Clone, Default)]
536#[non_exhaustive]
537pub struct FilterOptions {
538    /// Matches elements containing the specified text (case-insensitive substring by default).
539    pub has_text: Option<String>,
540    /// Matches elements that do **not** contain the specified text anywhere inside.
541    pub has_not_text: Option<String>,
542    /// Narrows to elements that contain a descendant matching this locator.
543    ///
544    /// The inner locator is queried relative to the outer locator's matched element,
545    /// not the document root.
546    pub has: Option<Locator>,
547    /// Narrows to elements that do **not** contain a descendant matching this locator.
548    pub has_not: Option<Locator>,
549    /// Narrows to elements that are visible (`true`) or hidden (`false`).
550    ///
551    /// [`Locator::visible`] is the shorthand for the `true` case.
552    pub visible: Option<bool>,
553}
554
555impl FilterOptions {
556    /// Keep only elements containing the given text (case-insensitive substring).
557    pub fn has_text(mut self, has_text: impl Into<String>) -> Self {
558        self.has_text = Some(has_text.into());
559        self
560    }
561    /// Keep only elements NOT containing the given text.
562    pub fn has_not_text(mut self, has_not_text: impl Into<String>) -> Self {
563        self.has_not_text = Some(has_not_text.into());
564        self
565    }
566    /// Keep only elements containing a match for the inner locator.
567    pub fn has(mut self, has: Locator) -> Self {
568        self.has = Some(has);
569        self
570    }
571    /// Keep only elements NOT containing a match for the inner locator.
572    pub fn has_not(mut self, has_not: Locator) -> Self {
573        self.has_not = Some(has_not);
574        self
575    }
576    /// Keep only visible (`true`) or only hidden (`false`) elements.
577    pub fn visible(mut self, visible: bool) -> Self {
578        self.visible = Some(visible);
579        self
580    }
581}
582
583/// The selector `filter` builds: each narrowing appended in the order
584/// upstream's client appends it, since the engine applies them in order.
585///
586/// The `has` / `has_not` selectors are passed separately because a
587/// [`FilterOptions`] holds whole locators, and only their selectors matter
588/// here.
589fn filter_selector(
590    base: &str,
591    options: &FilterOptions,
592    has: Option<&str>,
593    has_not: Option<&str>,
594) -> String {
595    let mut selector = base.to_string();
596
597    if let Some(text) = &options.has_text {
598        let escaped = escape_for_selector(text, false);
599        selector = format!("{selector} >> internal:has-text={escaped}");
600    }
601
602    if let Some(text) = &options.has_not_text {
603        let escaped = escape_for_selector(text, false);
604        selector = format!("{selector} >> internal:has-not-text={escaped}");
605    }
606
607    if let Some(inner) = has {
608        let quoted = serde_json::to_string(inner).unwrap_or_else(|_| format!("\"{inner}\""));
609        selector = format!("{selector} >> internal:has={quoted}");
610    }
611
612    if let Some(inner) = has_not {
613        let quoted = serde_json::to_string(inner).unwrap_or_else(|_| format!("\"{inner}\""));
614        selector = format!("{selector} >> internal:has-not={quoted}");
615    }
616
617    if let Some(visible) = options.visible {
618        selector = format!("{selector} >> visible={visible}");
619    }
620
621    selector
622}
623
624/// Locator represents a way to find element(s) on the page at any given moment.
625///
626/// Locators are lazy - they don't execute queries until an action is performed.
627/// This enables auto-waiting and retry-ability for robust test automation.
628///
629/// # Examples
630///
631/// ```no_run
632/// use playwright_rs::protocol::{Playwright, SelectOption};
633///
634/// #[tokio::main]
635/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
636///     let playwright = Playwright::launch().await?;
637///     let browser = playwright.chromium().launch().await?;
638///     let page = browser.new_page().await?;
639///
640///     // Demonstrate set_checked() - checkbox interaction
641///     let _ = page.goto(
642///         "data:text/html,<input type='checkbox' id='cb'>",
643///         None
644///     ).await;
645///     let checkbox = page.locator("#cb");
646///     checkbox.set_checked(true, None).await?;
647///     assert!(checkbox.is_checked().await?);
648///     checkbox.set_checked(false, None).await?;
649///     assert!(!checkbox.is_checked().await?);
650///
651///     // Demonstrate select_option() - select by value, label, and index
652///     let _ = page.goto(
653///         "data:text/html,<select id='fruits'>\
654///             <option value='apple'>Apple</option>\
655///             <option value='banana'>Banana</option>\
656///             <option value='cherry'>Cherry</option>\
657///         </select>",
658///         None
659///     ).await;
660///     let select = page.locator("#fruits");
661///     select.select_option("banana", None).await?;
662///     assert_eq!(select.input_value(None).await?, "banana");
663///     select.select_option(SelectOption::Label("Apple".to_string()), None).await?;
664///     assert_eq!(select.input_value(None).await?, "apple");
665///     select.select_option(SelectOption::Index(2), None).await?;
666///     assert_eq!(select.input_value(None).await?, "cherry");
667///
668///     // Demonstrate select_option_multiple() - multi-select
669///     let _ = page.goto(
670///         "data:text/html,<select id='colors' multiple>\
671///             <option value='red'>Red</option>\
672///             <option value='green'>Green</option>\
673///             <option value='blue'>Blue</option>\
674///             <option value='yellow'>Yellow</option>\
675///         </select>",
676///         None
677///     ).await;
678///     let multi = page.locator("#colors");
679///     let selected = multi.select_option_multiple(&["red", "blue"], None).await?;
680///     assert_eq!(selected.len(), 2);
681///     assert!(selected.contains(&"red".to_string()));
682///     assert!(selected.contains(&"blue".to_string()));
683///
684///     // Demonstrate get_by_text() - find elements by text content
685///     let _ = page.goto(
686///         "data:text/html,<button>Submit</button><button>Submit Order</button>",
687///         None
688///     ).await;
689///     let all_submits = page.get_by_text("Submit", false);
690///     assert_eq!(all_submits.count().await?, 2); // case-insensitive substring
691///     let exact_submit = page.get_by_text("Submit", true);
692///     assert_eq!(exact_submit.count().await?, 1); // exact match only
693///
694///     // Demonstrate get_by_label, get_by_placeholder, get_by_test_id
695///     let _ = page.goto(
696///         "data:text/html,<label for='email'>Email</label>\
697///             <input id='email' placeholder='you@example.com' data-testid='email-input' />",
698///         None
699///     ).await;
700///     let by_label = page.get_by_label("Email", false);
701///     assert_eq!(by_label.count().await?, 1);
702///     let by_placeholder = page.get_by_placeholder("you@example.com", true);
703///     assert_eq!(by_placeholder.count().await?, 1);
704///     let by_test_id = page.get_by_test_id("email-input");
705///     assert_eq!(by_test_id.count().await?, 1);
706///
707///     // Demonstrate screenshot() - element screenshot
708///     let _ = page.goto(
709///         "data:text/html,<h1 id='title'>Hello World</h1>",
710///         None
711///     ).await;
712///     let heading = page.locator("#title");
713///     let screenshot = heading.screenshot(None).await?;
714///     assert!(!screenshot.is_empty());
715///
716///     browser.close().await?;
717///     Ok(())
718/// }
719/// ```
720///
721/// See: <https://playwright.dev/docs/api/class-locator>
722#[derive(Clone)]
723pub struct Locator {
724    frame: Arc<Frame>,
725    selector: String,
726    page: crate::protocol::Page,
727}
728
729impl Locator {
730    /// Creates a new Locator (internal use only)
731    ///
732    /// Use `page.locator()` or `frame.locator()` to create locators in application code.
733    pub(crate) fn new(frame: Arc<Frame>, selector: String, page: crate::protocol::Page) -> Self {
734        Self {
735            frame,
736            selector,
737            page,
738        }
739    }
740
741    /// Returns the selector string for this locator
742    pub fn selector(&self) -> &str {
743        &self.selector
744    }
745
746    /// Returns the underlying frame for this locator (crate-internal use only).
747    pub(crate) fn frame(&self) -> &Arc<Frame> {
748        &self.frame
749    }
750
751    /// Waits until `expression` returns a truthy value, with the matched
752    /// element passed as its first argument.
753    ///
754    /// The element is resolved with strict matching, so a selector matching
755    /// more than one element is an error rather than a silent pick of the
756    /// first. Returns `()` rather than a handle: the protocol omits the
757    /// result when a selector is supplied, so there is nothing to hand back.
758    ///
759    /// `WaitForFunctionOptions::polling_interval` has no effect on this
760    /// form: the driver polls element-scoped waits on its own backoff
761    /// schedule (roughly 100ms growing toward 1s) and reads the interval
762    /// only for the page-global form.
763    ///
764    /// # Errors
765    ///
766    /// Returns an error if the expression does not become truthy within the
767    /// timeout (the page default unless set here), or if the selector
768    /// matches nothing or more than one element. The timeout is enforced by
769    /// the driver, so it surfaces as a protocol error carrying the driver's
770    /// "Timeout ...ms exceeded" message, with the selector appended.
771    ///
772    /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for-function>
773    pub async fn wait_for_function(
774        &self,
775        expression: &str,
776        options: impl Into<Option<crate::protocol::WaitForFunctionOptions>>,
777    ) -> Result<()> {
778        let options = self.with_timeout(options.into());
779        self.frame
780            .wait_for_function_internal(expression, Some(&self.selector), options)
781            .await
782            .map(|_| ())
783            .map_err(|e| self.wrap_error_with_selector(e))
784    }
785
786    /// Serializes this locator as a screenshot `mask` entry — `{ frame, selector }`
787    /// with the frame sent as a channel reference — matching the protocol shape
788    /// the driver expects. Used by [`crate::protocol::ScreenshotOptions`].
789    pub(crate) fn mask_json(&self) -> serde_json::Value {
790        use crate::server::channel_owner::ChannelOwner as _;
791        serde_json::json!({
792            "frame": { "guid": self.frame.guid() },
793            "selector": self.selector,
794        })
795    }
796
797    /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) scoped within this locator's subtree.
798    ///
799    /// The `selector` identifies an iframe element within the locator's scope.
800    ///
801    /// See: <https://playwright.dev/docs/api/class-locator#locator-frame-locator>
802    pub fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
803        crate::protocol::FrameLocator::new(
804            Arc::clone(&self.frame),
805            format!("{} >> {}", self.selector, selector),
806            self.page.clone(),
807        )
808    }
809
810    /// Returns the Page this locator belongs to.
811    ///
812    /// Each locator is bound to the page that created it. Chained locators (via
813    /// `first()`, `last()`, `nth()`, `locator()`, `filter()`, etc.) all return
814    /// the same owning page. This matches the behavior of `locator.page` in
815    /// other Playwright language bindings.
816    ///
817    /// # Example
818    ///
819    /// ```no_run
820    /// # use playwright_rs::Playwright;
821    /// # #[tokio::main]
822    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
823    /// let playwright = Playwright::launch().await?;
824    /// let browser = playwright.chromium().launch().await?;
825    /// let page = browser.new_page().await?;
826    /// page.goto("https://example.com", None).await?;
827    ///
828    /// let locator = page.locator("h1");
829    /// let locator_page = locator.page()?;
830    /// assert_eq!(locator_page.url(), page.url());
831    /// # Ok(())
832    /// # }
833    /// ```
834    ///
835    /// See: <https://playwright.dev/docs/api/class-locator#locator-page>
836    pub fn page(&self) -> Result<crate::protocol::Page> {
837        Ok(self.page.clone())
838    }
839
840    /// Evaluate a JavaScript expression in the frame context.
841    ///
842    /// Used internally for injecting CSS (e.g., disabling animations) before screenshot assertions.
843    #[cfg(feature = "screenshot-diff")]
844    pub(crate) async fn evaluate_js<T: serde::Serialize>(
845        &self,
846        expression: &str,
847        _arg: Option<T>,
848    ) -> Result<()> {
849        self.frame
850            .frame_evaluate_expression(expression)
851            .await
852            .map_err(|e| self.wrap_error_with_selector(e))
853    }
854
855    /// Creates a locator for the first matching element.
856    ///
857    /// See: <https://playwright.dev/docs/api/class-locator#locator-first>
858    pub fn first(&self) -> Locator {
859        Locator::new(
860            Arc::clone(&self.frame),
861            format!("{} >> nth=0", self.selector),
862            self.page.clone(),
863        )
864    }
865
866    /// Creates a locator for the last matching element.
867    ///
868    /// See: <https://playwright.dev/docs/api/class-locator#locator-last>
869    pub fn last(&self) -> Locator {
870        Locator::new(
871            Arc::clone(&self.frame),
872            format!("{} >> nth=-1", self.selector),
873            self.page.clone(),
874        )
875    }
876
877    /// Creates a locator for the nth matching element (0-indexed).
878    ///
879    /// See: <https://playwright.dev/docs/api/class-locator#locator-nth>
880    pub fn nth(&self, index: i32) -> Locator {
881        Locator::new(
882            Arc::clone(&self.frame),
883            format!("{} >> nth={}", self.selector, index),
884            self.page.clone(),
885        )
886    }
887
888    /// Returns a locator that matches elements containing the given text.
889    ///
890    /// By default, matching is case-insensitive and searches for a substring.
891    /// Set `exact` to `true` for case-sensitive exact matching.
892    ///
893    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-text>
894    pub fn get_by_text(&self, text: &str, exact: bool) -> Locator {
895        self.locator(get_by_text_selector(text, exact))
896    }
897
898    /// Returns a locator that matches elements by their associated label text.
899    ///
900    /// Targets form controls (`input`, `textarea`, `select`) linked via `<label>`,
901    /// `aria-label`, or `aria-labelledby`.
902    ///
903    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-label>
904    pub fn get_by_label(&self, text: &str, exact: bool) -> Locator {
905        self.locator(get_by_label_selector(text, exact))
906    }
907
908    /// Returns a locator that matches elements by their placeholder text.
909    ///
910    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-placeholder>
911    pub fn get_by_placeholder(&self, text: &str, exact: bool) -> Locator {
912        self.locator(get_by_placeholder_selector(text, exact))
913    }
914
915    /// Returns a locator that matches elements by their alt text.
916    ///
917    /// Typically used for `<img>` elements.
918    ///
919    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-alt-text>
920    pub fn get_by_alt_text(&self, text: &str, exact: bool) -> Locator {
921        self.locator(get_by_alt_text_selector(text, exact))
922    }
923
924    /// Returns a locator that matches elements by their title attribute.
925    ///
926    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-title>
927    pub fn get_by_title(&self, text: &str, exact: bool) -> Locator {
928        self.locator(get_by_title_selector(text, exact))
929    }
930
931    /// Returns a locator that matches elements by their test ID attribute.
932    ///
933    /// By default, uses the `data-testid` attribute. Call
934    /// `playwright.selectors().set_test_id_attribute()` to change the attribute name.
935    ///
936    /// Always uses exact matching (case-sensitive).
937    ///
938    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-test-id>
939    pub fn get_by_test_id(&self, test_id: &str) -> Locator {
940        use crate::server::channel_owner::ChannelOwner as _;
941        let attr = self.frame.connection().selectors().test_id_attribute();
942        self.locator(get_by_test_id_selector_with_attr(test_id, &attr))
943    }
944
945    /// Returns a locator that matches elements by their ARIA role.
946    ///
947    /// This is the recommended way to locate elements, as it matches the way
948    /// users and assistive technology perceive the page.
949    ///
950    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-role>
951    pub fn get_by_role(&self, role: AriaRole, options: Option<GetByRoleOptions>) -> Locator {
952        self.locator(get_by_role_selector(role, options))
953    }
954
955    /// Creates a sub-locator within this locator's subtree.
956    ///
957    /// See: <https://playwright.dev/docs/api/class-locator#locator-locator>
958    pub fn locator(&self, selector: impl Into<String>) -> Locator {
959        Locator::new(
960            Arc::clone(&self.frame),
961            format!("{} >> {}", self.selector, selector.into()),
962            self.page.clone(),
963        )
964    }
965
966    /// Narrows this locator according to the filter options.
967    ///
968    /// Can be chained to apply multiple filters in sequence.
969    ///
970    /// # Example
971    ///
972    /// ```no_run
973    /// use playwright_rs::{Playwright, FilterOptions};
974    ///
975    /// # #[tokio::main]
976    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
977    /// let playwright = Playwright::launch().await?;
978    /// let browser = playwright.chromium().launch().await?;
979    /// let page = browser.new_page().await?;
980    ///
981    /// // Filter rows to those containing "Apple"
982    /// let rows = page.locator("tr");
983    /// let apple_row = rows.filter(FilterOptions::default().has_text("Apple"));
984    /// # browser.close().await?;
985    /// # Ok(())
986    /// # }
987    /// ```
988    ///
989    /// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
990    pub fn filter(&self, options: FilterOptions) -> Locator {
991        let selector = filter_selector(
992            &self.selector,
993            &options,
994            options.has.as_ref().map(|l| l.selector.as_str()),
995            options.has_not.as_ref().map(|l| l.selector.as_str()),
996        );
997        Locator::new(Arc::clone(&self.frame), selector, self.page.clone())
998    }
999
1000    /// Returns a locator that matches only the visible elements of this one.
1001    ///
1002    /// This is the replacement Playwright recommends for the `:visible` CSS
1003    /// pseudo-class. `filter(FilterOptions::default().visible(false))` is the
1004    /// other half, matching only hidden elements.
1005    ///
1006    /// # Example
1007    ///
1008    /// ```no_run
1009    /// # use playwright_rs::Playwright;
1010    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1011    /// # let playwright = Playwright::launch().await?;
1012    /// # let browser = playwright.chromium().launch().await?;
1013    /// # let page = browser.new_page().await?;
1014    /// page.locator("button").visible().click(None).await?;
1015    /// # browser.close().await?;
1016    /// # Ok(())
1017    /// # }
1018    /// ```
1019    ///
1020    /// See: <https://playwright.dev/docs/api/class-locator#locator-visible>
1021    pub fn visible(&self) -> Locator {
1022        Locator::new(
1023            Arc::clone(&self.frame),
1024            format!("{} >> visible=true", self.selector),
1025            self.page.clone(),
1026        )
1027    }
1028
1029    /// Creates a locator matching elements that satisfy **both** this locator and `locator`.
1030    ///
1031    /// Note: named `and_` because `and` is a Rust keyword.
1032    ///
1033    /// # Example
1034    ///
1035    /// ```no_run
1036    /// use playwright_rs::Playwright;
1037    ///
1038    /// # #[tokio::main]
1039    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1040    /// let playwright = Playwright::launch().await?;
1041    /// let browser = playwright.chromium().launch().await?;
1042    /// let page = browser.new_page().await?;
1043    ///
1044    /// // Find a button that also has a specific title
1045    /// let button = page.locator("button");
1046    /// let titled = page.locator("[title='Subscribe']");
1047    /// let subscribe_btn = button.and_(&titled);
1048    /// # browser.close().await?;
1049    /// # Ok(())
1050    /// # }
1051    /// ```
1052    ///
1053    /// See: <https://playwright.dev/docs/api/class-locator#locator-and>
1054    pub fn and_(&self, locator: &Locator) -> Locator {
1055        let inner = serde_json::to_string(&locator.selector)
1056            .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
1057        Locator::new(
1058            Arc::clone(&self.frame),
1059            format!("{} >> internal:and={}", self.selector, inner),
1060            self.page.clone(),
1061        )
1062    }
1063
1064    /// Creates a locator matching elements that satisfy **either** this locator or `locator`.
1065    ///
1066    /// Note: named `or_` because `or` is a Rust keyword.
1067    ///
1068    /// # Example
1069    ///
1070    /// ```no_run
1071    /// use playwright_rs::Playwright;
1072    ///
1073    /// # #[tokio::main]
1074    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1075    /// let playwright = Playwright::launch().await?;
1076    /// let browser = playwright.chromium().launch().await?;
1077    /// let page = browser.new_page().await?;
1078    ///
1079    /// // Find any element that is either a button or a link
1080    /// let buttons = page.locator("button");
1081    /// let links = page.locator("a");
1082    /// let interactive = buttons.or_(&links);
1083    /// # browser.close().await?;
1084    /// # Ok(())
1085    /// # }
1086    /// ```
1087    ///
1088    /// See: <https://playwright.dev/docs/api/class-locator#locator-or>
1089    pub fn or_(&self, locator: &Locator) -> Locator {
1090        let inner = serde_json::to_string(&locator.selector)
1091            .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
1092        Locator::new(
1093            Arc::clone(&self.frame),
1094            format!("{} >> internal:or={}", self.selector, inner),
1095            self.page.clone(),
1096        )
1097    }
1098
1099    /// Returns the number of elements matching this locator.
1100    ///
1101    /// See: <https://playwright.dev/docs/api/class-locator#locator-count>
1102    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, count = tracing::field::Empty))]
1103    pub async fn count(&self) -> Result<usize> {
1104        let n = self
1105            .frame
1106            .locator_count(&self.selector)
1107            .await
1108            .map_err(|e| self.wrap_error_with_selector(e))?;
1109        tracing::Span::current().record("count", n);
1110        Ok(n)
1111    }
1112
1113    /// Returns an array of locators, one for each matching element.
1114    ///
1115    /// Note: `all()` does not wait for elements to match the locator,
1116    /// and instead immediately returns whatever is in the DOM.
1117    ///
1118    /// See: <https://playwright.dev/docs/api/class-locator#locator-all>
1119    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1120    pub async fn all(&self) -> Result<Vec<Locator>> {
1121        let count = self.count().await?;
1122        Ok((0..count).map(|i| self.nth(i as i32)).collect())
1123    }
1124
1125    /// Returns the text content of the element.
1126    ///
1127    /// See: <https://playwright.dev/docs/api/class-locator#locator-text-content>
1128    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1129    pub async fn text_content(&self) -> Result<Option<String>> {
1130        self.frame
1131            .locator_text_content(&self.selector)
1132            .await
1133            .map_err(|e| self.wrap_error_with_selector(e))
1134    }
1135
1136    /// Returns the inner text of the element (visible text).
1137    ///
1138    /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-text>
1139    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1140    pub async fn inner_text(&self) -> Result<String> {
1141        self.frame
1142            .locator_inner_text(&self.selector)
1143            .await
1144            .map_err(|e| self.wrap_error_with_selector(e))
1145    }
1146
1147    /// Returns the inner HTML of the element.
1148    ///
1149    /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-html>
1150    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1151    pub async fn inner_html(&self) -> Result<String> {
1152        self.frame
1153            .locator_inner_html(&self.selector)
1154            .await
1155            .map_err(|e| self.wrap_error_with_selector(e))
1156    }
1157
1158    /// Returns the value of the specified attribute.
1159    ///
1160    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-attribute>
1161    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, name = %name))]
1162    pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
1163        self.frame
1164            .locator_get_attribute(&self.selector, name)
1165            .await
1166            .map_err(|e| self.wrap_error_with_selector(e))
1167    }
1168
1169    /// Returns whether the element is visible.
1170    ///
1171    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-visible>
1172    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1173    pub async fn is_visible(&self) -> Result<bool> {
1174        self.frame
1175            .locator_is_visible(&self.selector)
1176            .await
1177            .map_err(|e| self.wrap_error_with_selector(e))
1178    }
1179
1180    /// Returns whether the element is enabled.
1181    ///
1182    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-enabled>
1183    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1184    pub async fn is_enabled(&self) -> Result<bool> {
1185        self.frame
1186            .locator_is_enabled(&self.selector)
1187            .await
1188            .map_err(|e| self.wrap_error_with_selector(e))
1189    }
1190
1191    /// Returns whether the checkbox or radio button is checked.
1192    ///
1193    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-checked>
1194    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1195    pub async fn is_checked(&self) -> Result<bool> {
1196        self.frame
1197            .locator_is_checked(&self.selector)
1198            .await
1199            .map_err(|e| self.wrap_error_with_selector(e))
1200    }
1201
1202    /// Returns whether the element is editable.
1203    ///
1204    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-editable>
1205    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1206    pub async fn is_editable(&self) -> Result<bool> {
1207        self.frame
1208            .locator_is_editable(&self.selector)
1209            .await
1210            .map_err(|e| self.wrap_error_with_selector(e))
1211    }
1212
1213    /// Returns whether the element is hidden.
1214    ///
1215    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-hidden>
1216    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1217    pub async fn is_hidden(&self) -> Result<bool> {
1218        self.frame
1219            .locator_is_hidden(&self.selector)
1220            .await
1221            .map_err(|e| self.wrap_error_with_selector(e))
1222    }
1223
1224    /// Returns whether the element is disabled.
1225    ///
1226    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-disabled>
1227    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1228    pub async fn is_disabled(&self) -> Result<bool> {
1229        self.frame
1230            .locator_is_disabled(&self.selector)
1231            .await
1232            .map_err(|e| self.wrap_error_with_selector(e))
1233    }
1234
1235    /// Returns whether the element is focused (currently has focus).
1236    ///
1237    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-focused>
1238    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1239    pub async fn is_focused(&self) -> Result<bool> {
1240        self.frame
1241            .locator_is_focused(&self.selector)
1242            .await
1243            .map_err(|e| self.wrap_error_with_selector(e))
1244    }
1245
1246    // Action methods
1247
1248    /// Clicks the element.
1249    ///
1250    /// See: <https://playwright.dev/docs/api/class-locator#locator-click>
1251    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1252    pub async fn click(
1253        &self,
1254        options: impl Into<Option<crate::protocol::ClickOptions>>,
1255    ) -> Result<()> {
1256        let options = options.into();
1257        self.frame
1258            .locator_click(&self.selector, Some(self.with_timeout(options)))
1259            .await
1260            .map_err(|e| self.wrap_error_with_selector(e))
1261    }
1262
1263    /// Ensures an options struct has the page's default timeout when none is explicitly set.
1264    fn with_timeout<T: HasTimeout + Default>(&self, options: Option<T>) -> T {
1265        let mut opts = options.unwrap_or_default();
1266        if opts.timeout_ref().is_none() {
1267            *opts.timeout_ref_mut() = Some(self.page.default_timeout_ms());
1268        }
1269        opts
1270    }
1271
1272    /// Wraps an error with selector context for better error messages.
1273    fn wrap_error_with_selector(&self, error: crate::error::Error) -> crate::error::Error {
1274        match &error {
1275            crate::error::Error::ProtocolError(msg) => {
1276                // Add selector context to protocol errors (timeouts, etc.)
1277                crate::error::Error::ProtocolError(format!("{} [selector: {}]", msg, self.selector))
1278            }
1279            crate::error::Error::Timeout(msg) => {
1280                crate::error::Error::Timeout(format!("{} [selector: {}]", msg, self.selector))
1281            }
1282            _ => error, // Other errors pass through unchanged
1283        }
1284    }
1285
1286    /// Double clicks the element.
1287    ///
1288    /// See: <https://playwright.dev/docs/api/class-locator#locator-dblclick>
1289    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1290    pub async fn dblclick(
1291        &self,
1292        options: impl Into<Option<crate::protocol::ClickOptions>>,
1293    ) -> Result<()> {
1294        let options = options.into();
1295        self.frame
1296            .locator_dblclick(&self.selector, Some(self.with_timeout(options)))
1297            .await
1298            .map_err(|e| self.wrap_error_with_selector(e))
1299    }
1300
1301    /// Fills the element with text.
1302    ///
1303    /// See: <https://playwright.dev/docs/api/class-locator#locator-fill>
1304    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1305    pub async fn fill(
1306        &self,
1307        text: &str,
1308        options: impl Into<Option<crate::protocol::FillOptions>>,
1309    ) -> Result<()> {
1310        let options = options.into();
1311        self.frame
1312            .locator_fill(&self.selector, text, Some(self.with_timeout(options)))
1313            .await
1314            .map_err(|e| self.wrap_error_with_selector(e))
1315    }
1316
1317    /// Clears the element's value.
1318    ///
1319    /// See: <https://playwright.dev/docs/api/class-locator#locator-clear>
1320    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1321    pub async fn clear(
1322        &self,
1323        options: impl Into<Option<crate::protocol::FillOptions>>,
1324    ) -> Result<()> {
1325        let options = options.into();
1326        self.frame
1327            .locator_clear(&self.selector, Some(self.with_timeout(options)))
1328            .await
1329            .map_err(|e| self.wrap_error_with_selector(e))
1330    }
1331
1332    /// Presses a key on the element.
1333    ///
1334    /// See: <https://playwright.dev/docs/api/class-locator#locator-press>
1335    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1336    pub async fn press(
1337        &self,
1338        key: &str,
1339        options: impl Into<Option<crate::protocol::PressOptions>>,
1340    ) -> Result<()> {
1341        let options = options.into();
1342        self.frame
1343            .locator_press(&self.selector, key, Some(self.with_timeout(options)))
1344            .await
1345            .map_err(|e| self.wrap_error_with_selector(e))
1346    }
1347
1348    /// Sets focus on the element.
1349    ///
1350    /// Calls the element's `focus()` method. Used to move keyboard focus to a
1351    /// specific element for subsequent keyboard interactions.
1352    ///
1353    /// See: <https://playwright.dev/docs/api/class-locator#locator-focus>
1354    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1355    pub async fn focus(&self) -> Result<()> {
1356        self.frame
1357            .locator_focus(&self.selector)
1358            .await
1359            .map_err(|e| self.wrap_error_with_selector(e))
1360    }
1361
1362    /// Removes focus from the element.
1363    ///
1364    /// Calls the element's `blur()` method. Moves keyboard focus away from the element.
1365    ///
1366    /// See: <https://playwright.dev/docs/api/class-locator#locator-blur>
1367    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1368    pub async fn blur(&self) -> Result<()> {
1369        self.frame
1370            .locator_blur(&self.selector)
1371            .await
1372            .map_err(|e| self.wrap_error_with_selector(e))
1373    }
1374
1375    /// Types `text` into the element character by character, as though it was typed
1376    /// on a real keyboard.
1377    ///
1378    /// Use this method when you need to simulate keystrokes with individual key events
1379    /// (e.g., for autocomplete widgets). For simply setting a field value, prefer
1380    /// [`Locator::fill()`].
1381    ///
1382    /// # Arguments
1383    ///
1384    /// * `text` - Text to type into the element
1385    /// * `options` - Optional [`PressSequentiallyOptions`](crate::protocol::PressSequentiallyOptions) (e.g., `delay` between key presses)
1386    ///
1387    /// See: <https://playwright.dev/docs/api/class-locator#locator-press-sequentially>
1388    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1389    pub async fn press_sequentially(
1390        &self,
1391        text: &str,
1392        options: impl Into<Option<crate::protocol::PressSequentiallyOptions>>,
1393    ) -> Result<()> {
1394        let options = options.into();
1395        self.frame
1396            .locator_press_sequentially(&self.selector, text, options)
1397            .await
1398            .map_err(|e| self.wrap_error_with_selector(e))
1399    }
1400
1401    /// Returns the `innerText` values of all elements matching this locator.
1402    ///
1403    /// Unlike [`Locator::inner_text()`] (which uses strict mode and requires exactly one match),
1404    /// `all_inner_texts()` returns text from all matching elements.
1405    ///
1406    /// See: <https://playwright.dev/docs/api/class-locator#locator-all-inner-texts>
1407    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1408    pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
1409        self.frame
1410            .locator_all_inner_texts(&self.selector)
1411            .await
1412            .map_err(|e| self.wrap_error_with_selector(e))
1413    }
1414
1415    /// Returns the `textContent` values of all elements matching this locator.
1416    ///
1417    /// Unlike [`Locator::text_content()`] (which uses strict mode and requires exactly one match),
1418    /// `all_text_contents()` returns text from all matching elements.
1419    ///
1420    /// See: <https://playwright.dev/docs/api/class-locator#locator-all-text-contents>
1421    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1422    pub async fn all_text_contents(&self) -> Result<Vec<String>> {
1423        self.frame
1424            .locator_all_text_contents(&self.selector)
1425            .await
1426            .map_err(|e| self.wrap_error_with_selector(e))
1427    }
1428
1429    /// Ensures the checkbox or radio button is checked.
1430    ///
1431    /// This method is idempotent - if already checked, does nothing.
1432    ///
1433    /// See: <https://playwright.dev/docs/api/class-locator#locator-check>
1434    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1435    pub async fn check(
1436        &self,
1437        options: impl Into<Option<crate::protocol::CheckOptions>>,
1438    ) -> Result<()> {
1439        let options = options.into();
1440        self.frame
1441            .locator_check(&self.selector, Some(self.with_timeout(options)))
1442            .await
1443            .map_err(|e| self.wrap_error_with_selector(e))
1444    }
1445
1446    /// Ensures the checkbox is unchecked.
1447    ///
1448    /// This method is idempotent - if already unchecked, does nothing.
1449    ///
1450    /// See: <https://playwright.dev/docs/api/class-locator#locator-uncheck>
1451    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1452    pub async fn uncheck(
1453        &self,
1454        options: impl Into<Option<crate::protocol::CheckOptions>>,
1455    ) -> Result<()> {
1456        let options = options.into();
1457        self.frame
1458            .locator_uncheck(&self.selector, Some(self.with_timeout(options)))
1459            .await
1460            .map_err(|e| self.wrap_error_with_selector(e))
1461    }
1462
1463    /// Sets the checkbox or radio button to the specified checked state.
1464    ///
1465    /// This is a convenience method that calls `check()` if `checked` is true,
1466    /// or `uncheck()` if `checked` is false.
1467    ///
1468    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-checked>
1469    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1470    pub async fn set_checked(
1471        &self,
1472        checked: bool,
1473        options: impl Into<Option<crate::protocol::CheckOptions>>,
1474    ) -> Result<()> {
1475        let options = options.into();
1476        if checked {
1477            self.check(options).await
1478        } else {
1479            self.uncheck(options).await
1480        }
1481    }
1482
1483    /// Hovers the mouse over the element.
1484    ///
1485    /// See: <https://playwright.dev/docs/api/class-locator#locator-hover>
1486    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1487    pub async fn hover(
1488        &self,
1489        options: impl Into<Option<crate::protocol::HoverOptions>>,
1490    ) -> Result<()> {
1491        let options = options.into();
1492        self.frame
1493            .locator_hover(&self.selector, Some(self.with_timeout(options)))
1494            .await
1495            .map_err(|e| self.wrap_error_with_selector(e))
1496    }
1497
1498    /// Returns the value of the input, textarea, or select element.
1499    ///
1500    /// See: <https://playwright.dev/docs/api/class-locator#locator-input-value>
1501    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1502    pub async fn input_value(&self, _options: impl Into<Option<()>>) -> Result<String> {
1503        self.frame
1504            .locator_input_value(&self.selector)
1505            .await
1506            .map_err(|e| self.wrap_error_with_selector(e))
1507    }
1508
1509    /// Selects one or more options in a select element.
1510    ///
1511    /// Returns an array of option values that have been successfully selected.
1512    ///
1513    /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1514    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1515    pub async fn select_option(
1516        &self,
1517        value: impl Into<crate::protocol::SelectOption>,
1518        options: impl Into<Option<crate::protocol::SelectOptions>>,
1519    ) -> Result<Vec<String>> {
1520        let options = options.into();
1521        self.frame
1522            .locator_select_option(
1523                &self.selector,
1524                value.into(),
1525                Some(self.with_timeout(options)),
1526            )
1527            .await
1528            .map_err(|e| self.wrap_error_with_selector(e))
1529    }
1530
1531    /// Selects multiple options in a select element.
1532    ///
1533    /// Returns an array of option values that have been successfully selected.
1534    ///
1535    /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1536    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1537    pub async fn select_option_multiple(
1538        &self,
1539        values: &[impl Into<crate::protocol::SelectOption> + Clone],
1540        options: impl Into<Option<crate::protocol::SelectOptions>>,
1541    ) -> Result<Vec<String>> {
1542        let options = options.into();
1543        let select_options: Vec<crate::protocol::SelectOption> =
1544            values.iter().map(|v| v.clone().into()).collect();
1545        self.frame
1546            .locator_select_option_multiple(
1547                &self.selector,
1548                select_options,
1549                Some(self.with_timeout(options)),
1550            )
1551            .await
1552            .map_err(|e| self.wrap_error_with_selector(e))
1553    }
1554
1555    /// Sets the file path(s) to upload to a file input element.
1556    ///
1557    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1558    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1559    pub async fn set_input_files(
1560        &self,
1561        file: &std::path::PathBuf,
1562        _options: impl Into<Option<()>>,
1563    ) -> Result<()> {
1564        self.frame
1565            .locator_set_input_files(&self.selector, file)
1566            .await
1567            .map_err(|e| self.wrap_error_with_selector(e))
1568    }
1569
1570    /// Sets multiple file paths to upload to a file input element.
1571    ///
1572    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1573    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1574    pub async fn set_input_files_multiple(
1575        &self,
1576        files: &[&std::path::PathBuf],
1577        _options: impl Into<Option<()>>,
1578    ) -> Result<()> {
1579        self.frame
1580            .locator_set_input_files_multiple(&self.selector, files)
1581            .await
1582            .map_err(|e| self.wrap_error_with_selector(e))
1583    }
1584
1585    /// Sets a file to upload using FilePayload (explicit name, mimeType, buffer).
1586    ///
1587    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1588    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1589    pub async fn set_input_files_payload(
1590        &self,
1591        file: crate::protocol::FilePayload,
1592        _options: impl Into<Option<()>>,
1593    ) -> Result<()> {
1594        self.frame
1595            .locator_set_input_files_payload(&self.selector, file)
1596            .await
1597            .map_err(|e| self.wrap_error_with_selector(e))
1598    }
1599
1600    /// Sets multiple files to upload using FilePayload.
1601    ///
1602    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1603    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1604    pub async fn set_input_files_payload_multiple(
1605        &self,
1606        files: &[crate::protocol::FilePayload],
1607        _options: impl Into<Option<()>>,
1608    ) -> Result<()> {
1609        self.frame
1610            .locator_set_input_files_payload_multiple(&self.selector, files)
1611            .await
1612            .map_err(|e| self.wrap_error_with_selector(e))
1613    }
1614
1615    /// Dispatches a DOM event on the element.
1616    ///
1617    /// Unlike clicking or typing, `dispatch_event` directly sends the event without
1618    /// performing any actionability checks. It still waits for the element to be present
1619    /// in the DOM.
1620    ///
1621    /// # Arguments
1622    ///
1623    /// * `type_` - The event type to dispatch, e.g. `"click"`, `"focus"`, `"myevent"`.
1624    /// * `event_init` - Optional event initializer properties (e.g. `{"detail": "value"}` for
1625    ///   `CustomEvent`). Corresponds to the second argument of `new Event(type, init)`.
1626    ///
1627    /// # Errors
1628    ///
1629    /// Returns an error if:
1630    /// - The element is not found within the timeout
1631    /// - The protocol call fails
1632    ///
1633    /// See: <https://playwright.dev/docs/api/class-locator#locator-dispatch-event>
1634    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1635    pub async fn dispatch_event(
1636        &self,
1637        type_: &str,
1638        event_init: Option<serde_json::Value>,
1639    ) -> Result<()> {
1640        self.frame
1641            .locator_dispatch_event(&self.selector, type_, event_init)
1642            .await
1643            .map_err(|e| self.wrap_error_with_selector(e))
1644    }
1645
1646    /// Returns the bounding box of the element, or `None` if the element is not visible.
1647    ///
1648    /// The bounding box is in pixels, relative to the top-left corner of the page.
1649    /// Returns `None` when the element has `display: none` or is otherwise not part of
1650    /// the layout.
1651    ///
1652    /// # Errors
1653    ///
1654    /// Returns an error if:
1655    /// - The element is not found within the timeout
1656    /// - The protocol call fails
1657    ///
1658    /// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
1659    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1660    pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
1661        self.frame
1662            .locator_bounding_box(&self.selector)
1663            .await
1664            .map_err(|e| self.wrap_error_with_selector(e))
1665    }
1666
1667    /// Scrolls the element into view if it is not already visible in the viewport.
1668    ///
1669    /// This is an alias for calling `element.scrollIntoView()` in the browser.
1670    ///
1671    /// # Errors
1672    ///
1673    /// Returns an error if:
1674    /// - The element is not found within the timeout
1675    /// - The protocol call fails
1676    ///
1677    /// See: <https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed>
1678    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1679    pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
1680        self.frame
1681            .locator_scroll_into_view_if_needed(&self.selector)
1682            .await
1683            .map_err(|e| self.wrap_error_with_selector(e))
1684    }
1685
1686    /// Takes a screenshot of the element and returns the image bytes.
1687    ///
1688    /// This method uses strict mode - it will fail if the selector matches multiple elements.
1689    /// Use `first()`, `last()`, or `nth()` to refine the selector to a single element.
1690    ///
1691    /// See: <https://playwright.dev/docs/api/class-locator#locator-screenshot>
1692    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector, bytes_len = tracing::field::Empty))]
1693    pub async fn screenshot(
1694        &self,
1695        options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
1696    ) -> Result<Vec<u8>> {
1697        let options = options.into();
1698        // Query for the element using strict mode (should return exactly one)
1699        let element = self
1700            .frame
1701            .query_selector(&self.selector)
1702            .await
1703            .map_err(|e| self.wrap_error_with_selector(e))?
1704            .ok_or_else(|| {
1705                crate::error::Error::ElementNotFound(format!(
1706                    "Element not found: {}",
1707                    self.selector
1708                ))
1709            })?;
1710
1711        // Delegate to ElementHandle.screenshot() with default timeout injected
1712        let bytes = element
1713            .screenshot(Some(self.with_timeout(options)))
1714            .await
1715            .map_err(|e| self.wrap_error_with_selector(e))?;
1716        tracing::Span::current().record("bytes_len", bytes.len());
1717        Ok(bytes)
1718    }
1719
1720    /// Performs a touch-tap on the element.
1721    ///
1722    /// This method dispatches a `touchstart` and `touchend` event on the element.
1723    /// For touch support to work, the browser context must be created with
1724    /// `has_touch: true`.
1725    ///
1726    /// # Arguments
1727    ///
1728    /// * `options` - Optional [`TapOptions`](crate::protocol::TapOptions) (force, modifiers, position, timeout, trial)
1729    ///
1730    /// # Errors
1731    ///
1732    /// Returns an error if:
1733    /// - The element is not found within the timeout
1734    /// - Actionability checks fail (unless `force: true`)
1735    /// - The browser context was not created with `has_touch: true`
1736    ///
1737    /// See: <https://playwright.dev/docs/api/class-locator#locator-tap>
1738    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1739    pub async fn tap(&self, options: impl Into<Option<crate::protocol::TapOptions>>) -> Result<()> {
1740        let options = options.into();
1741        self.frame
1742            .locator_tap(&self.selector, Some(self.with_timeout(options)))
1743            .await
1744            .map_err(|e| self.wrap_error_with_selector(e))
1745    }
1746
1747    /// Drags this element to the `target` element.
1748    ///
1749    /// Both this locator and `target` must resolve to elements in the same frame.
1750    /// Playwright performs a series of mouse events (move, press, move to target, release)
1751    /// to simulate the drag, so it drives real pointer-event chains, including
1752    /// UIs that call `setPointerCapture` in their `pointerdown` handler.
1753    ///
1754    /// `source_position` and `target_position` are offsets from the respective
1755    /// element's top-left corner. Setting `target_position` with a containing
1756    /// element (a canvas or stage) as `target` turns this into a drag to a
1757    /// coordinate rather than onto an element, which also covers "drag by a
1758    /// delta": compute the drop point from the source's position within the
1759    /// container. Prefer this over a held-button
1760    /// [`Mouse::move_to`](crate::protocol::Mouse::move_to) sequence, which can
1761    /// hang on headless Linux (see the note on that method).
1762    ///
1763    /// # Arguments
1764    ///
1765    /// * `target` - The locator of the element to drag onto
1766    /// * `options` - Optional [`DragToOptions`](crate::protocol::DragToOptions) (force, no_wait_after, timeout, trial,
1767    ///   source_position, target_position)
1768    ///
1769    /// # Example
1770    ///
1771    /// ```no_run
1772    /// # use playwright_rs::{Playwright, DragToOptions, Position};
1773    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1774    /// # let pw = Playwright::launch().await?;
1775    /// # let page = pw.chromium().launch().await?.new_page().await?;
1776    /// let handle = page.locator(".crop-handle");
1777    /// let stage = page.locator("#stage");
1778    ///
1779    /// // Drop the handle 180px right, 120px down from the stage's top-left corner.
1780    /// let opts = DragToOptions::builder()
1781    ///     .target_position(Position { x: 180.0, y: 120.0 })
1782    ///     .build();
1783    /// handle.drag_to(&stage, Some(opts)).await?;
1784    /// # Ok(())
1785    /// # }
1786    /// ```
1787    ///
1788    /// # Errors
1789    ///
1790    /// Returns an error if:
1791    /// - Either element is not found within the timeout
1792    /// - Actionability checks fail (unless `force: true`)
1793    /// - The protocol call fails
1794    ///
1795    /// See: <https://playwright.dev/docs/api/class-locator#locator-drag-to>
1796    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1797    pub async fn drag_to(
1798        &self,
1799        target: &Locator,
1800        options: impl Into<Option<crate::protocol::DragToOptions>>,
1801    ) -> Result<()> {
1802        let options = options.into();
1803        self.frame
1804            .locator_drag_to(
1805                &self.selector,
1806                &target.selector,
1807                Some(self.with_timeout(options)),
1808            )
1809            .await
1810            .map_err(|e| self.wrap_error_with_selector(e))
1811    }
1812
1813    /// Drops files and/or data onto this element (external drag-and-drop).
1814    ///
1815    /// Simulates dragging files or data from outside the page onto the element,
1816    /// such as an upload drop zone, by dispatching `dragenter`/`dragover`/`drop`
1817    /// with a synthetic `DataTransfer`. Set `files` and/or `data` on the
1818    /// [`DropOptions`](crate::protocol::DropOptions). This is distinct from
1819    /// [`drag_to`](Self::drag_to), which drags one element onto another within
1820    /// the page.
1821    ///
1822    /// # Arguments
1823    ///
1824    /// * `options` - [`DropOptions`](crate::protocol::DropOptions) carrying the
1825    ///   files / data to drop, plus optional `position` and `timeout`.
1826    ///
1827    /// # Errors
1828    ///
1829    /// Returns an error if:
1830    /// - The element is not found within the timeout
1831    /// - Actionability checks fail
1832    /// - The protocol call fails (e.g. neither files nor data were provided)
1833    ///
1834    /// See: <https://playwright.dev/docs/api/class-locator#locator-drop>
1835    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1836    pub async fn drop(&self, options: crate::protocol::DropOptions) -> Result<()> {
1837        self.frame
1838            .locator_drop(&self.selector, self.with_timeout(Some(options)))
1839            .await
1840            .map_err(|e| self.wrap_error_with_selector(e))
1841    }
1842
1843    /// Waits until the element satisfies the given state condition.
1844    ///
1845    /// If no state is specified, waits for the element to be `visible` (the default).
1846    ///
1847    /// This method is useful for waiting for lazy-rendered elements or elements that
1848    /// appear/disappear based on user interaction or async data loading.
1849    ///
1850    /// # Arguments
1851    ///
1852    /// * `options` - Optional [`WaitForOptions`](crate::protocol::WaitForOptions) specifying the `state` to wait for
1853    ///   (`Visible`, `Hidden`, `Attached`, or `Detached`) and a `timeout` in milliseconds.
1854    ///
1855    /// # Errors
1856    ///
1857    /// Returns an error if the element does not satisfy the expected state within the timeout.
1858    ///
1859    /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for>
1860    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1861    pub async fn wait_for(
1862        &self,
1863        options: impl Into<Option<crate::protocol::WaitForOptions>>,
1864    ) -> Result<()> {
1865        let options = options.into();
1866        self.frame
1867            .locator_wait_for(&self.selector, Some(self.with_timeout(options)))
1868            .await
1869            .map_err(|e| self.wrap_error_with_selector(e))
1870    }
1871
1872    /// Evaluates a JavaScript expression in the scope of the matched element.
1873    ///
1874    /// The element is passed as the first argument to the expression. The expression
1875    /// can be any JavaScript function or expression that returns a JSON-serializable value.
1876    ///
1877    /// # Arguments
1878    ///
1879    /// * `expression` - JavaScript expression or function, e.g. `"(el) => el.textContent"`
1880    /// * `arg` - Optional argument passed as the second argument to the function
1881    ///
1882    /// # Errors
1883    ///
1884    /// Returns an error if:
1885    /// - The element is not found within the timeout
1886    /// - The JavaScript expression throws an error
1887    /// - The return value is not JSON-serializable
1888    ///
1889    /// # Example
1890    ///
1891    /// ```no_run
1892    /// use playwright_rs::Playwright;
1893    ///
1894    /// # #[tokio::main]
1895    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1896    /// let playwright = Playwright::launch().await?;
1897    /// let browser = playwright.chromium().launch().await?;
1898    /// let page = browser.new_page().await?;
1899    /// let _ = page.goto("data:text/html,<h1>Hello</h1>", None).await;
1900    ///
1901    /// let heading = page.locator("h1");
1902    /// let text: String = heading.evaluate("(el) => el.textContent", None::<()>).await?;
1903    /// assert_eq!(text, "Hello");
1904    ///
1905    /// // With an argument
1906    /// let result: String = heading
1907    ///     .evaluate("(el, suffix) => el.textContent + suffix", Some("!"))
1908    ///     .await?;
1909    /// assert_eq!(result, "Hello!");
1910    /// # browser.close().await?;
1911    /// # Ok(())
1912    /// # }
1913    /// ```
1914    ///
1915    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate>
1916    #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1917    pub async fn evaluate<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1918    where
1919        R: serde::de::DeserializeOwned,
1920        T: serde::Serialize,
1921    {
1922        let raw = self
1923            .frame
1924            .locator_evaluate(&self.selector, expression, arg)
1925            .await
1926            .map_err(|e| self.wrap_error_with_selector(e))?;
1927        serde_json::from_value(raw).map_err(|e| {
1928            crate::error::Error::ProtocolError(format!(
1929                "evaluate result deserialization failed: {}",
1930                e
1931            ))
1932        })
1933    }
1934
1935    /// Evaluates a JavaScript expression in the scope of all elements matching this locator.
1936    ///
1937    /// The array of all matched elements is passed as the first argument to the expression.
1938    /// Unlike [`evaluate()`](Self::evaluate), this does not use strict mode — all matching
1939    /// elements are collected and passed as an array.
1940    ///
1941    /// # Arguments
1942    ///
1943    /// * `expression` - JavaScript function that receives an array of elements
1944    /// * `arg` - Optional argument passed as the second argument to the function
1945    ///
1946    /// # Errors
1947    ///
1948    /// Returns an error if:
1949    /// - The JavaScript expression throws an error
1950    /// - The return value is not JSON-serializable
1951    ///
1952    /// # Example
1953    ///
1954    /// ```no_run
1955    /// use playwright_rs::Playwright;
1956    ///
1957    /// # #[tokio::main]
1958    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1959    /// let playwright = Playwright::launch().await?;
1960    /// let browser = playwright.chromium().launch().await?;
1961    /// let page = browser.new_page().await?;
1962    /// let _ = page.goto(
1963    ///     "data:text/html,<li class='item'>A</li><li class='item'>B</li>",
1964    ///     None
1965    /// ).await;
1966    ///
1967    /// let items = page.locator(".item");
1968    /// let texts: Vec<String> = items
1969    ///     .evaluate_all("(elements) => elements.map(e => e.textContent)", None::<()>)
1970    ///     .await?;
1971    /// assert_eq!(texts, vec!["A", "B"]);
1972    /// # browser.close().await?;
1973    /// # Ok(())
1974    /// # }
1975    /// ```
1976    ///
1977    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate-all>
1978    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1979    pub async fn evaluate_all<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1980    where
1981        R: serde::de::DeserializeOwned,
1982        T: serde::Serialize,
1983    {
1984        let raw = self
1985            .frame
1986            .locator_evaluate_all(&self.selector, expression, arg)
1987            .await
1988            .map_err(|e| self.wrap_error_with_selector(e))?;
1989        serde_json::from_value(raw).map_err(|e| {
1990            crate::error::Error::ProtocolError(format!(
1991                "evaluate_all result deserialization failed: {}",
1992                e
1993            ))
1994        })
1995    }
1996
1997    /// Returns the ARIA accessibility tree snapshot as a YAML string.
1998    ///
1999    /// The snapshot describes the accessible roles, names, and properties of the matched
2000    /// element and its descendants. This is useful for writing stable accessibility assertions
2001    /// that are independent of CSS classes or DOM structure.
2002    ///
2003    /// # Errors
2004    ///
2005    /// Returns an error if:
2006    /// - The element is not found within the timeout
2007    /// - The protocol call fails
2008    ///
2009    /// See: <https://playwright.dev/docs/api/class-locator#locator-aria-snapshot>
2010    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, mode = tracing::field::Empty))]
2011    pub async fn aria_snapshot(
2012        &self,
2013        options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
2014    ) -> Result<String> {
2015        let options = options.into();
2016        self.frame
2017            .locator_aria_snapshot(&self.selector, options.as_ref())
2018            .await
2019            .map_err(|e| self.wrap_error_with_selector(e))
2020    }
2021
2022    /// The accessibility tree of the matched element, as JSON rather than
2023    /// the YAML markup [`aria_snapshot`](Self::aria_snapshot) returns, so a
2024    /// caller can walk it instead of parsing text.
2025    ///
2026    /// Takes the same options; `mode`, `depth`, and `boxes` all apply.
2027    ///
2028    /// # Errors
2029    ///
2030    /// Returns an error if the element is not found within the timeout, or
2031    /// if the driver rejects the request.
2032    ///
2033    /// See: <https://playwright.dev/docs/api/class-locator#locator-aria-snapshot-json>
2034    pub async fn aria_snapshot_json(
2035        &self,
2036        options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
2037    ) -> Result<serde_json::Value> {
2038        let options = options.into();
2039        self.frame
2040            .locator_aria_snapshot_json(&self.selector, options.as_ref())
2041            .await
2042            .map_err(|e| self.wrap_error_with_selector(e))
2043    }
2044
2045    /// Returns a new locator whose selector has been resolved to a
2046    /// best-practices canonical form — preferring test-ids, then ARIA
2047    /// roles, then accessible text. The resolved locator points at the
2048    /// same element(s) as `self` but uses a more robust selector that
2049    /// is less coupled to CSS classes or DOM structure. Useful as a
2050    /// building block for codegen helpers that want the "most stable
2051    /// selector for this element" primitive.
2052    ///
2053    /// See the module-level example for usage.
2054    ///
2055    /// # Errors
2056    ///
2057    /// Returns an error if:
2058    /// - No element matches the original selector
2059    /// - The protocol call fails
2060    ///
2061    /// See: <https://playwright.dev/docs/api/class-locator#locator-normalize>
2062    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
2063    pub async fn normalize(&self) -> Result<Locator> {
2064        let resolved = self
2065            .frame
2066            .frame_resolve_selector(&self.selector)
2067            .await
2068            .map_err(|e| self.wrap_error_with_selector(e))?;
2069        Ok(Locator {
2070            frame: Arc::clone(&self.frame),
2071            selector: resolved,
2072            page: self.page.clone(),
2073        })
2074    }
2075
2076    /// Returns a new Locator with an attached description for traces and error messages.
2077    ///
2078    /// The description does not affect element matching — it is purely informational,
2079    /// appearing in trace viewer labels and error messages to make them more readable.
2080    ///
2081    /// Appends `>> internal:describe="description"` to the selector, matching
2082    /// playwright-python's behavior exactly.
2083    ///
2084    /// See: <https://playwright.dev/docs/api/class-locator#locator-describe>
2085    pub fn describe(&self, description: &str) -> Locator {
2086        let escaped =
2087            serde_json::to_string(description).unwrap_or_else(|_| format!("\"{}\"", description));
2088        Locator::new(
2089            Arc::clone(&self.frame),
2090            format!("{} >> internal:describe={}", self.selector, escaped),
2091            self.page.clone(),
2092        )
2093    }
2094
2095    /// Highlights the matched element in the browser for visual debugging.
2096    ///
2097    /// Draws a colored overlay over the element for a short period. This is a
2098    /// debugging tool and has no effect on test assertions or element state.
2099    ///
2100    /// # Errors
2101    ///
2102    /// Returns an error if:
2103    /// - The element is not found within the timeout
2104    /// - The protocol call fails
2105    ///
2106    /// See: <https://playwright.dev/docs/api/class-locator#locator-highlight>
2107    #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
2108    pub async fn highlight(&self, options: impl Into<Option<HighlightOptions>>) -> Result<()> {
2109        let options = options.into();
2110        let style = options.and_then(|o| o.style);
2111        self.frame
2112            .locator_highlight(&self.selector, style.as_deref())
2113            .await
2114            .map_err(|e| self.wrap_error_with_selector(e))
2115    }
2116
2117    /// Returns a [`FrameLocator`](crate::protocol::FrameLocator) for the content of an
2118    /// `<iframe>` element matched by this locator.
2119    ///
2120    /// This is a client-side operation — it creates a `FrameLocator` scoped to the matched
2121    /// iframe element, allowing you to interact with elements inside the iframe using the
2122    /// standard `FrameLocator` API.
2123    ///
2124    /// Equivalent to `page.frame_locator(selector)`, but starting from an existing `Locator`.
2125    ///
2126    /// See: <https://playwright.dev/docs/api/class-locator#locator-content-frame>
2127    pub fn content_frame(&self) -> crate::protocol::FrameLocator {
2128        crate::protocol::FrameLocator::new(
2129            Arc::clone(&self.frame),
2130            self.selector.clone(),
2131            self.page.clone(),
2132        )
2133    }
2134}
2135
2136impl std::fmt::Debug for Locator {
2137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2138        f.debug_struct("Locator")
2139            .field("selector", &self.selector)
2140            .finish()
2141    }
2142}
2143
2144#[cfg(test)]
2145mod tests {
2146    use super::*;
2147
2148    #[test]
2149    fn test_escape_for_selector_case_insensitive() {
2150        assert_eq!(escape_for_selector("hello", false), "\"hello\"i");
2151    }
2152
2153    #[test]
2154    fn test_escape_for_selector_exact() {
2155        assert_eq!(escape_for_selector("hello", true), "\"hello\"s");
2156    }
2157
2158    #[test]
2159    fn test_escape_for_selector_with_quotes() {
2160        assert_eq!(
2161            escape_for_selector("say \"hi\"", false),
2162            "\"say \\\"hi\\\"\"i"
2163        );
2164    }
2165
2166    #[test]
2167    fn test_get_by_text_selector_case_insensitive() {
2168        assert_eq!(
2169            get_by_text_selector("Click me", false),
2170            "internal:text=\"Click me\"i"
2171        );
2172    }
2173
2174    #[test]
2175    fn test_get_by_text_selector_exact() {
2176        assert_eq!(
2177            get_by_text_selector("Click me", true),
2178            "internal:text=\"Click me\"s"
2179        );
2180    }
2181
2182    #[test]
2183    fn test_get_by_label_selector() {
2184        assert_eq!(
2185            get_by_label_selector("Email", false),
2186            "internal:label=\"Email\"i"
2187        );
2188    }
2189
2190    #[test]
2191    fn test_get_by_placeholder_selector() {
2192        assert_eq!(
2193            get_by_placeholder_selector("Enter name", false),
2194            "internal:attr=[placeholder=\"Enter name\"i]"
2195        );
2196    }
2197
2198    #[test]
2199    fn test_get_by_alt_text_selector() {
2200        assert_eq!(
2201            get_by_alt_text_selector("Logo", true),
2202            "internal:attr=[alt=\"Logo\"s]"
2203        );
2204    }
2205
2206    #[test]
2207    fn test_get_by_title_selector() {
2208        assert_eq!(
2209            get_by_title_selector("Help", false),
2210            "internal:attr=[title=\"Help\"i]"
2211        );
2212    }
2213
2214    #[test]
2215    fn test_get_by_test_id_selector() {
2216        assert_eq!(
2217            get_by_test_id_selector("submit-btn"),
2218            "internal:testid=[data-testid=\"submit-btn\"s]"
2219        );
2220    }
2221
2222    #[test]
2223    fn test_escape_for_attribute_selector_case_insensitive() {
2224        assert_eq!(
2225            escape_for_attribute_selector("Submit", false),
2226            "\"Submit\"i"
2227        );
2228    }
2229
2230    #[test]
2231    fn test_escape_for_attribute_selector_exact() {
2232        assert_eq!(escape_for_attribute_selector("Submit", true), "\"Submit\"s");
2233    }
2234
2235    #[test]
2236    fn test_escape_for_attribute_selector_escapes_quotes() {
2237        assert_eq!(
2238            escape_for_attribute_selector("Say \"hello\"", false),
2239            "\"Say \\\"hello\\\"\"i"
2240        );
2241    }
2242
2243    #[test]
2244    fn test_escape_for_attribute_selector_escapes_backslashes() {
2245        assert_eq!(
2246            escape_for_attribute_selector("path\\to", true),
2247            "\"path\\\\to\"s"
2248        );
2249    }
2250
2251    #[test]
2252    fn test_get_by_role_selector_role_only() {
2253        assert_eq!(
2254            get_by_role_selector(AriaRole::Button, None),
2255            "internal:role=button"
2256        );
2257    }
2258
2259    #[test]
2260    fn test_get_by_role_selector_with_name() {
2261        let opts = GetByRoleOptions::default().name("Submit");
2262        assert_eq!(
2263            get_by_role_selector(AriaRole::Button, Some(opts)),
2264            "internal:role=button[name=\"Submit\"i]"
2265        );
2266    }
2267
2268    #[test]
2269    fn test_filter_options_setters() {
2270        let opts = FilterOptions::default()
2271            .has_text("a")
2272            .has_not_text("b")
2273            .visible(false);
2274        assert_eq!(opts.has_text.as_deref(), Some("a"));
2275        assert_eq!(opts.has_not_text.as_deref(), Some("b"));
2276        assert_eq!(opts.visible, Some(false));
2277        assert_eq!(FilterOptions::default().visible, None);
2278    }
2279
2280    #[test]
2281    fn filter_selector_appends_each_narrowing_in_order() {
2282        let options = FilterOptions::default()
2283            .has_text("Apple")
2284            .has_not_text("Pear")
2285            .visible(true);
2286
2287        assert_eq!(
2288            filter_selector("tr", &options, Some("td.price"), Some("td.sold")),
2289            "tr >> internal:has-text=\"Apple\"i >> internal:has-not-text=\"Pear\"i \
2290             >> internal:has=\"td.price\" >> internal:has-not=\"td.sold\" >> visible=true"
2291        );
2292    }
2293
2294    #[test]
2295    fn filter_selector_leaves_the_base_alone_when_nothing_is_set() {
2296        assert_eq!(
2297            filter_selector("tr", &FilterOptions::default(), None, None),
2298            "tr"
2299        );
2300    }
2301
2302    #[test]
2303    fn filter_selector_spells_the_hidden_half() {
2304        let options = FilterOptions::default().visible(false);
2305        assert_eq!(
2306            filter_selector(".item", &options, None, None),
2307            ".item >> visible=false"
2308        );
2309    }
2310
2311    #[test]
2312    fn test_get_by_role_selector_with_description() {
2313        let opts = GetByRoleOptions::default().description("Close dialog");
2314        assert_eq!(
2315            get_by_role_selector(AriaRole::Button, Some(opts)),
2316            "internal:role=button[description=\"Close dialog\"i]"
2317        );
2318        let exact = GetByRoleOptions::default().description("Close").exact(true);
2319        assert_eq!(
2320            get_by_role_selector(AriaRole::Button, Some(exact)),
2321            "internal:role=button[description=\"Close\"s]"
2322        );
2323    }
2324
2325    #[test]
2326    fn test_get_by_role_selector_with_name_exact() {
2327        let opts = GetByRoleOptions::default().name("Submit").exact(true);
2328        assert_eq!(
2329            get_by_role_selector(AriaRole::Button, Some(opts)),
2330            "internal:role=button[name=\"Submit\"s]"
2331        );
2332    }
2333
2334    #[test]
2335    fn test_get_by_role_selector_with_checked() {
2336        let opts = GetByRoleOptions::default().checked(true);
2337        assert_eq!(
2338            get_by_role_selector(AriaRole::Checkbox, Some(opts)),
2339            "internal:role=checkbox[checked=true]"
2340        );
2341    }
2342
2343    #[test]
2344    fn test_get_by_role_selector_with_level() {
2345        let opts = GetByRoleOptions::default().level(2);
2346        assert_eq!(
2347            get_by_role_selector(AriaRole::Heading, Some(opts)),
2348            "internal:role=heading[level=2]"
2349        );
2350    }
2351
2352    #[test]
2353    fn test_get_by_role_selector_with_disabled() {
2354        let opts = GetByRoleOptions::default().disabled(true);
2355        assert_eq!(
2356            get_by_role_selector(AriaRole::Button, Some(opts)),
2357            "internal:role=button[disabled=true]"
2358        );
2359    }
2360
2361    #[test]
2362    fn test_get_by_role_selector_with_selected() {
2363        let opts = GetByRoleOptions::default().selected(true);
2364        assert_eq!(
2365            get_by_role_selector(AriaRole::Option, Some(opts)),
2366            "internal:role=option[selected=true]"
2367        );
2368    }
2369
2370    #[test]
2371    fn test_get_by_role_selector_with_expanded() {
2372        let opts = GetByRoleOptions::default().expanded(true);
2373        assert_eq!(
2374            get_by_role_selector(AriaRole::Button, Some(opts)),
2375            "internal:role=button[expanded=true]"
2376        );
2377    }
2378
2379    #[test]
2380    fn test_get_by_role_selector_include_hidden() {
2381        let opts = GetByRoleOptions::default().include_hidden(true);
2382        assert_eq!(
2383            get_by_role_selector(AriaRole::Button, Some(opts)),
2384            "internal:role=button[include-hidden=true]"
2385        );
2386    }
2387
2388    #[test]
2389    fn test_get_by_role_selector_property_order() {
2390        // All properties: checked, disabled, selected, expanded, include-hidden, level, name, pressed
2391        let opts = GetByRoleOptions::default()
2392            .pressed(true)
2393            .name("OK")
2394            .checked(false)
2395            .disabled(true);
2396        assert_eq!(
2397            get_by_role_selector(AriaRole::Button, Some(opts)),
2398            "internal:role=button[checked=false][disabled=true][name=\"OK\"i][pressed=true]"
2399        );
2400    }
2401
2402    #[test]
2403    fn test_get_by_role_selector_name_with_special_chars() {
2404        let opts = GetByRoleOptions::default()
2405            .name("Click \"here\" now")
2406            .exact(true);
2407        assert_eq!(
2408            get_by_role_selector(AriaRole::Link, Some(opts)),
2409            "internal:role=link[name=\"Click \\\"here\\\" now\"s]"
2410        );
2411    }
2412
2413    #[test]
2414    fn test_aria_role_as_str() {
2415        assert_eq!(AriaRole::Button.as_str(), "button");
2416        assert_eq!(AriaRole::Heading.as_str(), "heading");
2417        assert_eq!(AriaRole::Link.as_str(), "link");
2418        assert_eq!(AriaRole::Checkbox.as_str(), "checkbox");
2419        assert_eq!(AriaRole::Alert.as_str(), "alert");
2420        assert_eq!(AriaRole::Navigation.as_str(), "navigation");
2421        assert_eq!(AriaRole::Progressbar.as_str(), "progressbar");
2422        assert_eq!(AriaRole::Treeitem.as_str(), "treeitem");
2423    }
2424
2425    #[test]
2426    fn highlight_options_style_setter_writes_the_field() {
2427        let opts = HighlightOptions::default().style("outline: 2px solid red");
2428        assert_eq!(opts.style.as_deref(), Some("outline: 2px solid red"));
2429    }
2430}