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    /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) scoped within this locator's subtree.
549    ///
550    /// The `selector` identifies an iframe element within the locator's scope.
551    ///
552    /// See: <https://playwright.dev/docs/api/class-locator#locator-frame-locator>
553    pub fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
554        crate::protocol::FrameLocator::new(
555            Arc::clone(&self.frame),
556            format!("{} >> {}", self.selector, selector),
557            self.page.clone(),
558        )
559    }
560
561    /// Returns the Page this locator belongs to.
562    ///
563    /// Each locator is bound to the page that created it. Chained locators (via
564    /// `first()`, `last()`, `nth()`, `locator()`, `filter()`, etc.) all return
565    /// the same owning page. This matches the behavior of `locator.page` in
566    /// other Playwright language bindings.
567    ///
568    /// # Example
569    ///
570    /// ```ignore
571    /// # use playwright_rs::Playwright;
572    /// # #[tokio::main]
573    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
574    /// let playwright = Playwright::launch().await?;
575    /// let browser = playwright.chromium().launch().await?;
576    /// let page = browser.new_page().await?;
577    /// page.goto("https://example.com", None).await?;
578    ///
579    /// let locator = page.locator("h1").await;
580    /// let locator_page = locator.page()?;
581    /// assert_eq!(locator_page.url(), page.url());
582    /// # Ok(())
583    /// # }
584    /// ```
585    ///
586    /// See: <https://playwright.dev/docs/api/class-locator#locator-page>
587    pub fn page(&self) -> Result<crate::protocol::Page> {
588        Ok(self.page.clone())
589    }
590
591    /// Evaluate a JavaScript expression in the frame context.
592    ///
593    /// Used internally for injecting CSS (e.g., disabling animations) before screenshot assertions.
594    pub(crate) async fn evaluate_js<T: serde::Serialize>(
595        &self,
596        expression: &str,
597        _arg: Option<T>,
598    ) -> Result<()> {
599        self.frame
600            .frame_evaluate_expression(expression)
601            .await
602            .map_err(|e| self.wrap_error_with_selector(e))
603    }
604
605    /// Creates a locator for the first matching element.
606    ///
607    /// See: <https://playwright.dev/docs/api/class-locator#locator-first>
608    pub fn first(&self) -> Locator {
609        Locator::new(
610            Arc::clone(&self.frame),
611            format!("{} >> nth=0", self.selector),
612            self.page.clone(),
613        )
614    }
615
616    /// Creates a locator for the last matching element.
617    ///
618    /// See: <https://playwright.dev/docs/api/class-locator#locator-last>
619    pub fn last(&self) -> Locator {
620        Locator::new(
621            Arc::clone(&self.frame),
622            format!("{} >> nth=-1", self.selector),
623            self.page.clone(),
624        )
625    }
626
627    /// Creates a locator for the nth matching element (0-indexed).
628    ///
629    /// See: <https://playwright.dev/docs/api/class-locator#locator-nth>
630    pub fn nth(&self, index: i32) -> Locator {
631        Locator::new(
632            Arc::clone(&self.frame),
633            format!("{} >> nth={}", self.selector, index),
634            self.page.clone(),
635        )
636    }
637
638    /// Returns a locator that matches elements containing the given text.
639    ///
640    /// By default, matching is case-insensitive and searches for a substring.
641    /// Set `exact` to `true` for case-sensitive exact matching.
642    ///
643    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-text>
644    pub fn get_by_text(&self, text: &str, exact: bool) -> Locator {
645        self.locator(&get_by_text_selector(text, exact))
646    }
647
648    /// Returns a locator that matches elements by their associated label text.
649    ///
650    /// Targets form controls (`input`, `textarea`, `select`) linked via `<label>`,
651    /// `aria-label`, or `aria-labelledby`.
652    ///
653    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-label>
654    pub fn get_by_label(&self, text: &str, exact: bool) -> Locator {
655        self.locator(&get_by_label_selector(text, exact))
656    }
657
658    /// Returns a locator that matches elements by their placeholder text.
659    ///
660    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-placeholder>
661    pub fn get_by_placeholder(&self, text: &str, exact: bool) -> Locator {
662        self.locator(&get_by_placeholder_selector(text, exact))
663    }
664
665    /// Returns a locator that matches elements by their alt text.
666    ///
667    /// Typically used for `<img>` elements.
668    ///
669    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-alt-text>
670    pub fn get_by_alt_text(&self, text: &str, exact: bool) -> Locator {
671        self.locator(&get_by_alt_text_selector(text, exact))
672    }
673
674    /// Returns a locator that matches elements by their title attribute.
675    ///
676    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-title>
677    pub fn get_by_title(&self, text: &str, exact: bool) -> Locator {
678        self.locator(&get_by_title_selector(text, exact))
679    }
680
681    /// Returns a locator that matches elements by their test ID attribute.
682    ///
683    /// By default, uses the `data-testid` attribute. Call
684    /// `playwright.selectors().set_test_id_attribute()` to change the attribute name.
685    ///
686    /// Always uses exact matching (case-sensitive).
687    ///
688    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-test-id>
689    pub fn get_by_test_id(&self, test_id: &str) -> Locator {
690        use crate::server::channel_owner::ChannelOwner as _;
691        let attr = self.frame.connection().selectors().test_id_attribute();
692        self.locator(&get_by_test_id_selector_with_attr(test_id, &attr))
693    }
694
695    /// Returns a locator that matches elements by their ARIA role.
696    ///
697    /// This is the recommended way to locate elements, as it matches the way
698    /// users and assistive technology perceive the page.
699    ///
700    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-role>
701    pub fn get_by_role(&self, role: AriaRole, options: Option<GetByRoleOptions>) -> Locator {
702        self.locator(&get_by_role_selector(role, options))
703    }
704
705    /// Creates a sub-locator within this locator's subtree.
706    ///
707    /// See: <https://playwright.dev/docs/api/class-locator#locator-locator>
708    pub fn locator(&self, selector: &str) -> Locator {
709        Locator::new(
710            Arc::clone(&self.frame),
711            format!("{} >> {}", self.selector, selector),
712            self.page.clone(),
713        )
714    }
715
716    /// Narrows this locator according to the filter options.
717    ///
718    /// Can be chained to apply multiple filters in sequence.
719    ///
720    /// # Example
721    ///
722    /// ```ignore
723    /// use playwright_rs::{Playwright, FilterOptions};
724    ///
725    /// # #[tokio::main]
726    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
727    /// let playwright = Playwright::launch().await?;
728    /// let browser = playwright.chromium().launch().await?;
729    /// let page = browser.new_page().await?;
730    ///
731    /// // Filter rows to those containing "Apple"
732    /// let rows = page.locator("tr").await;
733    /// let apple_row = rows.filter(FilterOptions {
734    ///     has_text: Some("Apple".to_string()),
735    ///     ..Default::default()
736    /// });
737    /// # browser.close().await?;
738    /// # Ok(())
739    /// # }
740    /// ```
741    ///
742    /// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
743    pub fn filter(&self, options: FilterOptions) -> Locator {
744        let mut selector = self.selector.clone();
745
746        if let Some(text) = &options.has_text {
747            let escaped = escape_for_selector(text, false);
748            selector = format!("{} >> internal:has-text={}", selector, escaped);
749        }
750
751        if let Some(text) = &options.has_not_text {
752            let escaped = escape_for_selector(text, false);
753            selector = format!("{} >> internal:has-not-text={}", selector, escaped);
754        }
755
756        if let Some(locator) = &options.has {
757            let inner = serde_json::to_string(&locator.selector)
758                .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
759            selector = format!("{} >> internal:has={}", selector, inner);
760        }
761
762        if let Some(locator) = &options.has_not {
763            let inner = serde_json::to_string(&locator.selector)
764                .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
765            selector = format!("{} >> internal:has-not={}", selector, inner);
766        }
767
768        Locator::new(Arc::clone(&self.frame), selector, self.page.clone())
769    }
770
771    /// Creates a locator matching elements that satisfy **both** this locator and `locator`.
772    ///
773    /// Note: named `and_` because `and` is a Rust keyword.
774    ///
775    /// # Example
776    ///
777    /// ```ignore
778    /// use playwright_rs::Playwright;
779    ///
780    /// # #[tokio::main]
781    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
782    /// let playwright = Playwright::launch().await?;
783    /// let browser = playwright.chromium().launch().await?;
784    /// let page = browser.new_page().await?;
785    ///
786    /// // Find a button that also has a specific title
787    /// let button = page.locator("button").await;
788    /// let titled = page.locator("[title='Subscribe']").await;
789    /// let subscribe_btn = button.and_(&titled);
790    /// # browser.close().await?;
791    /// # Ok(())
792    /// # }
793    /// ```
794    ///
795    /// See: <https://playwright.dev/docs/api/class-locator#locator-and>
796    pub fn and_(&self, locator: &Locator) -> Locator {
797        let inner = serde_json::to_string(&locator.selector)
798            .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
799        Locator::new(
800            Arc::clone(&self.frame),
801            format!("{} >> internal:and={}", self.selector, inner),
802            self.page.clone(),
803        )
804    }
805
806    /// Creates a locator matching elements that satisfy **either** this locator or `locator`.
807    ///
808    /// Note: named `or_` because `or` is a Rust keyword.
809    ///
810    /// # Example
811    ///
812    /// ```ignore
813    /// use playwright_rs::Playwright;
814    ///
815    /// # #[tokio::main]
816    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
817    /// let playwright = Playwright::launch().await?;
818    /// let browser = playwright.chromium().launch().await?;
819    /// let page = browser.new_page().await?;
820    ///
821    /// // Find any element that is either a button or a link
822    /// let buttons = page.locator("button").await;
823    /// let links = page.locator("a").await;
824    /// let interactive = buttons.or_(&links);
825    /// # browser.close().await?;
826    /// # Ok(())
827    /// # }
828    /// ```
829    ///
830    /// See: <https://playwright.dev/docs/api/class-locator#locator-or>
831    pub fn or_(&self, locator: &Locator) -> Locator {
832        let inner = serde_json::to_string(&locator.selector)
833            .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
834        Locator::new(
835            Arc::clone(&self.frame),
836            format!("{} >> internal:or={}", self.selector, inner),
837            self.page.clone(),
838        )
839    }
840
841    /// Returns the number of elements matching this locator.
842    ///
843    /// See: <https://playwright.dev/docs/api/class-locator#locator-count>
844    pub async fn count(&self) -> Result<usize> {
845        self.frame
846            .locator_count(&self.selector)
847            .await
848            .map_err(|e| self.wrap_error_with_selector(e))
849    }
850
851    /// Returns an array of locators, one for each matching element.
852    ///
853    /// Note: `all()` does not wait for elements to match the locator,
854    /// and instead immediately returns whatever is in the DOM.
855    ///
856    /// See: <https://playwright.dev/docs/api/class-locator#locator-all>
857    pub async fn all(&self) -> Result<Vec<Locator>> {
858        let count = self.count().await?;
859        Ok((0..count).map(|i| self.nth(i as i32)).collect())
860    }
861
862    /// Returns the text content of the element.
863    ///
864    /// See: <https://playwright.dev/docs/api/class-locator#locator-text-content>
865    pub async fn text_content(&self) -> Result<Option<String>> {
866        self.frame
867            .locator_text_content(&self.selector)
868            .await
869            .map_err(|e| self.wrap_error_with_selector(e))
870    }
871
872    /// Returns the inner text of the element (visible text).
873    ///
874    /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-text>
875    pub async fn inner_text(&self) -> Result<String> {
876        self.frame
877            .locator_inner_text(&self.selector)
878            .await
879            .map_err(|e| self.wrap_error_with_selector(e))
880    }
881
882    /// Returns the inner HTML of the element.
883    ///
884    /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-html>
885    pub async fn inner_html(&self) -> Result<String> {
886        self.frame
887            .locator_inner_html(&self.selector)
888            .await
889            .map_err(|e| self.wrap_error_with_selector(e))
890    }
891
892    /// Returns the value of the specified attribute.
893    ///
894    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-attribute>
895    pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
896        self.frame
897            .locator_get_attribute(&self.selector, name)
898            .await
899            .map_err(|e| self.wrap_error_with_selector(e))
900    }
901
902    /// Returns whether the element is visible.
903    ///
904    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-visible>
905    pub async fn is_visible(&self) -> Result<bool> {
906        self.frame
907            .locator_is_visible(&self.selector)
908            .await
909            .map_err(|e| self.wrap_error_with_selector(e))
910    }
911
912    /// Returns whether the element is enabled.
913    ///
914    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-enabled>
915    pub async fn is_enabled(&self) -> Result<bool> {
916        self.frame
917            .locator_is_enabled(&self.selector)
918            .await
919            .map_err(|e| self.wrap_error_with_selector(e))
920    }
921
922    /// Returns whether the checkbox or radio button is checked.
923    ///
924    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-checked>
925    pub async fn is_checked(&self) -> Result<bool> {
926        self.frame
927            .locator_is_checked(&self.selector)
928            .await
929            .map_err(|e| self.wrap_error_with_selector(e))
930    }
931
932    /// Returns whether the element is editable.
933    ///
934    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-editable>
935    pub async fn is_editable(&self) -> Result<bool> {
936        self.frame
937            .locator_is_editable(&self.selector)
938            .await
939            .map_err(|e| self.wrap_error_with_selector(e))
940    }
941
942    /// Returns whether the element is hidden.
943    ///
944    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-hidden>
945    pub async fn is_hidden(&self) -> Result<bool> {
946        self.frame
947            .locator_is_hidden(&self.selector)
948            .await
949            .map_err(|e| self.wrap_error_with_selector(e))
950    }
951
952    /// Returns whether the element is disabled.
953    ///
954    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-disabled>
955    pub async fn is_disabled(&self) -> Result<bool> {
956        self.frame
957            .locator_is_disabled(&self.selector)
958            .await
959            .map_err(|e| self.wrap_error_with_selector(e))
960    }
961
962    /// Returns whether the element is focused (currently has focus).
963    ///
964    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-focused>
965    pub async fn is_focused(&self) -> Result<bool> {
966        self.frame
967            .locator_is_focused(&self.selector)
968            .await
969            .map_err(|e| self.wrap_error_with_selector(e))
970    }
971
972    // Action methods
973
974    /// Clicks the element.
975    ///
976    /// See: <https://playwright.dev/docs/api/class-locator#locator-click>
977    pub async fn click(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
978        self.frame
979            .locator_click(&self.selector, Some(self.with_timeout(options)))
980            .await
981            .map_err(|e| self.wrap_error_with_selector(e))
982    }
983
984    /// Ensures an options struct has the page's default timeout when none is explicitly set.
985    fn with_timeout<T: HasTimeout + Default>(&self, options: Option<T>) -> T {
986        let mut opts = options.unwrap_or_default();
987        if opts.timeout_ref().is_none() {
988            *opts.timeout_ref_mut() = Some(self.page.default_timeout_ms());
989        }
990        opts
991    }
992
993    /// Wraps an error with selector context for better error messages.
994    fn wrap_error_with_selector(&self, error: crate::error::Error) -> crate::error::Error {
995        match &error {
996            crate::error::Error::ProtocolError(msg) => {
997                // Add selector context to protocol errors (timeouts, etc.)
998                crate::error::Error::ProtocolError(format!("{} [selector: {}]", msg, self.selector))
999            }
1000            crate::error::Error::Timeout(msg) => {
1001                crate::error::Error::Timeout(format!("{} [selector: {}]", msg, self.selector))
1002            }
1003            _ => error, // Other errors pass through unchanged
1004        }
1005    }
1006
1007    /// Double clicks the element.
1008    ///
1009    /// See: <https://playwright.dev/docs/api/class-locator#locator-dblclick>
1010    pub async fn dblclick(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
1011        self.frame
1012            .locator_dblclick(&self.selector, Some(self.with_timeout(options)))
1013            .await
1014            .map_err(|e| self.wrap_error_with_selector(e))
1015    }
1016
1017    /// Fills the element with text.
1018    ///
1019    /// See: <https://playwright.dev/docs/api/class-locator#locator-fill>
1020    pub async fn fill(
1021        &self,
1022        text: &str,
1023        options: Option<crate::protocol::FillOptions>,
1024    ) -> Result<()> {
1025        self.frame
1026            .locator_fill(&self.selector, text, Some(self.with_timeout(options)))
1027            .await
1028            .map_err(|e| self.wrap_error_with_selector(e))
1029    }
1030
1031    /// Clears the element's value.
1032    ///
1033    /// See: <https://playwright.dev/docs/api/class-locator#locator-clear>
1034    pub async fn clear(&self, options: Option<crate::protocol::FillOptions>) -> Result<()> {
1035        self.frame
1036            .locator_clear(&self.selector, Some(self.with_timeout(options)))
1037            .await
1038            .map_err(|e| self.wrap_error_with_selector(e))
1039    }
1040
1041    /// Presses a key on the element.
1042    ///
1043    /// See: <https://playwright.dev/docs/api/class-locator#locator-press>
1044    pub async fn press(
1045        &self,
1046        key: &str,
1047        options: Option<crate::protocol::PressOptions>,
1048    ) -> Result<()> {
1049        self.frame
1050            .locator_press(&self.selector, key, Some(self.with_timeout(options)))
1051            .await
1052            .map_err(|e| self.wrap_error_with_selector(e))
1053    }
1054
1055    /// Sets focus on the element.
1056    ///
1057    /// Calls the element's `focus()` method. Used to move keyboard focus to a
1058    /// specific element for subsequent keyboard interactions.
1059    ///
1060    /// See: <https://playwright.dev/docs/api/class-locator#locator-focus>
1061    pub async fn focus(&self) -> Result<()> {
1062        self.frame
1063            .locator_focus(&self.selector)
1064            .await
1065            .map_err(|e| self.wrap_error_with_selector(e))
1066    }
1067
1068    /// Removes focus from the element.
1069    ///
1070    /// Calls the element's `blur()` method. Moves keyboard focus away from the element.
1071    ///
1072    /// See: <https://playwright.dev/docs/api/class-locator#locator-blur>
1073    pub async fn blur(&self) -> Result<()> {
1074        self.frame
1075            .locator_blur(&self.selector)
1076            .await
1077            .map_err(|e| self.wrap_error_with_selector(e))
1078    }
1079
1080    /// Types `text` into the element character by character, as though it was typed
1081    /// on a real keyboard.
1082    ///
1083    /// Use this method when you need to simulate keystrokes with individual key events
1084    /// (e.g., for autocomplete widgets). For simply setting a field value, prefer
1085    /// [`Locator::fill()`].
1086    ///
1087    /// # Arguments
1088    ///
1089    /// * `text` - Text to type into the element
1090    /// * `options` - Optional [`PressSequentiallyOptions`](crate::protocol::PressSequentiallyOptions) (e.g., `delay` between key presses)
1091    ///
1092    /// See: <https://playwright.dev/docs/api/class-locator#locator-press-sequentially>
1093    pub async fn press_sequentially(
1094        &self,
1095        text: &str,
1096        options: Option<crate::protocol::PressSequentiallyOptions>,
1097    ) -> Result<()> {
1098        self.frame
1099            .locator_press_sequentially(&self.selector, text, options)
1100            .await
1101            .map_err(|e| self.wrap_error_with_selector(e))
1102    }
1103
1104    /// Returns the `innerText` values of all elements matching this locator.
1105    ///
1106    /// Unlike [`Locator::inner_text()`] (which uses strict mode and requires exactly one match),
1107    /// `all_inner_texts()` returns text from all matching elements.
1108    ///
1109    /// See: <https://playwright.dev/docs/api/class-locator#locator-all-inner-texts>
1110    pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
1111        self.frame
1112            .locator_all_inner_texts(&self.selector)
1113            .await
1114            .map_err(|e| self.wrap_error_with_selector(e))
1115    }
1116
1117    /// Returns the `textContent` values of all elements matching this locator.
1118    ///
1119    /// Unlike [`Locator::text_content()`] (which uses strict mode and requires exactly one match),
1120    /// `all_text_contents()` returns text from all matching elements.
1121    ///
1122    /// See: <https://playwright.dev/docs/api/class-locator#locator-all-text-contents>
1123    pub async fn all_text_contents(&self) -> Result<Vec<String>> {
1124        self.frame
1125            .locator_all_text_contents(&self.selector)
1126            .await
1127            .map_err(|e| self.wrap_error_with_selector(e))
1128    }
1129
1130    /// Ensures the checkbox or radio button is checked.
1131    ///
1132    /// This method is idempotent - if already checked, does nothing.
1133    ///
1134    /// See: <https://playwright.dev/docs/api/class-locator#locator-check>
1135    pub async fn check(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
1136        self.frame
1137            .locator_check(&self.selector, Some(self.with_timeout(options)))
1138            .await
1139            .map_err(|e| self.wrap_error_with_selector(e))
1140    }
1141
1142    /// Ensures the checkbox is unchecked.
1143    ///
1144    /// This method is idempotent - if already unchecked, does nothing.
1145    ///
1146    /// See: <https://playwright.dev/docs/api/class-locator#locator-uncheck>
1147    pub async fn uncheck(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
1148        self.frame
1149            .locator_uncheck(&self.selector, Some(self.with_timeout(options)))
1150            .await
1151            .map_err(|e| self.wrap_error_with_selector(e))
1152    }
1153
1154    /// Sets the checkbox or radio button to the specified checked state.
1155    ///
1156    /// This is a convenience method that calls `check()` if `checked` is true,
1157    /// or `uncheck()` if `checked` is false.
1158    ///
1159    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-checked>
1160    pub async fn set_checked(
1161        &self,
1162        checked: bool,
1163        options: Option<crate::protocol::CheckOptions>,
1164    ) -> Result<()> {
1165        if checked {
1166            self.check(options).await
1167        } else {
1168            self.uncheck(options).await
1169        }
1170    }
1171
1172    /// Hovers the mouse over the element.
1173    ///
1174    /// See: <https://playwright.dev/docs/api/class-locator#locator-hover>
1175    pub async fn hover(&self, options: Option<crate::protocol::HoverOptions>) -> Result<()> {
1176        self.frame
1177            .locator_hover(&self.selector, Some(self.with_timeout(options)))
1178            .await
1179            .map_err(|e| self.wrap_error_with_selector(e))
1180    }
1181
1182    /// Returns the value of the input, textarea, or select element.
1183    ///
1184    /// See: <https://playwright.dev/docs/api/class-locator#locator-input-value>
1185    pub async fn input_value(&self, _options: Option<()>) -> Result<String> {
1186        self.frame
1187            .locator_input_value(&self.selector)
1188            .await
1189            .map_err(|e| self.wrap_error_with_selector(e))
1190    }
1191
1192    /// Selects one or more options in a select element.
1193    ///
1194    /// Returns an array of option values that have been successfully selected.
1195    ///
1196    /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1197    pub async fn select_option(
1198        &self,
1199        value: impl Into<crate::protocol::SelectOption>,
1200        options: Option<crate::protocol::SelectOptions>,
1201    ) -> Result<Vec<String>> {
1202        self.frame
1203            .locator_select_option(
1204                &self.selector,
1205                value.into(),
1206                Some(self.with_timeout(options)),
1207            )
1208            .await
1209            .map_err(|e| self.wrap_error_with_selector(e))
1210    }
1211
1212    /// Selects multiple options in a select element.
1213    ///
1214    /// Returns an array of option values that have been successfully selected.
1215    ///
1216    /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1217    pub async fn select_option_multiple(
1218        &self,
1219        values: &[impl Into<crate::protocol::SelectOption> + Clone],
1220        options: Option<crate::protocol::SelectOptions>,
1221    ) -> Result<Vec<String>> {
1222        let select_options: Vec<crate::protocol::SelectOption> =
1223            values.iter().map(|v| v.clone().into()).collect();
1224        self.frame
1225            .locator_select_option_multiple(
1226                &self.selector,
1227                select_options,
1228                Some(self.with_timeout(options)),
1229            )
1230            .await
1231            .map_err(|e| self.wrap_error_with_selector(e))
1232    }
1233
1234    /// Sets the file path(s) to upload to a file input element.
1235    ///
1236    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1237    pub async fn set_input_files(
1238        &self,
1239        file: &std::path::PathBuf,
1240        _options: Option<()>,
1241    ) -> Result<()> {
1242        self.frame
1243            .locator_set_input_files(&self.selector, file)
1244            .await
1245            .map_err(|e| self.wrap_error_with_selector(e))
1246    }
1247
1248    /// Sets multiple file paths to upload to a file input element.
1249    ///
1250    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1251    pub async fn set_input_files_multiple(
1252        &self,
1253        files: &[&std::path::PathBuf],
1254        _options: Option<()>,
1255    ) -> Result<()> {
1256        self.frame
1257            .locator_set_input_files_multiple(&self.selector, files)
1258            .await
1259            .map_err(|e| self.wrap_error_with_selector(e))
1260    }
1261
1262    /// Sets a file to upload using FilePayload (explicit name, mimeType, buffer).
1263    ///
1264    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1265    pub async fn set_input_files_payload(
1266        &self,
1267        file: crate::protocol::FilePayload,
1268        _options: Option<()>,
1269    ) -> Result<()> {
1270        self.frame
1271            .locator_set_input_files_payload(&self.selector, file)
1272            .await
1273            .map_err(|e| self.wrap_error_with_selector(e))
1274    }
1275
1276    /// Sets multiple files to upload using FilePayload.
1277    ///
1278    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1279    pub async fn set_input_files_payload_multiple(
1280        &self,
1281        files: &[crate::protocol::FilePayload],
1282        _options: Option<()>,
1283    ) -> Result<()> {
1284        self.frame
1285            .locator_set_input_files_payload_multiple(&self.selector, files)
1286            .await
1287            .map_err(|e| self.wrap_error_with_selector(e))
1288    }
1289
1290    /// Dispatches a DOM event on the element.
1291    ///
1292    /// Unlike clicking or typing, `dispatch_event` directly sends the event without
1293    /// performing any actionability checks. It still waits for the element to be present
1294    /// in the DOM.
1295    ///
1296    /// # Arguments
1297    ///
1298    /// * `type_` - The event type to dispatch, e.g. `"click"`, `"focus"`, `"myevent"`.
1299    /// * `event_init` - Optional event initializer properties (e.g. `{"detail": "value"}` for
1300    ///   `CustomEvent`). Corresponds to the second argument of `new Event(type, init)`.
1301    ///
1302    /// # Errors
1303    ///
1304    /// Returns an error if:
1305    /// - The element is not found within the timeout
1306    /// - The protocol call fails
1307    ///
1308    /// See: <https://playwright.dev/docs/api/class-locator#locator-dispatch-event>
1309    pub async fn dispatch_event(
1310        &self,
1311        type_: &str,
1312        event_init: Option<serde_json::Value>,
1313    ) -> Result<()> {
1314        self.frame
1315            .locator_dispatch_event(&self.selector, type_, event_init)
1316            .await
1317            .map_err(|e| self.wrap_error_with_selector(e))
1318    }
1319
1320    /// Returns the bounding box of the element, or `None` if the element is not visible.
1321    ///
1322    /// The bounding box is in pixels, relative to the top-left corner of the page.
1323    /// Returns `None` when the element has `display: none` or is otherwise not part of
1324    /// the layout.
1325    ///
1326    /// # Errors
1327    ///
1328    /// Returns an error if:
1329    /// - The element is not found within the timeout
1330    /// - The protocol call fails
1331    ///
1332    /// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
1333    pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
1334        self.frame
1335            .locator_bounding_box(&self.selector)
1336            .await
1337            .map_err(|e| self.wrap_error_with_selector(e))
1338    }
1339
1340    /// Scrolls the element into view if it is not already visible in the viewport.
1341    ///
1342    /// This is an alias for calling `element.scrollIntoView()` in the browser.
1343    ///
1344    /// # Errors
1345    ///
1346    /// Returns an error if:
1347    /// - The element is not found within the timeout
1348    /// - The protocol call fails
1349    ///
1350    /// See: <https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed>
1351    pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
1352        self.frame
1353            .locator_scroll_into_view_if_needed(&self.selector)
1354            .await
1355            .map_err(|e| self.wrap_error_with_selector(e))
1356    }
1357
1358    /// Takes a screenshot of the element and returns the image bytes.
1359    ///
1360    /// This method uses strict mode - it will fail if the selector matches multiple elements.
1361    /// Use `first()`, `last()`, or `nth()` to refine the selector to a single element.
1362    ///
1363    /// See: <https://playwright.dev/docs/api/class-locator#locator-screenshot>
1364    pub async fn screenshot(
1365        &self,
1366        options: Option<crate::protocol::ScreenshotOptions>,
1367    ) -> Result<Vec<u8>> {
1368        // Query for the element using strict mode (should return exactly one)
1369        let element = self
1370            .frame
1371            .query_selector(&self.selector)
1372            .await
1373            .map_err(|e| self.wrap_error_with_selector(e))?
1374            .ok_or_else(|| {
1375                crate::error::Error::ElementNotFound(format!(
1376                    "Element not found: {}",
1377                    self.selector
1378                ))
1379            })?;
1380
1381        // Delegate to ElementHandle.screenshot() with default timeout injected
1382        element
1383            .screenshot(Some(self.with_timeout(options)))
1384            .await
1385            .map_err(|e| self.wrap_error_with_selector(e))
1386    }
1387
1388    /// Performs a touch-tap on the element.
1389    ///
1390    /// This method dispatches a `touchstart` and `touchend` event on the element.
1391    /// For touch support to work, the browser context must be created with
1392    /// `has_touch: true`.
1393    ///
1394    /// # Arguments
1395    ///
1396    /// * `options` - Optional [`TapOptions`](crate::protocol::TapOptions) (force, modifiers, position, timeout, trial)
1397    ///
1398    /// # Errors
1399    ///
1400    /// Returns an error if:
1401    /// - The element is not found within the timeout
1402    /// - Actionability checks fail (unless `force: true`)
1403    /// - The browser context was not created with `has_touch: true`
1404    ///
1405    /// See: <https://playwright.dev/docs/api/class-locator#locator-tap>
1406    pub async fn tap(&self, options: Option<crate::protocol::TapOptions>) -> Result<()> {
1407        self.frame
1408            .locator_tap(&self.selector, Some(self.with_timeout(options)))
1409            .await
1410            .map_err(|e| self.wrap_error_with_selector(e))
1411    }
1412
1413    /// Drags this element to the `target` element.
1414    ///
1415    /// Both this locator and `target` must resolve to elements in the same frame.
1416    /// Playwright performs a series of mouse events (move, press, move to target, release)
1417    /// to simulate the drag.
1418    ///
1419    /// # Arguments
1420    ///
1421    /// * `target` - The locator of the element to drag onto
1422    /// * `options` - Optional [`DragToOptions`](crate::protocol::DragToOptions) (force, no_wait_after, timeout, trial,
1423    ///   source_position, target_position)
1424    ///
1425    /// # Errors
1426    ///
1427    /// Returns an error if:
1428    /// - Either element is not found within the timeout
1429    /// - Actionability checks fail (unless `force: true`)
1430    /// - The protocol call fails
1431    ///
1432    /// See: <https://playwright.dev/docs/api/class-locator#locator-drag-to>
1433    pub async fn drag_to(
1434        &self,
1435        target: &Locator,
1436        options: Option<crate::protocol::DragToOptions>,
1437    ) -> Result<()> {
1438        self.frame
1439            .locator_drag_to(
1440                &self.selector,
1441                &target.selector,
1442                Some(self.with_timeout(options)),
1443            )
1444            .await
1445            .map_err(|e| self.wrap_error_with_selector(e))
1446    }
1447
1448    /// Waits until the element satisfies the given state condition.
1449    ///
1450    /// If no state is specified, waits for the element to be `visible` (the default).
1451    ///
1452    /// This method is useful for waiting for lazy-rendered elements or elements that
1453    /// appear/disappear based on user interaction or async data loading.
1454    ///
1455    /// # Arguments
1456    ///
1457    /// * `options` - Optional [`WaitForOptions`](crate::protocol::WaitForOptions) specifying the `state` to wait for
1458    ///   (`Visible`, `Hidden`, `Attached`, or `Detached`) and a `timeout` in milliseconds.
1459    ///
1460    /// # Errors
1461    ///
1462    /// Returns an error if the element does not satisfy the expected state within the timeout.
1463    ///
1464    /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for>
1465    pub async fn wait_for(&self, options: Option<crate::protocol::WaitForOptions>) -> Result<()> {
1466        self.frame
1467            .locator_wait_for(&self.selector, Some(self.with_timeout(options)))
1468            .await
1469            .map_err(|e| self.wrap_error_with_selector(e))
1470    }
1471
1472    /// Evaluates a JavaScript expression in the scope of the matched element.
1473    ///
1474    /// The element is passed as the first argument to the expression. The expression
1475    /// can be any JavaScript function or expression that returns a JSON-serializable value.
1476    ///
1477    /// # Arguments
1478    ///
1479    /// * `expression` - JavaScript expression or function, e.g. `"(el) => el.textContent"`
1480    /// * `arg` - Optional argument passed as the second argument to the function
1481    ///
1482    /// # Errors
1483    ///
1484    /// Returns an error if:
1485    /// - The element is not found within the timeout
1486    /// - The JavaScript expression throws an error
1487    /// - The return value is not JSON-serializable
1488    ///
1489    /// # Example
1490    ///
1491    /// ```ignore
1492    /// use playwright_rs::Playwright;
1493    ///
1494    /// # #[tokio::main]
1495    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1496    /// let playwright = Playwright::launch().await?;
1497    /// let browser = playwright.chromium().launch().await?;
1498    /// let page = browser.new_page().await?;
1499    /// let _ = page.goto("data:text/html,<h1>Hello</h1>", None).await;
1500    ///
1501    /// let heading = page.locator("h1").await;
1502    /// let text: String = heading.evaluate("(el) => el.textContent", None::<()>).await?;
1503    /// assert_eq!(text, "Hello");
1504    ///
1505    /// // With an argument
1506    /// let result: String = heading
1507    ///     .evaluate("(el, suffix) => el.textContent + suffix", Some("!"))
1508    ///     .await?;
1509    /// assert_eq!(result, "Hello!");
1510    /// # browser.close().await?;
1511    /// # Ok(())
1512    /// # }
1513    /// ```
1514    ///
1515    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate>
1516    pub async fn evaluate<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1517    where
1518        R: serde::de::DeserializeOwned,
1519        T: serde::Serialize,
1520    {
1521        let raw = self
1522            .frame
1523            .locator_evaluate(&self.selector, expression, arg)
1524            .await
1525            .map_err(|e| self.wrap_error_with_selector(e))?;
1526        serde_json::from_value(raw).map_err(|e| {
1527            crate::error::Error::ProtocolError(format!(
1528                "evaluate result deserialization failed: {}",
1529                e
1530            ))
1531        })
1532    }
1533
1534    /// Evaluates a JavaScript expression in the scope of all elements matching this locator.
1535    ///
1536    /// The array of all matched elements is passed as the first argument to the expression.
1537    /// Unlike [`evaluate()`](Self::evaluate), this does not use strict mode — all matching
1538    /// elements are collected and passed as an array.
1539    ///
1540    /// # Arguments
1541    ///
1542    /// * `expression` - JavaScript function that receives an array of elements
1543    /// * `arg` - Optional argument passed as the second argument to the function
1544    ///
1545    /// # Errors
1546    ///
1547    /// Returns an error if:
1548    /// - The JavaScript expression throws an error
1549    /// - The return value is not JSON-serializable
1550    ///
1551    /// # Example
1552    ///
1553    /// ```ignore
1554    /// use playwright_rs::Playwright;
1555    ///
1556    /// # #[tokio::main]
1557    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1558    /// let playwright = Playwright::launch().await?;
1559    /// let browser = playwright.chromium().launch().await?;
1560    /// let page = browser.new_page().await?;
1561    /// let _ = page.goto(
1562    ///     "data:text/html,<li class='item'>A</li><li class='item'>B</li>",
1563    ///     None
1564    /// ).await;
1565    ///
1566    /// let items = page.locator(".item").await;
1567    /// let texts: Vec<String> = items
1568    ///     .evaluate_all("(elements) => elements.map(e => e.textContent)", None::<()>)
1569    ///     .await?;
1570    /// assert_eq!(texts, vec!["A", "B"]);
1571    /// # browser.close().await?;
1572    /// # Ok(())
1573    /// # }
1574    /// ```
1575    ///
1576    /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate-all>
1577    pub async fn evaluate_all<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1578    where
1579        R: serde::de::DeserializeOwned,
1580        T: serde::Serialize,
1581    {
1582        let raw = self
1583            .frame
1584            .locator_evaluate_all(&self.selector, expression, arg)
1585            .await
1586            .map_err(|e| self.wrap_error_with_selector(e))?;
1587        serde_json::from_value(raw).map_err(|e| {
1588            crate::error::Error::ProtocolError(format!(
1589                "evaluate_all result deserialization failed: {}",
1590                e
1591            ))
1592        })
1593    }
1594}
1595
1596impl std::fmt::Debug for Locator {
1597    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1598        f.debug_struct("Locator")
1599            .field("selector", &self.selector)
1600            .finish()
1601    }
1602}
1603
1604#[cfg(test)]
1605mod tests {
1606    use super::*;
1607
1608    #[test]
1609    fn test_escape_for_selector_case_insensitive() {
1610        assert_eq!(escape_for_selector("hello", false), "\"hello\"i");
1611    }
1612
1613    #[test]
1614    fn test_escape_for_selector_exact() {
1615        assert_eq!(escape_for_selector("hello", true), "\"hello\"s");
1616    }
1617
1618    #[test]
1619    fn test_escape_for_selector_with_quotes() {
1620        assert_eq!(
1621            escape_for_selector("say \"hi\"", false),
1622            "\"say \\\"hi\\\"\"i"
1623        );
1624    }
1625
1626    #[test]
1627    fn test_get_by_text_selector_case_insensitive() {
1628        assert_eq!(
1629            get_by_text_selector("Click me", false),
1630            "internal:text=\"Click me\"i"
1631        );
1632    }
1633
1634    #[test]
1635    fn test_get_by_text_selector_exact() {
1636        assert_eq!(
1637            get_by_text_selector("Click me", true),
1638            "internal:text=\"Click me\"s"
1639        );
1640    }
1641
1642    #[test]
1643    fn test_get_by_label_selector() {
1644        assert_eq!(
1645            get_by_label_selector("Email", false),
1646            "internal:label=\"Email\"i"
1647        );
1648    }
1649
1650    #[test]
1651    fn test_get_by_placeholder_selector() {
1652        assert_eq!(
1653            get_by_placeholder_selector("Enter name", false),
1654            "internal:attr=[placeholder=\"Enter name\"i]"
1655        );
1656    }
1657
1658    #[test]
1659    fn test_get_by_alt_text_selector() {
1660        assert_eq!(
1661            get_by_alt_text_selector("Logo", true),
1662            "internal:attr=[alt=\"Logo\"s]"
1663        );
1664    }
1665
1666    #[test]
1667    fn test_get_by_title_selector() {
1668        assert_eq!(
1669            get_by_title_selector("Help", false),
1670            "internal:attr=[title=\"Help\"i]"
1671        );
1672    }
1673
1674    #[test]
1675    fn test_get_by_test_id_selector() {
1676        assert_eq!(
1677            get_by_test_id_selector("submit-btn"),
1678            "internal:testid=[data-testid=\"submit-btn\"s]"
1679        );
1680    }
1681
1682    #[test]
1683    fn test_escape_for_attribute_selector_case_insensitive() {
1684        assert_eq!(
1685            escape_for_attribute_selector("Submit", false),
1686            "\"Submit\"i"
1687        );
1688    }
1689
1690    #[test]
1691    fn test_escape_for_attribute_selector_exact() {
1692        assert_eq!(escape_for_attribute_selector("Submit", true), "\"Submit\"s");
1693    }
1694
1695    #[test]
1696    fn test_escape_for_attribute_selector_escapes_quotes() {
1697        assert_eq!(
1698            escape_for_attribute_selector("Say \"hello\"", false),
1699            "\"Say \\\"hello\\\"\"i"
1700        );
1701    }
1702
1703    #[test]
1704    fn test_escape_for_attribute_selector_escapes_backslashes() {
1705        assert_eq!(
1706            escape_for_attribute_selector("path\\to", true),
1707            "\"path\\\\to\"s"
1708        );
1709    }
1710
1711    #[test]
1712    fn test_get_by_role_selector_role_only() {
1713        assert_eq!(
1714            get_by_role_selector(AriaRole::Button, None),
1715            "internal:role=button"
1716        );
1717    }
1718
1719    #[test]
1720    fn test_get_by_role_selector_with_name() {
1721        let opts = GetByRoleOptions {
1722            name: Some("Submit".to_string()),
1723            ..Default::default()
1724        };
1725        assert_eq!(
1726            get_by_role_selector(AriaRole::Button, Some(opts)),
1727            "internal:role=button[name=\"Submit\"i]"
1728        );
1729    }
1730
1731    #[test]
1732    fn test_get_by_role_selector_with_name_exact() {
1733        let opts = GetByRoleOptions {
1734            name: Some("Submit".to_string()),
1735            exact: Some(true),
1736            ..Default::default()
1737        };
1738        assert_eq!(
1739            get_by_role_selector(AriaRole::Button, Some(opts)),
1740            "internal:role=button[name=\"Submit\"s]"
1741        );
1742    }
1743
1744    #[test]
1745    fn test_get_by_role_selector_with_checked() {
1746        let opts = GetByRoleOptions {
1747            checked: Some(true),
1748            ..Default::default()
1749        };
1750        assert_eq!(
1751            get_by_role_selector(AriaRole::Checkbox, Some(opts)),
1752            "internal:role=checkbox[checked=true]"
1753        );
1754    }
1755
1756    #[test]
1757    fn test_get_by_role_selector_with_level() {
1758        let opts = GetByRoleOptions {
1759            level: Some(2),
1760            ..Default::default()
1761        };
1762        assert_eq!(
1763            get_by_role_selector(AriaRole::Heading, Some(opts)),
1764            "internal:role=heading[level=2]"
1765        );
1766    }
1767
1768    #[test]
1769    fn test_get_by_role_selector_with_disabled() {
1770        let opts = GetByRoleOptions {
1771            disabled: Some(true),
1772            ..Default::default()
1773        };
1774        assert_eq!(
1775            get_by_role_selector(AriaRole::Button, Some(opts)),
1776            "internal:role=button[disabled=true]"
1777        );
1778    }
1779
1780    #[test]
1781    fn test_get_by_role_selector_include_hidden() {
1782        let opts = GetByRoleOptions {
1783            include_hidden: Some(true),
1784            ..Default::default()
1785        };
1786        assert_eq!(
1787            get_by_role_selector(AriaRole::Button, Some(opts)),
1788            "internal:role=button[include-hidden=true]"
1789        );
1790    }
1791
1792    #[test]
1793    fn test_get_by_role_selector_property_order() {
1794        // All properties: checked, disabled, selected, expanded, include-hidden, level, name, pressed
1795        let opts = GetByRoleOptions {
1796            pressed: Some(true),
1797            name: Some("OK".to_string()),
1798            checked: Some(false),
1799            disabled: Some(true),
1800            ..Default::default()
1801        };
1802        assert_eq!(
1803            get_by_role_selector(AriaRole::Button, Some(opts)),
1804            "internal:role=button[checked=false][disabled=true][name=\"OK\"i][pressed=true]"
1805        );
1806    }
1807
1808    #[test]
1809    fn test_get_by_role_selector_name_with_special_chars() {
1810        let opts = GetByRoleOptions {
1811            name: Some("Click \"here\" now".to_string()),
1812            exact: Some(true),
1813            ..Default::default()
1814        };
1815        assert_eq!(
1816            get_by_role_selector(AriaRole::Link, Some(opts)),
1817            "internal:role=link[name=\"Click \\\"here\\\" now\"s]"
1818        );
1819    }
1820
1821    #[test]
1822    fn test_aria_role_as_str() {
1823        assert_eq!(AriaRole::Button.as_str(), "button");
1824        assert_eq!(AriaRole::Heading.as_str(), "heading");
1825        assert_eq!(AriaRole::Link.as_str(), "link");
1826        assert_eq!(AriaRole::Checkbox.as_str(), "checkbox");
1827        assert_eq!(AriaRole::Alert.as_str(), "alert");
1828        assert_eq!(AriaRole::Navigation.as_str(), "navigation");
1829        assert_eq!(AriaRole::Progressbar.as_str(), "progressbar");
1830        assert_eq!(AriaRole::Treeitem.as_str(), "treeitem");
1831    }
1832}