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