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