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
4//! semantics. They represent *a way to find element(s)* at any given
5//! moment — not an element handle. Each action re-queries.
6//!
7//! Key characteristics:
8//! - Lazy: don't execute until an action is performed
9//! - Retryable: auto-wait for elements to match actionability checks
10//! - Chainable: can create sub-locators via `first()`, `last()`,
11//! `nth()`, `locator()`, `filter()`
12//!
13//! Architecture:
14//! - Locator is **not** a ChannelOwner; it's a lightweight wrapper
15//! - Stores a selector string + reference to its Frame + parent Page
16//! - Delegates all operations to Frame with `strict=true`
17//!
18//! # Example
19//!
20//! ```no_run
21//! use playwright_rs::Playwright;
22//!
23//! #[tokio::main]
24//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! let pw = Playwright::launch().await?;
26//! let browser = pw.chromium().launch().await?;
27//! let page = browser.new_page().await?;
28//!
29//! page.set_content(
30//! r#"<button data-testid="submit" role="button">Submit</button>
31//! <ul><li class="item">A</li><li class="item">B</li></ul>"#,
32//! None,
33//! ).await?;
34//!
35//! // Basic locator + action
36//! page.locator("button").click(None).await?;
37//!
38//! // Robust locator from a fragile starting point: normalize() asks
39//! // Playwright for the canonical equivalent (test-id / role / text).
40//! let stable = page
41//! .locator("body button:nth-child(1)")
42//! .normalize()
43//! .await?;
44//! assert!(!stable.selector().is_empty());
45//!
46//! // Chain primitives: filter, count, nth
47//! let items = page.locator(".item");
48//! assert_eq!(items.count().await?, 2);
49//! items.nth(0).click(None).await?;
50//!
51//! browser.close().await?;
52//! Ok(())
53//! }
54//! ```
55//!
56//! See: <https://playwright.dev/docs/api/class-locator>
57
58use crate::error::Result;
59use crate::protocol::Frame;
60use serde::Deserialize;
61
62/// Trait for action option structs that have an optional timeout field.
63/// Used by `Locator::with_timeout` to inject the page's default timeout.
64pub(crate) trait HasTimeout {
65 fn timeout_ref(&self) -> &Option<f64>;
66 fn timeout_ref_mut(&mut self) -> &mut Option<f64>;
67}
68
69macro_rules! impl_has_timeout {
70 ($($ty:ty),+ $(,)?) => {
71 $(impl HasTimeout for $ty {
72 fn timeout_ref(&self) -> &Option<f64> { &self.timeout }
73 fn timeout_ref_mut(&mut self) -> &mut Option<f64> { &mut self.timeout }
74 })+
75 };
76}
77
78impl_has_timeout!(
79 crate::protocol::WaitForFunctionOptions,
80 crate::protocol::ClickOptions,
81 crate::protocol::FillOptions,
82 crate::protocol::PressOptions,
83 crate::protocol::CheckOptions,
84 crate::protocol::HoverOptions,
85 crate::protocol::SelectOptions,
86 crate::protocol::ScreenshotOptions,
87 crate::protocol::TapOptions,
88 crate::protocol::DragToOptions,
89 crate::protocol::DropOptions,
90 crate::protocol::WaitForOptions,
91);
92use std::sync::Arc;
93
94/// The bounding box of an element in pixels.
95///
96/// All values are measured relative to the top-left corner of the page.
97///
98/// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
99#[derive(Debug, Clone, PartialEq, Deserialize)]
100#[non_exhaustive]
101pub struct BoundingBox {
102 /// The x coordinate of the top-left corner of the element in pixels.
103 pub x: f64,
104 /// The y coordinate of the top-left corner of the element in pixels.
105 pub y: f64,
106 /// The width of the element in pixels.
107 pub width: f64,
108 /// The height of the element in pixels.
109 pub height: f64,
110}
111
112/// Escapes text for use in Playwright's internal selector engine.
113///
114/// JSON-stringifies the text and appends `i` (case-insensitive) or `s` (strict/exact).
115/// Matches the `escapeForTextSelector`/`escapeForAttributeSelector` in Playwright TypeScript.
116fn escape_for_selector(text: &str, exact: bool) -> String {
117 let suffix = if exact { "s" } else { "i" };
118 let escaped = serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text));
119 format!("{}{}", escaped, suffix)
120}
121
122/// Builds the internal selector string for `get_by_text`.
123///
124/// - `exact=false` → `internal:text="text"i` (case-insensitive substring)
125/// - `exact=true` → `internal:text="text"s` (case-sensitive exact)
126pub(crate) fn get_by_text_selector(text: &str, exact: bool) -> String {
127 format!("internal:text={}", escape_for_selector(text, exact))
128}
129
130/// Builds the internal selector string for `get_by_label`.
131///
132/// - `exact=false` → `internal:label="text"i`
133/// - `exact=true` → `internal:label="text"s`
134pub(crate) fn get_by_label_selector(text: &str, exact: bool) -> String {
135 format!("internal:label={}", escape_for_selector(text, exact))
136}
137
138/// Builds the internal selector string for `get_by_placeholder`.
139///
140/// - `exact=false` → `internal:attr=[placeholder="text"i]`
141/// - `exact=true` → `internal:attr=[placeholder="text"s]`
142pub(crate) fn get_by_placeholder_selector(text: &str, exact: bool) -> String {
143 format!(
144 "internal:attr=[placeholder={}]",
145 escape_for_selector(text, exact)
146 )
147}
148
149/// Builds the internal selector string for `get_by_alt_text`.
150///
151/// - `exact=false` → `internal:attr=[alt="text"i]`
152/// - `exact=true` → `internal:attr=[alt="text"s]`
153pub(crate) fn get_by_alt_text_selector(text: &str, exact: bool) -> String {
154 format!("internal:attr=[alt={}]", escape_for_selector(text, exact))
155}
156
157/// Builds the internal selector string for `get_by_title`.
158///
159/// - `exact=false` → `internal:attr=[title="text"i]`
160/// - `exact=true` → `internal:attr=[title="text"s]`
161pub(crate) fn get_by_title_selector(text: &str, exact: bool) -> String {
162 format!("internal:attr=[title={}]", escape_for_selector(text, exact))
163}
164
165/// Builds the internal selector string for `get_by_test_id`.
166///
167/// Uses `data-testid` attribute by default (matching Playwright's default).
168/// Always uses exact matching (`s` suffix).
169pub(crate) fn get_by_test_id_selector(test_id: &str) -> String {
170 get_by_test_id_selector_with_attr(test_id, "data-testid")
171}
172
173/// Builds the internal selector string for `get_by_test_id` with a custom attribute.
174///
175/// Used when `playwright.selectors().set_test_id_attribute()` has been called.
176pub(crate) fn get_by_test_id_selector_with_attr(test_id: &str, attribute: &str) -> String {
177 format!(
178 "internal:testid=[{}={}]",
179 attribute,
180 escape_for_selector(test_id, true)
181 )
182}
183
184/// Escapes text for use in Playwright's attribute role selector.
185///
186/// Unlike `escape_for_selector` (which uses JSON encoding), this only escapes
187/// backslashes and double quotes, matching Playwright's `escapeForAttributeSelector`.
188fn escape_for_attribute_selector(text: &str, exact: bool) -> String {
189 let suffix = if exact { "s" } else { "i" };
190 let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
191 format!("\"{}\"{}", escaped, suffix)
192}
193
194/// Builds the internal selector string for `get_by_role`.
195///
196/// Format: `internal:role=<role>[prop1=val1][prop2=val2]...`
197///
198/// Properties are appended in Playwright's required order:
199/// checked, disabled, selected, expanded, include-hidden, level, name, pressed.
200pub(crate) fn get_by_role_selector(role: AriaRole, options: Option<GetByRoleOptions>) -> String {
201 let mut selector = format!("internal:role={}", role.as_str());
202
203 if let Some(opts) = options {
204 if let Some(checked) = opts.checked {
205 selector.push_str(&format!("[checked={}]", checked));
206 }
207 if let Some(disabled) = opts.disabled {
208 selector.push_str(&format!("[disabled={}]", disabled));
209 }
210 if let Some(selected) = opts.selected {
211 selector.push_str(&format!("[selected={}]", selected));
212 }
213 if let Some(expanded) = opts.expanded {
214 selector.push_str(&format!("[expanded={}]", expanded));
215 }
216 if let Some(include_hidden) = opts.include_hidden {
217 selector.push_str(&format!("[include-hidden={}]", include_hidden));
218 }
219 if let Some(level) = opts.level {
220 selector.push_str(&format!("[level={}]", level));
221 }
222 if let Some(name) = &opts.name {
223 let exact = opts.exact.unwrap_or(false);
224 selector.push_str(&format!(
225 "[name={}]",
226 escape_for_attribute_selector(name, exact)
227 ));
228 }
229 if let Some(description) = &opts.description {
230 let exact = opts.exact.unwrap_or(false);
231 selector.push_str(&format!(
232 "[description={}]",
233 escape_for_attribute_selector(description, exact)
234 ));
235 }
236 if let Some(pressed) = opts.pressed {
237 selector.push_str(&format!("[pressed={}]", pressed));
238 }
239 }
240
241 selector
242}
243
244/// ARIA roles for `get_by_role()` locator.
245///
246/// Represents WAI-ARIA roles used to locate elements by their accessibility role.
247/// Matches Playwright's `AriaRole` enum across all language bindings.
248///
249/// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251#[non_exhaustive]
252pub enum AriaRole {
253 Alert,
254 Alertdialog,
255 Application,
256 Article,
257 Banner,
258 Blockquote,
259 Button,
260 Caption,
261 Cell,
262 Checkbox,
263 Code,
264 Columnheader,
265 Combobox,
266 Complementary,
267 Contentinfo,
268 Definition,
269 Deletion,
270 Dialog,
271 Directory,
272 Document,
273 Emphasis,
274 Feed,
275 Figure,
276 Form,
277 Generic,
278 Grid,
279 Gridcell,
280 Group,
281 Heading,
282 Img,
283 Insertion,
284 Link,
285 List,
286 Listbox,
287 Listitem,
288 Log,
289 Main,
290 Marquee,
291 Math,
292 Meter,
293 Menu,
294 Menubar,
295 Menuitem,
296 Menuitemcheckbox,
297 Menuitemradio,
298 Navigation,
299 None,
300 Note,
301 Option,
302 Paragraph,
303 Presentation,
304 Progressbar,
305 Radio,
306 Radiogroup,
307 Region,
308 Row,
309 Rowgroup,
310 Rowheader,
311 Scrollbar,
312 Search,
313 Searchbox,
314 Separator,
315 Slider,
316 Spinbutton,
317 Status,
318 Strong,
319 Subscript,
320 Superscript,
321 Switch,
322 Tab,
323 Table,
324 Tablist,
325 Tabpanel,
326 Term,
327 Textbox,
328 Time,
329 Timer,
330 Toolbar,
331 Tooltip,
332 Tree,
333 Treegrid,
334 Treeitem,
335}
336
337impl AriaRole {
338 /// Returns the lowercase string representation used in selectors.
339 pub fn as_str(&self) -> &'static str {
340 match self {
341 Self::Alert => "alert",
342 Self::Alertdialog => "alertdialog",
343 Self::Application => "application",
344 Self::Article => "article",
345 Self::Banner => "banner",
346 Self::Blockquote => "blockquote",
347 Self::Button => "button",
348 Self::Caption => "caption",
349 Self::Cell => "cell",
350 Self::Checkbox => "checkbox",
351 Self::Code => "code",
352 Self::Columnheader => "columnheader",
353 Self::Combobox => "combobox",
354 Self::Complementary => "complementary",
355 Self::Contentinfo => "contentinfo",
356 Self::Definition => "definition",
357 Self::Deletion => "deletion",
358 Self::Dialog => "dialog",
359 Self::Directory => "directory",
360 Self::Document => "document",
361 Self::Emphasis => "emphasis",
362 Self::Feed => "feed",
363 Self::Figure => "figure",
364 Self::Form => "form",
365 Self::Generic => "generic",
366 Self::Grid => "grid",
367 Self::Gridcell => "gridcell",
368 Self::Group => "group",
369 Self::Heading => "heading",
370 Self::Img => "img",
371 Self::Insertion => "insertion",
372 Self::Link => "link",
373 Self::List => "list",
374 Self::Listbox => "listbox",
375 Self::Listitem => "listitem",
376 Self::Log => "log",
377 Self::Main => "main",
378 Self::Marquee => "marquee",
379 Self::Math => "math",
380 Self::Meter => "meter",
381 Self::Menu => "menu",
382 Self::Menubar => "menubar",
383 Self::Menuitem => "menuitem",
384 Self::Menuitemcheckbox => "menuitemcheckbox",
385 Self::Menuitemradio => "menuitemradio",
386 Self::Navigation => "navigation",
387 Self::None => "none",
388 Self::Note => "note",
389 Self::Option => "option",
390 Self::Paragraph => "paragraph",
391 Self::Presentation => "presentation",
392 Self::Progressbar => "progressbar",
393 Self::Radio => "radio",
394 Self::Radiogroup => "radiogroup",
395 Self::Region => "region",
396 Self::Row => "row",
397 Self::Rowgroup => "rowgroup",
398 Self::Rowheader => "rowheader",
399 Self::Scrollbar => "scrollbar",
400 Self::Search => "search",
401 Self::Searchbox => "searchbox",
402 Self::Separator => "separator",
403 Self::Slider => "slider",
404 Self::Spinbutton => "spinbutton",
405 Self::Status => "status",
406 Self::Strong => "strong",
407 Self::Subscript => "subscript",
408 Self::Superscript => "superscript",
409 Self::Switch => "switch",
410 Self::Tab => "tab",
411 Self::Table => "table",
412 Self::Tablist => "tablist",
413 Self::Tabpanel => "tabpanel",
414 Self::Term => "term",
415 Self::Textbox => "textbox",
416 Self::Time => "time",
417 Self::Timer => "timer",
418 Self::Toolbar => "toolbar",
419 Self::Tooltip => "tooltip",
420 Self::Tree => "tree",
421 Self::Treegrid => "treegrid",
422 Self::Treeitem => "treeitem",
423 }
424 }
425}
426
427/// Options for `get_by_role()` locator.
428///
429/// All fields are optional. When not specified, the property is not included
430/// in the role selector, meaning it matches any value.
431///
432/// See: <https://playwright.dev/docs/api/class-page#page-get-by-role>
433#[derive(Debug, Clone, Default)]
434#[non_exhaustive]
435pub struct GetByRoleOptions {
436 /// Whether the element is checked (for checkboxes, radio buttons).
437 pub checked: Option<bool>,
438 /// Whether the element is disabled.
439 pub disabled: Option<bool>,
440 /// Whether the element is selected (for options).
441 pub selected: Option<bool>,
442 /// Whether the element is expanded (for tree items, comboboxes).
443 pub expanded: Option<bool>,
444 /// Whether to include hidden elements.
445 pub include_hidden: Option<bool>,
446 /// The heading level (1-6, for heading role).
447 pub level: Option<u32>,
448 /// The accessible name of the element.
449 pub name: Option<String>,
450 /// The accessible description of the element (WAI-ARIA), matched in addition
451 /// to role/name. Honors `exact` like `name`.
452 pub description: Option<String>,
453 /// Whether `name`/`description` matching is exact (case-sensitive,
454 /// full-string). Default is false (case-insensitive substring).
455 pub exact: Option<bool>,
456 /// Whether the element is pressed (for toggle buttons).
457 pub pressed: Option<bool>,
458}
459
460impl GetByRoleOptions {
461 /// Match only checked / unchecked elements.
462 pub fn checked(mut self, checked: bool) -> Self {
463 self.checked = Some(checked);
464 self
465 }
466 /// Match only enabled / disabled elements.
467 pub fn disabled(mut self, disabled: bool) -> Self {
468 self.disabled = Some(disabled);
469 self
470 }
471 /// Match only selected / unselected elements.
472 pub fn selected(mut self, selected: bool) -> Self {
473 self.selected = Some(selected);
474 self
475 }
476 /// Match only expanded / collapsed elements.
477 pub fn expanded(mut self, expanded: bool) -> Self {
478 self.expanded = Some(expanded);
479 self
480 }
481 /// Include elements normally excluded from the accessibility tree.
482 pub fn include_hidden(mut self, include_hidden: bool) -> Self {
483 self.include_hidden = Some(include_hidden);
484 self
485 }
486 /// Match the aria-level (e.g. heading level).
487 pub fn level(mut self, level: u32) -> Self {
488 self.level = Some(level);
489 self
490 }
491 /// Match the accessible name.
492 pub fn name(mut self, name: impl Into<String>) -> Self {
493 self.name = Some(name.into());
494 self
495 }
496 /// Match the accessible description.
497 pub fn description(mut self, description: impl Into<String>) -> Self {
498 self.description = Some(description.into());
499 self
500 }
501 /// Whether `name`/`description` match exactly (case-sensitive whole string).
502 pub fn exact(mut self, exact: bool) -> Self {
503 self.exact = Some(exact);
504 self
505 }
506 /// Match only pressed / unpressed elements.
507 pub fn pressed(mut self, pressed: bool) -> Self {
508 self.pressed = Some(pressed);
509 self
510 }
511}
512
513/// Options for [`Locator::highlight()`].
514#[derive(Debug, Clone, Default)]
515#[non_exhaustive]
516pub struct HighlightOptions {
517 /// Extra inline CSS applied to the debug highlight overlay.
518 pub style: Option<String>,
519}
520
521impl HighlightOptions {
522 /// Extra inline CSS applied to the debug highlight overlay.
523 pub fn style(mut self, style: impl Into<String>) -> Self {
524 self.style = Some(style.into());
525 self
526 }
527}
528
529/// Options for [`Locator::filter()`].
530///
531/// Narrows an existing locator according to the specified criteria.
532/// All fields are optional; unset fields are ignored.
533///
534/// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
535#[derive(Debug, Clone, Default)]
536#[non_exhaustive]
537pub struct FilterOptions {
538 /// Matches elements containing the specified text (case-insensitive substring by default).
539 pub has_text: Option<String>,
540 /// Matches elements that do **not** contain the specified text anywhere inside.
541 pub has_not_text: Option<String>,
542 /// Narrows to elements that contain a descendant matching this locator.
543 ///
544 /// The inner locator is queried relative to the outer locator's matched element,
545 /// not the document root.
546 pub has: Option<Locator>,
547 /// Narrows to elements that do **not** contain a descendant matching this locator.
548 pub has_not: Option<Locator>,
549 /// Narrows to elements that are visible (`true`) or hidden (`false`).
550 ///
551 /// [`Locator::visible`] is the shorthand for the `true` case.
552 pub visible: Option<bool>,
553}
554
555impl FilterOptions {
556 /// Keep only elements containing the given text (case-insensitive substring).
557 pub fn has_text(mut self, has_text: impl Into<String>) -> Self {
558 self.has_text = Some(has_text.into());
559 self
560 }
561 /// Keep only elements NOT containing the given text.
562 pub fn has_not_text(mut self, has_not_text: impl Into<String>) -> Self {
563 self.has_not_text = Some(has_not_text.into());
564 self
565 }
566 /// Keep only elements containing a match for the inner locator.
567 pub fn has(mut self, has: Locator) -> Self {
568 self.has = Some(has);
569 self
570 }
571 /// Keep only elements NOT containing a match for the inner locator.
572 pub fn has_not(mut self, has_not: Locator) -> Self {
573 self.has_not = Some(has_not);
574 self
575 }
576 /// Keep only visible (`true`) or only hidden (`false`) elements.
577 pub fn visible(mut self, visible: bool) -> Self {
578 self.visible = Some(visible);
579 self
580 }
581}
582
583/// The selector `filter` builds: each narrowing appended in the order
584/// upstream's client appends it, since the engine applies them in order.
585///
586/// The `has` / `has_not` selectors are passed separately because a
587/// [`FilterOptions`] holds whole locators, and only their selectors matter
588/// here.
589fn filter_selector(
590 base: &str,
591 options: &FilterOptions,
592 has: Option<&str>,
593 has_not: Option<&str>,
594) -> String {
595 let mut selector = base.to_string();
596
597 if let Some(text) = &options.has_text {
598 let escaped = escape_for_selector(text, false);
599 selector = format!("{selector} >> internal:has-text={escaped}");
600 }
601
602 if let Some(text) = &options.has_not_text {
603 let escaped = escape_for_selector(text, false);
604 selector = format!("{selector} >> internal:has-not-text={escaped}");
605 }
606
607 if let Some(inner) = has {
608 let quoted = serde_json::to_string(inner).unwrap_or_else(|_| format!("\"{inner}\""));
609 selector = format!("{selector} >> internal:has={quoted}");
610 }
611
612 if let Some(inner) = has_not {
613 let quoted = serde_json::to_string(inner).unwrap_or_else(|_| format!("\"{inner}\""));
614 selector = format!("{selector} >> internal:has-not={quoted}");
615 }
616
617 if let Some(visible) = options.visible {
618 selector = format!("{selector} >> visible={visible}");
619 }
620
621 selector
622}
623
624/// Locator represents a way to find element(s) on the page at any given moment.
625///
626/// Locators are lazy - they don't execute queries until an action is performed.
627/// This enables auto-waiting and retry-ability for robust test automation.
628///
629/// # Examples
630///
631/// ```no_run
632/// use playwright_rs::protocol::{Playwright, SelectOption};
633///
634/// #[tokio::main]
635/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
636/// let playwright = Playwright::launch().await?;
637/// let browser = playwright.chromium().launch().await?;
638/// let page = browser.new_page().await?;
639///
640/// // Demonstrate set_checked() - checkbox interaction
641/// let _ = page.goto(
642/// "data:text/html,<input type='checkbox' id='cb'>",
643/// None
644/// ).await;
645/// let checkbox = page.locator("#cb");
646/// checkbox.set_checked(true, None).await?;
647/// assert!(checkbox.is_checked().await?);
648/// checkbox.set_checked(false, None).await?;
649/// assert!(!checkbox.is_checked().await?);
650///
651/// // Demonstrate select_option() - select by value, label, and index
652/// let _ = page.goto(
653/// "data:text/html,<select id='fruits'>\
654/// <option value='apple'>Apple</option>\
655/// <option value='banana'>Banana</option>\
656/// <option value='cherry'>Cherry</option>\
657/// </select>",
658/// None
659/// ).await;
660/// let select = page.locator("#fruits");
661/// select.select_option("banana", None).await?;
662/// assert_eq!(select.input_value(None).await?, "banana");
663/// select.select_option(SelectOption::Label("Apple".to_string()), None).await?;
664/// assert_eq!(select.input_value(None).await?, "apple");
665/// select.select_option(SelectOption::Index(2), None).await?;
666/// assert_eq!(select.input_value(None).await?, "cherry");
667///
668/// // Demonstrate select_option_multiple() - multi-select
669/// let _ = page.goto(
670/// "data:text/html,<select id='colors' multiple>\
671/// <option value='red'>Red</option>\
672/// <option value='green'>Green</option>\
673/// <option value='blue'>Blue</option>\
674/// <option value='yellow'>Yellow</option>\
675/// </select>",
676/// None
677/// ).await;
678/// let multi = page.locator("#colors");
679/// let selected = multi.select_option_multiple(&["red", "blue"], None).await?;
680/// assert_eq!(selected.len(), 2);
681/// assert!(selected.contains(&"red".to_string()));
682/// assert!(selected.contains(&"blue".to_string()));
683///
684/// // Demonstrate get_by_text() - find elements by text content
685/// let _ = page.goto(
686/// "data:text/html,<button>Submit</button><button>Submit Order</button>",
687/// None
688/// ).await;
689/// let all_submits = page.get_by_text("Submit", false);
690/// assert_eq!(all_submits.count().await?, 2); // case-insensitive substring
691/// let exact_submit = page.get_by_text("Submit", true);
692/// assert_eq!(exact_submit.count().await?, 1); // exact match only
693///
694/// // Demonstrate get_by_label, get_by_placeholder, get_by_test_id
695/// let _ = page.goto(
696/// "data:text/html,<label for='email'>Email</label>\
697/// <input id='email' placeholder='you@example.com' data-testid='email-input' />",
698/// None
699/// ).await;
700/// let by_label = page.get_by_label("Email", false);
701/// assert_eq!(by_label.count().await?, 1);
702/// let by_placeholder = page.get_by_placeholder("you@example.com", true);
703/// assert_eq!(by_placeholder.count().await?, 1);
704/// let by_test_id = page.get_by_test_id("email-input");
705/// assert_eq!(by_test_id.count().await?, 1);
706///
707/// // Demonstrate screenshot() - element screenshot
708/// let _ = page.goto(
709/// "data:text/html,<h1 id='title'>Hello World</h1>",
710/// None
711/// ).await;
712/// let heading = page.locator("#title");
713/// let screenshot = heading.screenshot(None).await?;
714/// assert!(!screenshot.is_empty());
715///
716/// browser.close().await?;
717/// Ok(())
718/// }
719/// ```
720///
721/// See: <https://playwright.dev/docs/api/class-locator>
722#[derive(Clone)]
723pub struct Locator {
724 frame: Arc<Frame>,
725 selector: String,
726 page: crate::protocol::Page,
727}
728
729impl Locator {
730 /// Creates a new Locator (internal use only)
731 ///
732 /// Use `page.locator()` or `frame.locator()` to create locators in application code.
733 pub(crate) fn new(frame: Arc<Frame>, selector: String, page: crate::protocol::Page) -> Self {
734 Self {
735 frame,
736 selector,
737 page,
738 }
739 }
740
741 /// Returns the selector string for this locator
742 pub fn selector(&self) -> &str {
743 &self.selector
744 }
745
746 /// Returns the underlying frame for this locator (crate-internal use only).
747 pub(crate) fn frame(&self) -> &Arc<Frame> {
748 &self.frame
749 }
750
751 /// Waits until `expression` returns a truthy value, with the matched
752 /// element passed as its first argument.
753 ///
754 /// The element is resolved with strict matching, so a selector matching
755 /// more than one element is an error rather than a silent pick of the
756 /// first. Returns `()` rather than a handle: the protocol omits the
757 /// result when a selector is supplied, so there is nothing to hand back.
758 ///
759 /// `WaitForFunctionOptions::polling_interval` has no effect on this
760 /// form: the driver polls element-scoped waits on its own backoff
761 /// schedule (roughly 100ms growing toward 1s) and reads the interval
762 /// only for the page-global form.
763 ///
764 /// # Errors
765 ///
766 /// Returns an error if the expression does not become truthy within the
767 /// timeout (the page default unless set here), or if the selector
768 /// matches nothing or more than one element. The timeout is enforced by
769 /// the driver, so it surfaces as a protocol error carrying the driver's
770 /// "Timeout ...ms exceeded" message, with the selector appended.
771 ///
772 /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for-function>
773 pub async fn wait_for_function(
774 &self,
775 expression: &str,
776 options: impl Into<Option<crate::protocol::WaitForFunctionOptions>>,
777 ) -> Result<()> {
778 let options = self.with_timeout(options.into());
779 self.frame
780 .wait_for_function_internal(expression, Some(&self.selector), options)
781 .await
782 .map(|_| ())
783 .map_err(|e| self.wrap_error_with_selector(e))
784 }
785
786 /// Serializes this locator as a screenshot `mask` entry — `{ frame, selector }`
787 /// with the frame sent as a channel reference — matching the protocol shape
788 /// the driver expects. Used by [`crate::protocol::ScreenshotOptions`].
789 pub(crate) fn mask_json(&self) -> serde_json::Value {
790 use crate::server::channel_owner::ChannelOwner as _;
791 serde_json::json!({
792 "frame": { "guid": self.frame.guid() },
793 "selector": self.selector,
794 })
795 }
796
797 /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) scoped within this locator's subtree.
798 ///
799 /// The `selector` identifies an iframe element within the locator's scope.
800 ///
801 /// See: <https://playwright.dev/docs/api/class-locator#locator-frame-locator>
802 pub fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
803 crate::protocol::FrameLocator::new(
804 Arc::clone(&self.frame),
805 format!("{} >> {}", self.selector, selector),
806 self.page.clone(),
807 )
808 }
809
810 /// Returns the Page this locator belongs to.
811 ///
812 /// Each locator is bound to the page that created it. Chained locators (via
813 /// `first()`, `last()`, `nth()`, `locator()`, `filter()`, etc.) all return
814 /// the same owning page. This matches the behavior of `locator.page` in
815 /// other Playwright language bindings.
816 ///
817 /// # Example
818 ///
819 /// ```no_run
820 /// # use playwright_rs::Playwright;
821 /// # #[tokio::main]
822 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
823 /// let playwright = Playwright::launch().await?;
824 /// let browser = playwright.chromium().launch().await?;
825 /// let page = browser.new_page().await?;
826 /// page.goto("https://example.com", None).await?;
827 ///
828 /// let locator = page.locator("h1");
829 /// let locator_page = locator.page()?;
830 /// assert_eq!(locator_page.url(), page.url());
831 /// # Ok(())
832 /// # }
833 /// ```
834 ///
835 /// See: <https://playwright.dev/docs/api/class-locator#locator-page>
836 pub fn page(&self) -> Result<crate::protocol::Page> {
837 Ok(self.page.clone())
838 }
839
840 /// Evaluate a JavaScript expression in the frame context.
841 ///
842 /// Used internally for injecting CSS (e.g., disabling animations) before screenshot assertions.
843 #[cfg(feature = "screenshot-diff")]
844 pub(crate) async fn evaluate_js<T: serde::Serialize>(
845 &self,
846 expression: &str,
847 _arg: Option<T>,
848 ) -> Result<()> {
849 self.frame
850 .frame_evaluate_expression(expression)
851 .await
852 .map_err(|e| self.wrap_error_with_selector(e))
853 }
854
855 /// Creates a locator for the first matching element.
856 ///
857 /// See: <https://playwright.dev/docs/api/class-locator#locator-first>
858 pub fn first(&self) -> Locator {
859 Locator::new(
860 Arc::clone(&self.frame),
861 format!("{} >> nth=0", self.selector),
862 self.page.clone(),
863 )
864 }
865
866 /// Creates a locator for the last matching element.
867 ///
868 /// See: <https://playwright.dev/docs/api/class-locator#locator-last>
869 pub fn last(&self) -> Locator {
870 Locator::new(
871 Arc::clone(&self.frame),
872 format!("{} >> nth=-1", self.selector),
873 self.page.clone(),
874 )
875 }
876
877 /// Creates a locator for the nth matching element (0-indexed).
878 ///
879 /// See: <https://playwright.dev/docs/api/class-locator#locator-nth>
880 pub fn nth(&self, index: i32) -> Locator {
881 Locator::new(
882 Arc::clone(&self.frame),
883 format!("{} >> nth={}", self.selector, index),
884 self.page.clone(),
885 )
886 }
887
888 /// Returns a locator that matches elements containing the given text.
889 ///
890 /// By default, matching is case-insensitive and searches for a substring.
891 /// Set `exact` to `true` for case-sensitive exact matching.
892 ///
893 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-text>
894 pub fn get_by_text(&self, text: &str, exact: bool) -> Locator {
895 self.locator(get_by_text_selector(text, exact))
896 }
897
898 /// Returns a locator that matches elements by their associated label text.
899 ///
900 /// Targets form controls (`input`, `textarea`, `select`) linked via `<label>`,
901 /// `aria-label`, or `aria-labelledby`.
902 ///
903 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-label>
904 pub fn get_by_label(&self, text: &str, exact: bool) -> Locator {
905 self.locator(get_by_label_selector(text, exact))
906 }
907
908 /// Returns a locator that matches elements by their placeholder text.
909 ///
910 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-placeholder>
911 pub fn get_by_placeholder(&self, text: &str, exact: bool) -> Locator {
912 self.locator(get_by_placeholder_selector(text, exact))
913 }
914
915 /// Returns a locator that matches elements by their alt text.
916 ///
917 /// Typically used for `<img>` elements.
918 ///
919 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-alt-text>
920 pub fn get_by_alt_text(&self, text: &str, exact: bool) -> Locator {
921 self.locator(get_by_alt_text_selector(text, exact))
922 }
923
924 /// Returns a locator that matches elements by their title attribute.
925 ///
926 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-title>
927 pub fn get_by_title(&self, text: &str, exact: bool) -> Locator {
928 self.locator(get_by_title_selector(text, exact))
929 }
930
931 /// Returns a locator that matches elements by their test ID attribute.
932 ///
933 /// By default, uses the `data-testid` attribute. Call
934 /// `playwright.selectors().set_test_id_attribute()` to change the attribute name.
935 ///
936 /// Always uses exact matching (case-sensitive).
937 ///
938 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-test-id>
939 pub fn get_by_test_id(&self, test_id: &str) -> Locator {
940 use crate::server::channel_owner::ChannelOwner as _;
941 let attr = self.frame.connection().selectors().test_id_attribute();
942 self.locator(get_by_test_id_selector_with_attr(test_id, &attr))
943 }
944
945 /// Returns a locator that matches elements by their ARIA role.
946 ///
947 /// This is the recommended way to locate elements, as it matches the way
948 /// users and assistive technology perceive the page.
949 ///
950 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-role>
951 pub fn get_by_role(&self, role: AriaRole, options: Option<GetByRoleOptions>) -> Locator {
952 self.locator(get_by_role_selector(role, options))
953 }
954
955 /// Creates a sub-locator within this locator's subtree.
956 ///
957 /// See: <https://playwright.dev/docs/api/class-locator#locator-locator>
958 pub fn locator(&self, selector: impl Into<String>) -> Locator {
959 Locator::new(
960 Arc::clone(&self.frame),
961 format!("{} >> {}", self.selector, selector.into()),
962 self.page.clone(),
963 )
964 }
965
966 /// Narrows this locator according to the filter options.
967 ///
968 /// Can be chained to apply multiple filters in sequence.
969 ///
970 /// # Example
971 ///
972 /// ```no_run
973 /// use playwright_rs::{Playwright, FilterOptions};
974 ///
975 /// # #[tokio::main]
976 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
977 /// let playwright = Playwright::launch().await?;
978 /// let browser = playwright.chromium().launch().await?;
979 /// let page = browser.new_page().await?;
980 ///
981 /// // Filter rows to those containing "Apple"
982 /// let rows = page.locator("tr");
983 /// let apple_row = rows.filter(FilterOptions::default().has_text("Apple"));
984 /// # browser.close().await?;
985 /// # Ok(())
986 /// # }
987 /// ```
988 ///
989 /// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
990 pub fn filter(&self, options: FilterOptions) -> Locator {
991 let selector = filter_selector(
992 &self.selector,
993 &options,
994 options.has.as_ref().map(|l| l.selector.as_str()),
995 options.has_not.as_ref().map(|l| l.selector.as_str()),
996 );
997 Locator::new(Arc::clone(&self.frame), selector, self.page.clone())
998 }
999
1000 /// Returns a locator that matches only the visible elements of this one.
1001 ///
1002 /// This is the replacement Playwright recommends for the `:visible` CSS
1003 /// pseudo-class. `filter(FilterOptions::default().visible(false))` is the
1004 /// other half, matching only hidden elements.
1005 ///
1006 /// # Example
1007 ///
1008 /// ```no_run
1009 /// # use playwright_rs::Playwright;
1010 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1011 /// # let playwright = Playwright::launch().await?;
1012 /// # let browser = playwright.chromium().launch().await?;
1013 /// # let page = browser.new_page().await?;
1014 /// page.locator("button").visible().click(None).await?;
1015 /// # browser.close().await?;
1016 /// # Ok(())
1017 /// # }
1018 /// ```
1019 ///
1020 /// See: <https://playwright.dev/docs/api/class-locator#locator-visible>
1021 pub fn visible(&self) -> Locator {
1022 Locator::new(
1023 Arc::clone(&self.frame),
1024 format!("{} >> visible=true", self.selector),
1025 self.page.clone(),
1026 )
1027 }
1028
1029 /// Creates a locator matching elements that satisfy **both** this locator and `locator`.
1030 ///
1031 /// Note: named `and_` because `and` is a Rust keyword.
1032 ///
1033 /// # Example
1034 ///
1035 /// ```no_run
1036 /// use playwright_rs::Playwright;
1037 ///
1038 /// # #[tokio::main]
1039 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1040 /// let playwright = Playwright::launch().await?;
1041 /// let browser = playwright.chromium().launch().await?;
1042 /// let page = browser.new_page().await?;
1043 ///
1044 /// // Find a button that also has a specific title
1045 /// let button = page.locator("button");
1046 /// let titled = page.locator("[title='Subscribe']");
1047 /// let subscribe_btn = button.and_(&titled);
1048 /// # browser.close().await?;
1049 /// # Ok(())
1050 /// # }
1051 /// ```
1052 ///
1053 /// See: <https://playwright.dev/docs/api/class-locator#locator-and>
1054 pub fn and_(&self, locator: &Locator) -> Locator {
1055 let inner = serde_json::to_string(&locator.selector)
1056 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
1057 Locator::new(
1058 Arc::clone(&self.frame),
1059 format!("{} >> internal:and={}", self.selector, inner),
1060 self.page.clone(),
1061 )
1062 }
1063
1064 /// Creates a locator matching elements that satisfy **either** this locator or `locator`.
1065 ///
1066 /// Note: named `or_` because `or` is a Rust keyword.
1067 ///
1068 /// # Example
1069 ///
1070 /// ```no_run
1071 /// use playwright_rs::Playwright;
1072 ///
1073 /// # #[tokio::main]
1074 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1075 /// let playwright = Playwright::launch().await?;
1076 /// let browser = playwright.chromium().launch().await?;
1077 /// let page = browser.new_page().await?;
1078 ///
1079 /// // Find any element that is either a button or a link
1080 /// let buttons = page.locator("button");
1081 /// let links = page.locator("a");
1082 /// let interactive = buttons.or_(&links);
1083 /// # browser.close().await?;
1084 /// # Ok(())
1085 /// # }
1086 /// ```
1087 ///
1088 /// See: <https://playwright.dev/docs/api/class-locator#locator-or>
1089 pub fn or_(&self, locator: &Locator) -> Locator {
1090 let inner = serde_json::to_string(&locator.selector)
1091 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
1092 Locator::new(
1093 Arc::clone(&self.frame),
1094 format!("{} >> internal:or={}", self.selector, inner),
1095 self.page.clone(),
1096 )
1097 }
1098
1099 /// Returns the number of elements matching this locator.
1100 ///
1101 /// See: <https://playwright.dev/docs/api/class-locator#locator-count>
1102 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, count = tracing::field::Empty))]
1103 pub async fn count(&self) -> Result<usize> {
1104 let n = self
1105 .frame
1106 .locator_count(&self.selector)
1107 .await
1108 .map_err(|e| self.wrap_error_with_selector(e))?;
1109 tracing::Span::current().record("count", n);
1110 Ok(n)
1111 }
1112
1113 /// Returns an array of locators, one for each matching element.
1114 ///
1115 /// Note: `all()` does not wait for elements to match the locator,
1116 /// and instead immediately returns whatever is in the DOM.
1117 ///
1118 /// See: <https://playwright.dev/docs/api/class-locator#locator-all>
1119 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1120 pub async fn all(&self) -> Result<Vec<Locator>> {
1121 let count = self.count().await?;
1122 Ok((0..count).map(|i| self.nth(i as i32)).collect())
1123 }
1124
1125 /// Returns the text content of the element.
1126 ///
1127 /// See: <https://playwright.dev/docs/api/class-locator#locator-text-content>
1128 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1129 pub async fn text_content(&self) -> Result<Option<String>> {
1130 self.frame
1131 .locator_text_content(&self.selector)
1132 .await
1133 .map_err(|e| self.wrap_error_with_selector(e))
1134 }
1135
1136 /// Returns the inner text of the element (visible text).
1137 ///
1138 /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-text>
1139 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1140 pub async fn inner_text(&self) -> Result<String> {
1141 self.frame
1142 .locator_inner_text(&self.selector)
1143 .await
1144 .map_err(|e| self.wrap_error_with_selector(e))
1145 }
1146
1147 /// Returns the inner HTML of the element.
1148 ///
1149 /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-html>
1150 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1151 pub async fn inner_html(&self) -> Result<String> {
1152 self.frame
1153 .locator_inner_html(&self.selector)
1154 .await
1155 .map_err(|e| self.wrap_error_with_selector(e))
1156 }
1157
1158 /// Returns the value of the specified attribute.
1159 ///
1160 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-attribute>
1161 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, name = %name))]
1162 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
1163 self.frame
1164 .locator_get_attribute(&self.selector, name)
1165 .await
1166 .map_err(|e| self.wrap_error_with_selector(e))
1167 }
1168
1169 /// Returns whether the element is visible.
1170 ///
1171 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-visible>
1172 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1173 pub async fn is_visible(&self) -> Result<bool> {
1174 self.frame
1175 .locator_is_visible(&self.selector)
1176 .await
1177 .map_err(|e| self.wrap_error_with_selector(e))
1178 }
1179
1180 /// Returns whether the element is enabled.
1181 ///
1182 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-enabled>
1183 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1184 pub async fn is_enabled(&self) -> Result<bool> {
1185 self.frame
1186 .locator_is_enabled(&self.selector)
1187 .await
1188 .map_err(|e| self.wrap_error_with_selector(e))
1189 }
1190
1191 /// Returns whether the checkbox or radio button is checked.
1192 ///
1193 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-checked>
1194 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1195 pub async fn is_checked(&self) -> Result<bool> {
1196 self.frame
1197 .locator_is_checked(&self.selector)
1198 .await
1199 .map_err(|e| self.wrap_error_with_selector(e))
1200 }
1201
1202 /// Returns whether the element is editable.
1203 ///
1204 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-editable>
1205 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1206 pub async fn is_editable(&self) -> Result<bool> {
1207 self.frame
1208 .locator_is_editable(&self.selector)
1209 .await
1210 .map_err(|e| self.wrap_error_with_selector(e))
1211 }
1212
1213 /// Returns whether the element is hidden.
1214 ///
1215 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-hidden>
1216 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1217 pub async fn is_hidden(&self) -> Result<bool> {
1218 self.frame
1219 .locator_is_hidden(&self.selector)
1220 .await
1221 .map_err(|e| self.wrap_error_with_selector(e))
1222 }
1223
1224 /// Returns whether the element is disabled.
1225 ///
1226 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-disabled>
1227 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1228 pub async fn is_disabled(&self) -> Result<bool> {
1229 self.frame
1230 .locator_is_disabled(&self.selector)
1231 .await
1232 .map_err(|e| self.wrap_error_with_selector(e))
1233 }
1234
1235 /// Returns whether the element is focused (currently has focus).
1236 ///
1237 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-focused>
1238 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1239 pub async fn is_focused(&self) -> Result<bool> {
1240 self.frame
1241 .locator_is_focused(&self.selector)
1242 .await
1243 .map_err(|e| self.wrap_error_with_selector(e))
1244 }
1245
1246 // Action methods
1247
1248 /// Clicks the element.
1249 ///
1250 /// See: <https://playwright.dev/docs/api/class-locator#locator-click>
1251 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1252 pub async fn click(
1253 &self,
1254 options: impl Into<Option<crate::protocol::ClickOptions>>,
1255 ) -> Result<()> {
1256 let options = options.into();
1257 self.frame
1258 .locator_click(&self.selector, Some(self.with_timeout(options)))
1259 .await
1260 .map_err(|e| self.wrap_error_with_selector(e))
1261 }
1262
1263 /// Ensures an options struct has the page's default timeout when none is explicitly set.
1264 fn with_timeout<T: HasTimeout + Default>(&self, options: Option<T>) -> T {
1265 let mut opts = options.unwrap_or_default();
1266 if opts.timeout_ref().is_none() {
1267 *opts.timeout_ref_mut() = Some(self.page.default_timeout_ms());
1268 }
1269 opts
1270 }
1271
1272 /// Wraps an error with selector context for better error messages.
1273 fn wrap_error_with_selector(&self, error: crate::error::Error) -> crate::error::Error {
1274 match &error {
1275 crate::error::Error::ProtocolError(msg) => {
1276 // Add selector context to protocol errors (timeouts, etc.)
1277 crate::error::Error::ProtocolError(format!("{} [selector: {}]", msg, self.selector))
1278 }
1279 crate::error::Error::Timeout(msg) => {
1280 crate::error::Error::Timeout(format!("{} [selector: {}]", msg, self.selector))
1281 }
1282 _ => error, // Other errors pass through unchanged
1283 }
1284 }
1285
1286 /// Double clicks the element.
1287 ///
1288 /// See: <https://playwright.dev/docs/api/class-locator#locator-dblclick>
1289 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1290 pub async fn dblclick(
1291 &self,
1292 options: impl Into<Option<crate::protocol::ClickOptions>>,
1293 ) -> Result<()> {
1294 let options = options.into();
1295 self.frame
1296 .locator_dblclick(&self.selector, Some(self.with_timeout(options)))
1297 .await
1298 .map_err(|e| self.wrap_error_with_selector(e))
1299 }
1300
1301 /// Fills the element with text.
1302 ///
1303 /// See: <https://playwright.dev/docs/api/class-locator#locator-fill>
1304 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1305 pub async fn fill(
1306 &self,
1307 text: &str,
1308 options: impl Into<Option<crate::protocol::FillOptions>>,
1309 ) -> Result<()> {
1310 let options = options.into();
1311 self.frame
1312 .locator_fill(&self.selector, text, Some(self.with_timeout(options)))
1313 .await
1314 .map_err(|e| self.wrap_error_with_selector(e))
1315 }
1316
1317 /// Clears the element's value.
1318 ///
1319 /// See: <https://playwright.dev/docs/api/class-locator#locator-clear>
1320 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1321 pub async fn clear(
1322 &self,
1323 options: impl Into<Option<crate::protocol::FillOptions>>,
1324 ) -> Result<()> {
1325 let options = options.into();
1326 self.frame
1327 .locator_clear(&self.selector, Some(self.with_timeout(options)))
1328 .await
1329 .map_err(|e| self.wrap_error_with_selector(e))
1330 }
1331
1332 /// Presses a key on the element.
1333 ///
1334 /// See: <https://playwright.dev/docs/api/class-locator#locator-press>
1335 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1336 pub async fn press(
1337 &self,
1338 key: &str,
1339 options: impl Into<Option<crate::protocol::PressOptions>>,
1340 ) -> Result<()> {
1341 let options = options.into();
1342 self.frame
1343 .locator_press(&self.selector, key, Some(self.with_timeout(options)))
1344 .await
1345 .map_err(|e| self.wrap_error_with_selector(e))
1346 }
1347
1348 /// Sets focus on the element.
1349 ///
1350 /// Calls the element's `focus()` method. Used to move keyboard focus to a
1351 /// specific element for subsequent keyboard interactions.
1352 ///
1353 /// See: <https://playwright.dev/docs/api/class-locator#locator-focus>
1354 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1355 pub async fn focus(&self) -> Result<()> {
1356 self.frame
1357 .locator_focus(&self.selector)
1358 .await
1359 .map_err(|e| self.wrap_error_with_selector(e))
1360 }
1361
1362 /// Removes focus from the element.
1363 ///
1364 /// Calls the element's `blur()` method. Moves keyboard focus away from the element.
1365 ///
1366 /// See: <https://playwright.dev/docs/api/class-locator#locator-blur>
1367 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1368 pub async fn blur(&self) -> Result<()> {
1369 self.frame
1370 .locator_blur(&self.selector)
1371 .await
1372 .map_err(|e| self.wrap_error_with_selector(e))
1373 }
1374
1375 /// Types `text` into the element character by character, as though it was typed
1376 /// on a real keyboard.
1377 ///
1378 /// Use this method when you need to simulate keystrokes with individual key events
1379 /// (e.g., for autocomplete widgets). For simply setting a field value, prefer
1380 /// [`Locator::fill()`].
1381 ///
1382 /// # Arguments
1383 ///
1384 /// * `text` - Text to type into the element
1385 /// * `options` - Optional [`PressSequentiallyOptions`](crate::protocol::PressSequentiallyOptions) (e.g., `delay` between key presses)
1386 ///
1387 /// See: <https://playwright.dev/docs/api/class-locator#locator-press-sequentially>
1388 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1389 pub async fn press_sequentially(
1390 &self,
1391 text: &str,
1392 options: impl Into<Option<crate::protocol::PressSequentiallyOptions>>,
1393 ) -> Result<()> {
1394 let options = options.into();
1395 self.frame
1396 .locator_press_sequentially(&self.selector, text, options)
1397 .await
1398 .map_err(|e| self.wrap_error_with_selector(e))
1399 }
1400
1401 /// Returns the `innerText` values of all elements matching this locator.
1402 ///
1403 /// Unlike [`Locator::inner_text()`] (which uses strict mode and requires exactly one match),
1404 /// `all_inner_texts()` returns text from all matching elements.
1405 ///
1406 /// See: <https://playwright.dev/docs/api/class-locator#locator-all-inner-texts>
1407 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1408 pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
1409 self.frame
1410 .locator_all_inner_texts(&self.selector)
1411 .await
1412 .map_err(|e| self.wrap_error_with_selector(e))
1413 }
1414
1415 /// Returns the `textContent` values of all elements matching this locator.
1416 ///
1417 /// Unlike [`Locator::text_content()`] (which uses strict mode and requires exactly one match),
1418 /// `all_text_contents()` returns text from all matching elements.
1419 ///
1420 /// See: <https://playwright.dev/docs/api/class-locator#locator-all-text-contents>
1421 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1422 pub async fn all_text_contents(&self) -> Result<Vec<String>> {
1423 self.frame
1424 .locator_all_text_contents(&self.selector)
1425 .await
1426 .map_err(|e| self.wrap_error_with_selector(e))
1427 }
1428
1429 /// Ensures the checkbox or radio button is checked.
1430 ///
1431 /// This method is idempotent - if already checked, does nothing.
1432 ///
1433 /// See: <https://playwright.dev/docs/api/class-locator#locator-check>
1434 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1435 pub async fn check(
1436 &self,
1437 options: impl Into<Option<crate::protocol::CheckOptions>>,
1438 ) -> Result<()> {
1439 let options = options.into();
1440 self.frame
1441 .locator_check(&self.selector, Some(self.with_timeout(options)))
1442 .await
1443 .map_err(|e| self.wrap_error_with_selector(e))
1444 }
1445
1446 /// Ensures the checkbox is unchecked.
1447 ///
1448 /// This method is idempotent - if already unchecked, does nothing.
1449 ///
1450 /// See: <https://playwright.dev/docs/api/class-locator#locator-uncheck>
1451 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1452 pub async fn uncheck(
1453 &self,
1454 options: impl Into<Option<crate::protocol::CheckOptions>>,
1455 ) -> Result<()> {
1456 let options = options.into();
1457 self.frame
1458 .locator_uncheck(&self.selector, Some(self.with_timeout(options)))
1459 .await
1460 .map_err(|e| self.wrap_error_with_selector(e))
1461 }
1462
1463 /// Sets the checkbox or radio button to the specified checked state.
1464 ///
1465 /// This is a convenience method that calls `check()` if `checked` is true,
1466 /// or `uncheck()` if `checked` is false.
1467 ///
1468 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-checked>
1469 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1470 pub async fn set_checked(
1471 &self,
1472 checked: bool,
1473 options: impl Into<Option<crate::protocol::CheckOptions>>,
1474 ) -> Result<()> {
1475 let options = options.into();
1476 if checked {
1477 self.check(options).await
1478 } else {
1479 self.uncheck(options).await
1480 }
1481 }
1482
1483 /// Hovers the mouse over the element.
1484 ///
1485 /// See: <https://playwright.dev/docs/api/class-locator#locator-hover>
1486 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1487 pub async fn hover(
1488 &self,
1489 options: impl Into<Option<crate::protocol::HoverOptions>>,
1490 ) -> Result<()> {
1491 let options = options.into();
1492 self.frame
1493 .locator_hover(&self.selector, Some(self.with_timeout(options)))
1494 .await
1495 .map_err(|e| self.wrap_error_with_selector(e))
1496 }
1497
1498 /// Returns the value of the input, textarea, or select element.
1499 ///
1500 /// See: <https://playwright.dev/docs/api/class-locator#locator-input-value>
1501 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1502 pub async fn input_value(&self, _options: impl Into<Option<()>>) -> Result<String> {
1503 self.frame
1504 .locator_input_value(&self.selector)
1505 .await
1506 .map_err(|e| self.wrap_error_with_selector(e))
1507 }
1508
1509 /// Selects one or more options in a select element.
1510 ///
1511 /// Returns an array of option values that have been successfully selected.
1512 ///
1513 /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1514 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1515 pub async fn select_option(
1516 &self,
1517 value: impl Into<crate::protocol::SelectOption>,
1518 options: impl Into<Option<crate::protocol::SelectOptions>>,
1519 ) -> Result<Vec<String>> {
1520 let options = options.into();
1521 self.frame
1522 .locator_select_option(
1523 &self.selector,
1524 value.into(),
1525 Some(self.with_timeout(options)),
1526 )
1527 .await
1528 .map_err(|e| self.wrap_error_with_selector(e))
1529 }
1530
1531 /// Selects multiple options in a select element.
1532 ///
1533 /// Returns an array of option values that have been successfully selected.
1534 ///
1535 /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1536 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1537 pub async fn select_option_multiple(
1538 &self,
1539 values: &[impl Into<crate::protocol::SelectOption> + Clone],
1540 options: impl Into<Option<crate::protocol::SelectOptions>>,
1541 ) -> Result<Vec<String>> {
1542 let options = options.into();
1543 let select_options: Vec<crate::protocol::SelectOption> =
1544 values.iter().map(|v| v.clone().into()).collect();
1545 self.frame
1546 .locator_select_option_multiple(
1547 &self.selector,
1548 select_options,
1549 Some(self.with_timeout(options)),
1550 )
1551 .await
1552 .map_err(|e| self.wrap_error_with_selector(e))
1553 }
1554
1555 /// Sets the file path(s) to upload to a file input element.
1556 ///
1557 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1558 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1559 pub async fn set_input_files(
1560 &self,
1561 file: &std::path::PathBuf,
1562 _options: impl Into<Option<()>>,
1563 ) -> Result<()> {
1564 self.frame
1565 .locator_set_input_files(&self.selector, file)
1566 .await
1567 .map_err(|e| self.wrap_error_with_selector(e))
1568 }
1569
1570 /// Sets multiple file paths to upload to a file input element.
1571 ///
1572 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1573 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1574 pub async fn set_input_files_multiple(
1575 &self,
1576 files: &[&std::path::PathBuf],
1577 _options: impl Into<Option<()>>,
1578 ) -> Result<()> {
1579 self.frame
1580 .locator_set_input_files_multiple(&self.selector, files)
1581 .await
1582 .map_err(|e| self.wrap_error_with_selector(e))
1583 }
1584
1585 /// Sets a file to upload using FilePayload (explicit name, mimeType, buffer).
1586 ///
1587 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1588 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1589 pub async fn set_input_files_payload(
1590 &self,
1591 file: crate::protocol::FilePayload,
1592 _options: impl Into<Option<()>>,
1593 ) -> Result<()> {
1594 self.frame
1595 .locator_set_input_files_payload(&self.selector, file)
1596 .await
1597 .map_err(|e| self.wrap_error_with_selector(e))
1598 }
1599
1600 /// Sets multiple files to upload using FilePayload.
1601 ///
1602 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1603 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1604 pub async fn set_input_files_payload_multiple(
1605 &self,
1606 files: &[crate::protocol::FilePayload],
1607 _options: impl Into<Option<()>>,
1608 ) -> Result<()> {
1609 self.frame
1610 .locator_set_input_files_payload_multiple(&self.selector, files)
1611 .await
1612 .map_err(|e| self.wrap_error_with_selector(e))
1613 }
1614
1615 /// Dispatches a DOM event on the element.
1616 ///
1617 /// Unlike clicking or typing, `dispatch_event` directly sends the event without
1618 /// performing any actionability checks. It still waits for the element to be present
1619 /// in the DOM.
1620 ///
1621 /// # Arguments
1622 ///
1623 /// * `type_` - The event type to dispatch, e.g. `"click"`, `"focus"`, `"myevent"`.
1624 /// * `event_init` - Optional event initializer properties (e.g. `{"detail": "value"}` for
1625 /// `CustomEvent`). Corresponds to the second argument of `new Event(type, init)`.
1626 ///
1627 /// # Errors
1628 ///
1629 /// Returns an error if:
1630 /// - The element is not found within the timeout
1631 /// - The protocol call fails
1632 ///
1633 /// See: <https://playwright.dev/docs/api/class-locator#locator-dispatch-event>
1634 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1635 pub async fn dispatch_event(
1636 &self,
1637 type_: &str,
1638 event_init: Option<serde_json::Value>,
1639 ) -> Result<()> {
1640 self.frame
1641 .locator_dispatch_event(&self.selector, type_, event_init)
1642 .await
1643 .map_err(|e| self.wrap_error_with_selector(e))
1644 }
1645
1646 /// Returns the bounding box of the element, or `None` if the element is not visible.
1647 ///
1648 /// The bounding box is in pixels, relative to the top-left corner of the page.
1649 /// Returns `None` when the element has `display: none` or is otherwise not part of
1650 /// the layout.
1651 ///
1652 /// # Errors
1653 ///
1654 /// Returns an error if:
1655 /// - The element is not found within the timeout
1656 /// - The protocol call fails
1657 ///
1658 /// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
1659 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1660 pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
1661 self.frame
1662 .locator_bounding_box(&self.selector)
1663 .await
1664 .map_err(|e| self.wrap_error_with_selector(e))
1665 }
1666
1667 /// Scrolls the element into view if it is not already visible in the viewport.
1668 ///
1669 /// This is an alias for calling `element.scrollIntoView()` in the browser.
1670 ///
1671 /// # Errors
1672 ///
1673 /// Returns an error if:
1674 /// - The element is not found within the timeout
1675 /// - The protocol call fails
1676 ///
1677 /// See: <https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed>
1678 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1679 pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
1680 self.frame
1681 .locator_scroll_into_view_if_needed(&self.selector)
1682 .await
1683 .map_err(|e| self.wrap_error_with_selector(e))
1684 }
1685
1686 /// Takes a screenshot of the element and returns the image bytes.
1687 ///
1688 /// This method uses strict mode - it will fail if the selector matches multiple elements.
1689 /// Use `first()`, `last()`, or `nth()` to refine the selector to a single element.
1690 ///
1691 /// See: <https://playwright.dev/docs/api/class-locator#locator-screenshot>
1692 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector, bytes_len = tracing::field::Empty))]
1693 pub async fn screenshot(
1694 &self,
1695 options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
1696 ) -> Result<Vec<u8>> {
1697 let options = options.into();
1698 // Query for the element using strict mode (should return exactly one)
1699 let element = self
1700 .frame
1701 .query_selector(&self.selector)
1702 .await
1703 .map_err(|e| self.wrap_error_with_selector(e))?
1704 .ok_or_else(|| {
1705 crate::error::Error::ElementNotFound(format!(
1706 "Element not found: {}",
1707 self.selector
1708 ))
1709 })?;
1710
1711 // Delegate to ElementHandle.screenshot() with default timeout injected
1712 let bytes = element
1713 .screenshot(Some(self.with_timeout(options)))
1714 .await
1715 .map_err(|e| self.wrap_error_with_selector(e))?;
1716 tracing::Span::current().record("bytes_len", bytes.len());
1717 Ok(bytes)
1718 }
1719
1720 /// Performs a touch-tap on the element.
1721 ///
1722 /// This method dispatches a `touchstart` and `touchend` event on the element.
1723 /// For touch support to work, the browser context must be created with
1724 /// `has_touch: true`.
1725 ///
1726 /// # Arguments
1727 ///
1728 /// * `options` - Optional [`TapOptions`](crate::protocol::TapOptions) (force, modifiers, position, timeout, trial)
1729 ///
1730 /// # Errors
1731 ///
1732 /// Returns an error if:
1733 /// - The element is not found within the timeout
1734 /// - Actionability checks fail (unless `force: true`)
1735 /// - The browser context was not created with `has_touch: true`
1736 ///
1737 /// See: <https://playwright.dev/docs/api/class-locator#locator-tap>
1738 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1739 pub async fn tap(&self, options: impl Into<Option<crate::protocol::TapOptions>>) -> Result<()> {
1740 let options = options.into();
1741 self.frame
1742 .locator_tap(&self.selector, Some(self.with_timeout(options)))
1743 .await
1744 .map_err(|e| self.wrap_error_with_selector(e))
1745 }
1746
1747 /// Drags this element to the `target` element.
1748 ///
1749 /// Both this locator and `target` must resolve to elements in the same frame.
1750 /// Playwright performs a series of mouse events (move, press, move to target, release)
1751 /// to simulate the drag, so it drives real pointer-event chains, including
1752 /// UIs that call `setPointerCapture` in their `pointerdown` handler.
1753 ///
1754 /// `source_position` and `target_position` are offsets from the respective
1755 /// element's top-left corner. Setting `target_position` with a containing
1756 /// element (a canvas or stage) as `target` turns this into a drag to a
1757 /// coordinate rather than onto an element, which also covers "drag by a
1758 /// delta": compute the drop point from the source's position within the
1759 /// container. Prefer this over a held-button
1760 /// [`Mouse::move_to`](crate::protocol::Mouse::move_to) sequence, which can
1761 /// hang on headless Linux (see the note on that method).
1762 ///
1763 /// # Arguments
1764 ///
1765 /// * `target` - The locator of the element to drag onto
1766 /// * `options` - Optional [`DragToOptions`](crate::protocol::DragToOptions) (force, no_wait_after, timeout, trial,
1767 /// source_position, target_position)
1768 ///
1769 /// # Example
1770 ///
1771 /// ```no_run
1772 /// # use playwright_rs::{Playwright, DragToOptions, Position};
1773 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1774 /// # let pw = Playwright::launch().await?;
1775 /// # let page = pw.chromium().launch().await?.new_page().await?;
1776 /// let handle = page.locator(".crop-handle");
1777 /// let stage = page.locator("#stage");
1778 ///
1779 /// // Drop the handle 180px right, 120px down from the stage's top-left corner.
1780 /// let opts = DragToOptions::builder()
1781 /// .target_position(Position { x: 180.0, y: 120.0 })
1782 /// .build();
1783 /// handle.drag_to(&stage, Some(opts)).await?;
1784 /// # Ok(())
1785 /// # }
1786 /// ```
1787 ///
1788 /// # Errors
1789 ///
1790 /// Returns an error if:
1791 /// - Either element is not found within the timeout
1792 /// - Actionability checks fail (unless `force: true`)
1793 /// - The protocol call fails
1794 ///
1795 /// See: <https://playwright.dev/docs/api/class-locator#locator-drag-to>
1796 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1797 pub async fn drag_to(
1798 &self,
1799 target: &Locator,
1800 options: impl Into<Option<crate::protocol::DragToOptions>>,
1801 ) -> Result<()> {
1802 let options = options.into();
1803 self.frame
1804 .locator_drag_to(
1805 &self.selector,
1806 &target.selector,
1807 Some(self.with_timeout(options)),
1808 )
1809 .await
1810 .map_err(|e| self.wrap_error_with_selector(e))
1811 }
1812
1813 /// Drops files and/or data onto this element (external drag-and-drop).
1814 ///
1815 /// Simulates dragging files or data from outside the page onto the element,
1816 /// such as an upload drop zone, by dispatching `dragenter`/`dragover`/`drop`
1817 /// with a synthetic `DataTransfer`. Set `files` and/or `data` on the
1818 /// [`DropOptions`](crate::protocol::DropOptions). This is distinct from
1819 /// [`drag_to`](Self::drag_to), which drags one element onto another within
1820 /// the page.
1821 ///
1822 /// # Arguments
1823 ///
1824 /// * `options` - [`DropOptions`](crate::protocol::DropOptions) carrying the
1825 /// files / data to drop, plus optional `position` and `timeout`.
1826 ///
1827 /// # Errors
1828 ///
1829 /// Returns an error if:
1830 /// - The element is not found within the timeout
1831 /// - Actionability checks fail
1832 /// - The protocol call fails (e.g. neither files nor data were provided)
1833 ///
1834 /// See: <https://playwright.dev/docs/api/class-locator#locator-drop>
1835 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1836 pub async fn drop(&self, options: crate::protocol::DropOptions) -> Result<()> {
1837 self.frame
1838 .locator_drop(&self.selector, self.with_timeout(Some(options)))
1839 .await
1840 .map_err(|e| self.wrap_error_with_selector(e))
1841 }
1842
1843 /// Waits until the element satisfies the given state condition.
1844 ///
1845 /// If no state is specified, waits for the element to be `visible` (the default).
1846 ///
1847 /// This method is useful for waiting for lazy-rendered elements or elements that
1848 /// appear/disappear based on user interaction or async data loading.
1849 ///
1850 /// # Arguments
1851 ///
1852 /// * `options` - Optional [`WaitForOptions`](crate::protocol::WaitForOptions) specifying the `state` to wait for
1853 /// (`Visible`, `Hidden`, `Attached`, or `Detached`) and a `timeout` in milliseconds.
1854 ///
1855 /// # Errors
1856 ///
1857 /// Returns an error if the element does not satisfy the expected state within the timeout.
1858 ///
1859 /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for>
1860 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1861 pub async fn wait_for(
1862 &self,
1863 options: impl Into<Option<crate::protocol::WaitForOptions>>,
1864 ) -> Result<()> {
1865 let options = options.into();
1866 self.frame
1867 .locator_wait_for(&self.selector, Some(self.with_timeout(options)))
1868 .await
1869 .map_err(|e| self.wrap_error_with_selector(e))
1870 }
1871
1872 /// Evaluates a JavaScript expression in the scope of the matched element.
1873 ///
1874 /// The element is passed as the first argument to the expression. The expression
1875 /// can be any JavaScript function or expression that returns a JSON-serializable value.
1876 ///
1877 /// # Arguments
1878 ///
1879 /// * `expression` - JavaScript expression or function, e.g. `"(el) => el.textContent"`
1880 /// * `arg` - Optional argument passed as the second argument to the function
1881 ///
1882 /// # Errors
1883 ///
1884 /// Returns an error if:
1885 /// - The element is not found within the timeout
1886 /// - The JavaScript expression throws an error
1887 /// - The return value is not JSON-serializable
1888 ///
1889 /// # Example
1890 ///
1891 /// ```no_run
1892 /// use playwright_rs::Playwright;
1893 ///
1894 /// # #[tokio::main]
1895 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1896 /// let playwright = Playwright::launch().await?;
1897 /// let browser = playwright.chromium().launch().await?;
1898 /// let page = browser.new_page().await?;
1899 /// let _ = page.goto("data:text/html,<h1>Hello</h1>", None).await;
1900 ///
1901 /// let heading = page.locator("h1");
1902 /// let text: String = heading.evaluate("(el) => el.textContent", None::<()>).await?;
1903 /// assert_eq!(text, "Hello");
1904 ///
1905 /// // With an argument
1906 /// let result: String = heading
1907 /// .evaluate("(el, suffix) => el.textContent + suffix", Some("!"))
1908 /// .await?;
1909 /// assert_eq!(result, "Hello!");
1910 /// # browser.close().await?;
1911 /// # Ok(())
1912 /// # }
1913 /// ```
1914 ///
1915 /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate>
1916 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1917 pub async fn evaluate<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1918 where
1919 R: serde::de::DeserializeOwned,
1920 T: serde::Serialize,
1921 {
1922 let raw = self
1923 .frame
1924 .locator_evaluate(&self.selector, expression, arg)
1925 .await
1926 .map_err(|e| self.wrap_error_with_selector(e))?;
1927 serde_json::from_value(raw).map_err(|e| {
1928 crate::error::Error::ProtocolError(format!(
1929 "evaluate result deserialization failed: {}",
1930 e
1931 ))
1932 })
1933 }
1934
1935 /// Evaluates a JavaScript expression in the scope of all elements matching this locator.
1936 ///
1937 /// The array of all matched elements is passed as the first argument to the expression.
1938 /// Unlike [`evaluate()`](Self::evaluate), this does not use strict mode — all matching
1939 /// elements are collected and passed as an array.
1940 ///
1941 /// # Arguments
1942 ///
1943 /// * `expression` - JavaScript function that receives an array of elements
1944 /// * `arg` - Optional argument passed as the second argument to the function
1945 ///
1946 /// # Errors
1947 ///
1948 /// Returns an error if:
1949 /// - The JavaScript expression throws an error
1950 /// - The return value is not JSON-serializable
1951 ///
1952 /// # Example
1953 ///
1954 /// ```no_run
1955 /// use playwright_rs::Playwright;
1956 ///
1957 /// # #[tokio::main]
1958 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1959 /// let playwright = Playwright::launch().await?;
1960 /// let browser = playwright.chromium().launch().await?;
1961 /// let page = browser.new_page().await?;
1962 /// let _ = page.goto(
1963 /// "data:text/html,<li class='item'>A</li><li class='item'>B</li>",
1964 /// None
1965 /// ).await;
1966 ///
1967 /// let items = page.locator(".item");
1968 /// let texts: Vec<String> = items
1969 /// .evaluate_all("(elements) => elements.map(e => e.textContent)", None::<()>)
1970 /// .await?;
1971 /// assert_eq!(texts, vec!["A", "B"]);
1972 /// # browser.close().await?;
1973 /// # Ok(())
1974 /// # }
1975 /// ```
1976 ///
1977 /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate-all>
1978 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1979 pub async fn evaluate_all<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1980 where
1981 R: serde::de::DeserializeOwned,
1982 T: serde::Serialize,
1983 {
1984 let raw = self
1985 .frame
1986 .locator_evaluate_all(&self.selector, expression, arg)
1987 .await
1988 .map_err(|e| self.wrap_error_with_selector(e))?;
1989 serde_json::from_value(raw).map_err(|e| {
1990 crate::error::Error::ProtocolError(format!(
1991 "evaluate_all result deserialization failed: {}",
1992 e
1993 ))
1994 })
1995 }
1996
1997 /// Returns the ARIA accessibility tree snapshot as a YAML string.
1998 ///
1999 /// The snapshot describes the accessible roles, names, and properties of the matched
2000 /// element and its descendants. This is useful for writing stable accessibility assertions
2001 /// that are independent of CSS classes or DOM structure.
2002 ///
2003 /// # Errors
2004 ///
2005 /// Returns an error if:
2006 /// - The element is not found within the timeout
2007 /// - The protocol call fails
2008 ///
2009 /// See: <https://playwright.dev/docs/api/class-locator#locator-aria-snapshot>
2010 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, mode = tracing::field::Empty))]
2011 pub async fn aria_snapshot(
2012 &self,
2013 options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
2014 ) -> Result<String> {
2015 let options = options.into();
2016 self.frame
2017 .locator_aria_snapshot(&self.selector, options.as_ref())
2018 .await
2019 .map_err(|e| self.wrap_error_with_selector(e))
2020 }
2021
2022 /// The accessibility tree of the matched element, as JSON rather than
2023 /// the YAML markup [`aria_snapshot`](Self::aria_snapshot) returns, so a
2024 /// caller can walk it instead of parsing text.
2025 ///
2026 /// Takes the same options; `mode`, `depth`, and `boxes` all apply.
2027 ///
2028 /// # Errors
2029 ///
2030 /// Returns an error if the element is not found within the timeout, or
2031 /// if the driver rejects the request.
2032 ///
2033 /// See: <https://playwright.dev/docs/api/class-locator#locator-aria-snapshot-json>
2034 pub async fn aria_snapshot_json(
2035 &self,
2036 options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
2037 ) -> Result<serde_json::Value> {
2038 let options = options.into();
2039 self.frame
2040 .locator_aria_snapshot_json(&self.selector, options.as_ref())
2041 .await
2042 .map_err(|e| self.wrap_error_with_selector(e))
2043 }
2044
2045 /// Returns a new locator whose selector has been resolved to a
2046 /// best-practices canonical form — preferring test-ids, then ARIA
2047 /// roles, then accessible text. The resolved locator points at the
2048 /// same element(s) as `self` but uses a more robust selector that
2049 /// is less coupled to CSS classes or DOM structure. Useful as a
2050 /// building block for codegen helpers that want the "most stable
2051 /// selector for this element" primitive.
2052 ///
2053 /// See the module-level example for usage.
2054 ///
2055 /// # Errors
2056 ///
2057 /// Returns an error if:
2058 /// - No element matches the original selector
2059 /// - The protocol call fails
2060 ///
2061 /// See: <https://playwright.dev/docs/api/class-locator#locator-normalize>
2062 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
2063 pub async fn normalize(&self) -> Result<Locator> {
2064 let resolved = self
2065 .frame
2066 .frame_resolve_selector(&self.selector)
2067 .await
2068 .map_err(|e| self.wrap_error_with_selector(e))?;
2069 Ok(Locator {
2070 frame: Arc::clone(&self.frame),
2071 selector: resolved,
2072 page: self.page.clone(),
2073 })
2074 }
2075
2076 /// Returns a new Locator with an attached description for traces and error messages.
2077 ///
2078 /// The description does not affect element matching — it is purely informational,
2079 /// appearing in trace viewer labels and error messages to make them more readable.
2080 ///
2081 /// Appends `>> internal:describe="description"` to the selector, matching
2082 /// playwright-python's behavior exactly.
2083 ///
2084 /// See: <https://playwright.dev/docs/api/class-locator#locator-describe>
2085 pub fn describe(&self, description: &str) -> Locator {
2086 let escaped =
2087 serde_json::to_string(description).unwrap_or_else(|_| format!("\"{}\"", description));
2088 Locator::new(
2089 Arc::clone(&self.frame),
2090 format!("{} >> internal:describe={}", self.selector, escaped),
2091 self.page.clone(),
2092 )
2093 }
2094
2095 /// Highlights the matched element in the browser for visual debugging.
2096 ///
2097 /// Draws a colored overlay over the element for a short period. This is a
2098 /// debugging tool and has no effect on test assertions or element state.
2099 ///
2100 /// # Errors
2101 ///
2102 /// Returns an error if:
2103 /// - The element is not found within the timeout
2104 /// - The protocol call fails
2105 ///
2106 /// See: <https://playwright.dev/docs/api/class-locator#locator-highlight>
2107 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
2108 pub async fn highlight(&self, options: impl Into<Option<HighlightOptions>>) -> Result<()> {
2109 let options = options.into();
2110 let style = options.and_then(|o| o.style);
2111 self.frame
2112 .locator_highlight(&self.selector, style.as_deref())
2113 .await
2114 .map_err(|e| self.wrap_error_with_selector(e))
2115 }
2116
2117 /// Returns a [`FrameLocator`](crate::protocol::FrameLocator) for the content of an
2118 /// `<iframe>` element matched by this locator.
2119 ///
2120 /// This is a client-side operation — it creates a `FrameLocator` scoped to the matched
2121 /// iframe element, allowing you to interact with elements inside the iframe using the
2122 /// standard `FrameLocator` API.
2123 ///
2124 /// Equivalent to `page.frame_locator(selector)`, but starting from an existing `Locator`.
2125 ///
2126 /// See: <https://playwright.dev/docs/api/class-locator#locator-content-frame>
2127 pub fn content_frame(&self) -> crate::protocol::FrameLocator {
2128 crate::protocol::FrameLocator::new(
2129 Arc::clone(&self.frame),
2130 self.selector.clone(),
2131 self.page.clone(),
2132 )
2133 }
2134}
2135
2136impl std::fmt::Debug for Locator {
2137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2138 f.debug_struct("Locator")
2139 .field("selector", &self.selector)
2140 .finish()
2141 }
2142}
2143
2144#[cfg(test)]
2145mod tests {
2146 use super::*;
2147
2148 #[test]
2149 fn test_escape_for_selector_case_insensitive() {
2150 assert_eq!(escape_for_selector("hello", false), "\"hello\"i");
2151 }
2152
2153 #[test]
2154 fn test_escape_for_selector_exact() {
2155 assert_eq!(escape_for_selector("hello", true), "\"hello\"s");
2156 }
2157
2158 #[test]
2159 fn test_escape_for_selector_with_quotes() {
2160 assert_eq!(
2161 escape_for_selector("say \"hi\"", false),
2162 "\"say \\\"hi\\\"\"i"
2163 );
2164 }
2165
2166 #[test]
2167 fn test_get_by_text_selector_case_insensitive() {
2168 assert_eq!(
2169 get_by_text_selector("Click me", false),
2170 "internal:text=\"Click me\"i"
2171 );
2172 }
2173
2174 #[test]
2175 fn test_get_by_text_selector_exact() {
2176 assert_eq!(
2177 get_by_text_selector("Click me", true),
2178 "internal:text=\"Click me\"s"
2179 );
2180 }
2181
2182 #[test]
2183 fn test_get_by_label_selector() {
2184 assert_eq!(
2185 get_by_label_selector("Email", false),
2186 "internal:label=\"Email\"i"
2187 );
2188 }
2189
2190 #[test]
2191 fn test_get_by_placeholder_selector() {
2192 assert_eq!(
2193 get_by_placeholder_selector("Enter name", false),
2194 "internal:attr=[placeholder=\"Enter name\"i]"
2195 );
2196 }
2197
2198 #[test]
2199 fn test_get_by_alt_text_selector() {
2200 assert_eq!(
2201 get_by_alt_text_selector("Logo", true),
2202 "internal:attr=[alt=\"Logo\"s]"
2203 );
2204 }
2205
2206 #[test]
2207 fn test_get_by_title_selector() {
2208 assert_eq!(
2209 get_by_title_selector("Help", false),
2210 "internal:attr=[title=\"Help\"i]"
2211 );
2212 }
2213
2214 #[test]
2215 fn test_get_by_test_id_selector() {
2216 assert_eq!(
2217 get_by_test_id_selector("submit-btn"),
2218 "internal:testid=[data-testid=\"submit-btn\"s]"
2219 );
2220 }
2221
2222 #[test]
2223 fn test_escape_for_attribute_selector_case_insensitive() {
2224 assert_eq!(
2225 escape_for_attribute_selector("Submit", false),
2226 "\"Submit\"i"
2227 );
2228 }
2229
2230 #[test]
2231 fn test_escape_for_attribute_selector_exact() {
2232 assert_eq!(escape_for_attribute_selector("Submit", true), "\"Submit\"s");
2233 }
2234
2235 #[test]
2236 fn test_escape_for_attribute_selector_escapes_quotes() {
2237 assert_eq!(
2238 escape_for_attribute_selector("Say \"hello\"", false),
2239 "\"Say \\\"hello\\\"\"i"
2240 );
2241 }
2242
2243 #[test]
2244 fn test_escape_for_attribute_selector_escapes_backslashes() {
2245 assert_eq!(
2246 escape_for_attribute_selector("path\\to", true),
2247 "\"path\\\\to\"s"
2248 );
2249 }
2250
2251 #[test]
2252 fn test_get_by_role_selector_role_only() {
2253 assert_eq!(
2254 get_by_role_selector(AriaRole::Button, None),
2255 "internal:role=button"
2256 );
2257 }
2258
2259 #[test]
2260 fn test_get_by_role_selector_with_name() {
2261 let opts = GetByRoleOptions::default().name("Submit");
2262 assert_eq!(
2263 get_by_role_selector(AriaRole::Button, Some(opts)),
2264 "internal:role=button[name=\"Submit\"i]"
2265 );
2266 }
2267
2268 #[test]
2269 fn test_filter_options_setters() {
2270 let opts = FilterOptions::default()
2271 .has_text("a")
2272 .has_not_text("b")
2273 .visible(false);
2274 assert_eq!(opts.has_text.as_deref(), Some("a"));
2275 assert_eq!(opts.has_not_text.as_deref(), Some("b"));
2276 assert_eq!(opts.visible, Some(false));
2277 assert_eq!(FilterOptions::default().visible, None);
2278 }
2279
2280 #[test]
2281 fn filter_selector_appends_each_narrowing_in_order() {
2282 let options = FilterOptions::default()
2283 .has_text("Apple")
2284 .has_not_text("Pear")
2285 .visible(true);
2286
2287 assert_eq!(
2288 filter_selector("tr", &options, Some("td.price"), Some("td.sold")),
2289 "tr >> internal:has-text=\"Apple\"i >> internal:has-not-text=\"Pear\"i \
2290 >> internal:has=\"td.price\" >> internal:has-not=\"td.sold\" >> visible=true"
2291 );
2292 }
2293
2294 #[test]
2295 fn filter_selector_leaves_the_base_alone_when_nothing_is_set() {
2296 assert_eq!(
2297 filter_selector("tr", &FilterOptions::default(), None, None),
2298 "tr"
2299 );
2300 }
2301
2302 #[test]
2303 fn filter_selector_spells_the_hidden_half() {
2304 let options = FilterOptions::default().visible(false);
2305 assert_eq!(
2306 filter_selector(".item", &options, None, None),
2307 ".item >> visible=false"
2308 );
2309 }
2310
2311 #[test]
2312 fn test_get_by_role_selector_with_description() {
2313 let opts = GetByRoleOptions::default().description("Close dialog");
2314 assert_eq!(
2315 get_by_role_selector(AriaRole::Button, Some(opts)),
2316 "internal:role=button[description=\"Close dialog\"i]"
2317 );
2318 let exact = GetByRoleOptions::default().description("Close").exact(true);
2319 assert_eq!(
2320 get_by_role_selector(AriaRole::Button, Some(exact)),
2321 "internal:role=button[description=\"Close\"s]"
2322 );
2323 }
2324
2325 #[test]
2326 fn test_get_by_role_selector_with_name_exact() {
2327 let opts = GetByRoleOptions::default().name("Submit").exact(true);
2328 assert_eq!(
2329 get_by_role_selector(AriaRole::Button, Some(opts)),
2330 "internal:role=button[name=\"Submit\"s]"
2331 );
2332 }
2333
2334 #[test]
2335 fn test_get_by_role_selector_with_checked() {
2336 let opts = GetByRoleOptions::default().checked(true);
2337 assert_eq!(
2338 get_by_role_selector(AriaRole::Checkbox, Some(opts)),
2339 "internal:role=checkbox[checked=true]"
2340 );
2341 }
2342
2343 #[test]
2344 fn test_get_by_role_selector_with_level() {
2345 let opts = GetByRoleOptions::default().level(2);
2346 assert_eq!(
2347 get_by_role_selector(AriaRole::Heading, Some(opts)),
2348 "internal:role=heading[level=2]"
2349 );
2350 }
2351
2352 #[test]
2353 fn test_get_by_role_selector_with_disabled() {
2354 let opts = GetByRoleOptions::default().disabled(true);
2355 assert_eq!(
2356 get_by_role_selector(AriaRole::Button, Some(opts)),
2357 "internal:role=button[disabled=true]"
2358 );
2359 }
2360
2361 #[test]
2362 fn test_get_by_role_selector_with_selected() {
2363 let opts = GetByRoleOptions::default().selected(true);
2364 assert_eq!(
2365 get_by_role_selector(AriaRole::Option, Some(opts)),
2366 "internal:role=option[selected=true]"
2367 );
2368 }
2369
2370 #[test]
2371 fn test_get_by_role_selector_with_expanded() {
2372 let opts = GetByRoleOptions::default().expanded(true);
2373 assert_eq!(
2374 get_by_role_selector(AriaRole::Button, Some(opts)),
2375 "internal:role=button[expanded=true]"
2376 );
2377 }
2378
2379 #[test]
2380 fn test_get_by_role_selector_include_hidden() {
2381 let opts = GetByRoleOptions::default().include_hidden(true);
2382 assert_eq!(
2383 get_by_role_selector(AriaRole::Button, Some(opts)),
2384 "internal:role=button[include-hidden=true]"
2385 );
2386 }
2387
2388 #[test]
2389 fn test_get_by_role_selector_property_order() {
2390 // All properties: checked, disabled, selected, expanded, include-hidden, level, name, pressed
2391 let opts = GetByRoleOptions::default()
2392 .pressed(true)
2393 .name("OK")
2394 .checked(false)
2395 .disabled(true);
2396 assert_eq!(
2397 get_by_role_selector(AriaRole::Button, Some(opts)),
2398 "internal:role=button[checked=false][disabled=true][name=\"OK\"i][pressed=true]"
2399 );
2400 }
2401
2402 #[test]
2403 fn test_get_by_role_selector_name_with_special_chars() {
2404 let opts = GetByRoleOptions::default()
2405 .name("Click \"here\" now")
2406 .exact(true);
2407 assert_eq!(
2408 get_by_role_selector(AriaRole::Link, Some(opts)),
2409 "internal:role=link[name=\"Click \\\"here\\\" now\"s]"
2410 );
2411 }
2412
2413 #[test]
2414 fn test_aria_role_as_str() {
2415 assert_eq!(AriaRole::Button.as_str(), "button");
2416 assert_eq!(AriaRole::Heading.as_str(), "heading");
2417 assert_eq!(AriaRole::Link.as_str(), "link");
2418 assert_eq!(AriaRole::Checkbox.as_str(), "checkbox");
2419 assert_eq!(AriaRole::Alert.as_str(), "alert");
2420 assert_eq!(AriaRole::Navigation.as_str(), "navigation");
2421 assert_eq!(AriaRole::Progressbar.as_str(), "progressbar");
2422 assert_eq!(AriaRole::Treeitem.as_str(), "treeitem");
2423 }
2424
2425 #[test]
2426 fn highlight_options_style_setter_writes_the_field() {
2427 let opts = HighlightOptions::default().style("outline: 2px solid red");
2428 assert_eq!(opts.style.as_deref(), Some("outline: 2px solid red"));
2429 }
2430}