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