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