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