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