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}