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
521/// Options for [`Locator::filter()`].
522///
523/// Narrows an existing locator according to the specified criteria.
524/// All fields are optional; unset fields are ignored.
525///
526/// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
527#[derive(Debug, Clone, Default)]
528#[non_exhaustive]
529pub struct FilterOptions {
530 /// Matches elements containing the specified text (case-insensitive substring by default).
531 pub has_text: Option<String>,
532 /// Matches elements that do **not** contain the specified text anywhere inside.
533 pub has_not_text: Option<String>,
534 /// Narrows to elements that contain a descendant matching this locator.
535 ///
536 /// The inner locator is queried relative to the outer locator's matched element,
537 /// not the document root.
538 pub has: Option<Locator>,
539 /// Narrows to elements that do **not** contain a descendant matching this locator.
540 pub has_not: Option<Locator>,
541}
542
543impl FilterOptions {
544 /// Keep only elements containing the given text (case-insensitive substring).
545 pub fn has_text(mut self, has_text: impl Into<String>) -> Self {
546 self.has_text = Some(has_text.into());
547 self
548 }
549 /// Keep only elements NOT containing the given text.
550 pub fn has_not_text(mut self, has_not_text: impl Into<String>) -> Self {
551 self.has_not_text = Some(has_not_text.into());
552 self
553 }
554 /// Keep only elements containing a match for the inner locator.
555 pub fn has(mut self, has: Locator) -> Self {
556 self.has = Some(has);
557 self
558 }
559 /// Keep only elements NOT containing a match for the inner locator.
560 pub fn has_not(mut self, has_not: Locator) -> Self {
561 self.has_not = Some(has_not);
562 self
563 }
564}
565
566/// Locator represents a way to find element(s) on the page at any given moment.
567///
568/// Locators are lazy - they don't execute queries until an action is performed.
569/// This enables auto-waiting and retry-ability for robust test automation.
570///
571/// # Examples
572///
573/// ```no_run
574/// use playwright_rs::protocol::{Playwright, SelectOption};
575///
576/// #[tokio::main]
577/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
578/// let playwright = Playwright::launch().await?;
579/// let browser = playwright.chromium().launch().await?;
580/// let page = browser.new_page().await?;
581///
582/// // Demonstrate set_checked() - checkbox interaction
583/// let _ = page.goto(
584/// "data:text/html,<input type='checkbox' id='cb'>",
585/// None
586/// ).await;
587/// let checkbox = page.locator("#cb");
588/// checkbox.set_checked(true, None).await?;
589/// assert!(checkbox.is_checked().await?);
590/// checkbox.set_checked(false, None).await?;
591/// assert!(!checkbox.is_checked().await?);
592///
593/// // Demonstrate select_option() - select by value, label, and index
594/// let _ = page.goto(
595/// "data:text/html,<select id='fruits'>\
596/// <option value='apple'>Apple</option>\
597/// <option value='banana'>Banana</option>\
598/// <option value='cherry'>Cherry</option>\
599/// </select>",
600/// None
601/// ).await;
602/// let select = page.locator("#fruits");
603/// select.select_option("banana", None).await?;
604/// assert_eq!(select.input_value(None).await?, "banana");
605/// select.select_option(SelectOption::Label("Apple".to_string()), None).await?;
606/// assert_eq!(select.input_value(None).await?, "apple");
607/// select.select_option(SelectOption::Index(2), None).await?;
608/// assert_eq!(select.input_value(None).await?, "cherry");
609///
610/// // Demonstrate select_option_multiple() - multi-select
611/// let _ = page.goto(
612/// "data:text/html,<select id='colors' multiple>\
613/// <option value='red'>Red</option>\
614/// <option value='green'>Green</option>\
615/// <option value='blue'>Blue</option>\
616/// <option value='yellow'>Yellow</option>\
617/// </select>",
618/// None
619/// ).await;
620/// let multi = page.locator("#colors");
621/// let selected = multi.select_option_multiple(&["red", "blue"], None).await?;
622/// assert_eq!(selected.len(), 2);
623/// assert!(selected.contains(&"red".to_string()));
624/// assert!(selected.contains(&"blue".to_string()));
625///
626/// // Demonstrate get_by_text() - find elements by text content
627/// let _ = page.goto(
628/// "data:text/html,<button>Submit</button><button>Submit Order</button>",
629/// None
630/// ).await;
631/// let all_submits = page.get_by_text("Submit", false);
632/// assert_eq!(all_submits.count().await?, 2); // case-insensitive substring
633/// let exact_submit = page.get_by_text("Submit", true);
634/// assert_eq!(exact_submit.count().await?, 1); // exact match only
635///
636/// // Demonstrate get_by_label, get_by_placeholder, get_by_test_id
637/// let _ = page.goto(
638/// "data:text/html,<label for='email'>Email</label>\
639/// <input id='email' placeholder='you@example.com' data-testid='email-input' />",
640/// None
641/// ).await;
642/// let by_label = page.get_by_label("Email", false);
643/// assert_eq!(by_label.count().await?, 1);
644/// let by_placeholder = page.get_by_placeholder("you@example.com", true);
645/// assert_eq!(by_placeholder.count().await?, 1);
646/// let by_test_id = page.get_by_test_id("email-input");
647/// assert_eq!(by_test_id.count().await?, 1);
648///
649/// // Demonstrate screenshot() - element screenshot
650/// let _ = page.goto(
651/// "data:text/html,<h1 id='title'>Hello World</h1>",
652/// None
653/// ).await;
654/// let heading = page.locator("#title");
655/// let screenshot = heading.screenshot(None).await?;
656/// assert!(!screenshot.is_empty());
657///
658/// browser.close().await?;
659/// Ok(())
660/// }
661/// ```
662///
663/// See: <https://playwright.dev/docs/api/class-locator>
664#[derive(Clone)]
665pub struct Locator {
666 frame: Arc<Frame>,
667 selector: String,
668 page: crate::protocol::Page,
669}
670
671impl Locator {
672 /// Creates a new Locator (internal use only)
673 ///
674 /// Use `page.locator()` or `frame.locator()` to create locators in application code.
675 pub(crate) fn new(frame: Arc<Frame>, selector: String, page: crate::protocol::Page) -> Self {
676 Self {
677 frame,
678 selector,
679 page,
680 }
681 }
682
683 /// Returns the selector string for this locator
684 pub fn selector(&self) -> &str {
685 &self.selector
686 }
687
688 /// Returns the underlying frame for this locator (crate-internal use only).
689 pub(crate) fn frame(&self) -> &Arc<Frame> {
690 &self.frame
691 }
692
693 /// Waits until `expression` returns a truthy value, with the matched
694 /// element passed as its first argument.
695 ///
696 /// The element is resolved with strict matching, so a selector matching
697 /// more than one element is an error rather than a silent pick of the
698 /// first. Returns `()` rather than a handle: the protocol omits the
699 /// result when a selector is supplied, so there is nothing to hand back.
700 ///
701 /// `WaitForFunctionOptions::polling_interval` has no effect on this
702 /// form: the driver polls element-scoped waits on its own backoff
703 /// schedule (roughly 100ms growing toward 1s) and reads the interval
704 /// only for the page-global form.
705 ///
706 /// # Errors
707 ///
708 /// Returns an error if the expression does not become truthy within the
709 /// timeout (the page default unless set here), or if the selector
710 /// matches nothing or more than one element. The timeout is enforced by
711 /// the driver, so it surfaces as a protocol error carrying the driver's
712 /// "Timeout ...ms exceeded" message, with the selector appended.
713 ///
714 /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for-function>
715 pub async fn wait_for_function(
716 &self,
717 expression: &str,
718 options: impl Into<Option<crate::protocol::WaitForFunctionOptions>>,
719 ) -> Result<()> {
720 let options = self.with_timeout(options.into());
721 self.frame
722 .wait_for_function_internal(expression, Some(&self.selector), options)
723 .await
724 .map(|_| ())
725 .map_err(|e| self.wrap_error_with_selector(e))
726 }
727
728 /// Serializes this locator as a screenshot `mask` entry — `{ frame, selector }`
729 /// with the frame sent as a channel reference — matching the protocol shape
730 /// the driver expects. Used by [`crate::protocol::ScreenshotOptions`].
731 pub(crate) fn mask_json(&self) -> serde_json::Value {
732 use crate::server::channel_owner::ChannelOwner as _;
733 serde_json::json!({
734 "frame": { "guid": self.frame.guid() },
735 "selector": self.selector,
736 })
737 }
738
739 /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) scoped within this locator's subtree.
740 ///
741 /// The `selector` identifies an iframe element within the locator's scope.
742 ///
743 /// See: <https://playwright.dev/docs/api/class-locator#locator-frame-locator>
744 pub fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
745 crate::protocol::FrameLocator::new(
746 Arc::clone(&self.frame),
747 format!("{} >> {}", self.selector, selector),
748 self.page.clone(),
749 )
750 }
751
752 /// Returns the Page this locator belongs to.
753 ///
754 /// Each locator is bound to the page that created it. Chained locators (via
755 /// `first()`, `last()`, `nth()`, `locator()`, `filter()`, etc.) all return
756 /// the same owning page. This matches the behavior of `locator.page` in
757 /// other Playwright language bindings.
758 ///
759 /// # Example
760 ///
761 /// ```no_run
762 /// # use playwright_rs::Playwright;
763 /// # #[tokio::main]
764 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
765 /// let playwright = Playwright::launch().await?;
766 /// let browser = playwright.chromium().launch().await?;
767 /// let page = browser.new_page().await?;
768 /// page.goto("https://example.com", None).await?;
769 ///
770 /// let locator = page.locator("h1");
771 /// let locator_page = locator.page()?;
772 /// assert_eq!(locator_page.url(), page.url());
773 /// # Ok(())
774 /// # }
775 /// ```
776 ///
777 /// See: <https://playwright.dev/docs/api/class-locator#locator-page>
778 pub fn page(&self) -> Result<crate::protocol::Page> {
779 Ok(self.page.clone())
780 }
781
782 /// Evaluate a JavaScript expression in the frame context.
783 ///
784 /// Used internally for injecting CSS (e.g., disabling animations) before screenshot assertions.
785 #[cfg(feature = "screenshot-diff")]
786 pub(crate) async fn evaluate_js<T: serde::Serialize>(
787 &self,
788 expression: &str,
789 _arg: Option<T>,
790 ) -> Result<()> {
791 self.frame
792 .frame_evaluate_expression(expression)
793 .await
794 .map_err(|e| self.wrap_error_with_selector(e))
795 }
796
797 /// Creates a locator for the first matching element.
798 ///
799 /// See: <https://playwright.dev/docs/api/class-locator#locator-first>
800 pub fn first(&self) -> Locator {
801 Locator::new(
802 Arc::clone(&self.frame),
803 format!("{} >> nth=0", self.selector),
804 self.page.clone(),
805 )
806 }
807
808 /// Creates a locator for the last matching element.
809 ///
810 /// See: <https://playwright.dev/docs/api/class-locator#locator-last>
811 pub fn last(&self) -> Locator {
812 Locator::new(
813 Arc::clone(&self.frame),
814 format!("{} >> nth=-1", self.selector),
815 self.page.clone(),
816 )
817 }
818
819 /// Creates a locator for the nth matching element (0-indexed).
820 ///
821 /// See: <https://playwright.dev/docs/api/class-locator#locator-nth>
822 pub fn nth(&self, index: i32) -> Locator {
823 Locator::new(
824 Arc::clone(&self.frame),
825 format!("{} >> nth={}", self.selector, index),
826 self.page.clone(),
827 )
828 }
829
830 /// Returns a locator that matches elements containing the given text.
831 ///
832 /// By default, matching is case-insensitive and searches for a substring.
833 /// Set `exact` to `true` for case-sensitive exact matching.
834 ///
835 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-text>
836 pub fn get_by_text(&self, text: &str, exact: bool) -> Locator {
837 self.locator(get_by_text_selector(text, exact))
838 }
839
840 /// Returns a locator that matches elements by their associated label text.
841 ///
842 /// Targets form controls (`input`, `textarea`, `select`) linked via `<label>`,
843 /// `aria-label`, or `aria-labelledby`.
844 ///
845 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-label>
846 pub fn get_by_label(&self, text: &str, exact: bool) -> Locator {
847 self.locator(get_by_label_selector(text, exact))
848 }
849
850 /// Returns a locator that matches elements by their placeholder text.
851 ///
852 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-placeholder>
853 pub fn get_by_placeholder(&self, text: &str, exact: bool) -> Locator {
854 self.locator(get_by_placeholder_selector(text, exact))
855 }
856
857 /// Returns a locator that matches elements by their alt text.
858 ///
859 /// Typically used for `<img>` elements.
860 ///
861 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-alt-text>
862 pub fn get_by_alt_text(&self, text: &str, exact: bool) -> Locator {
863 self.locator(get_by_alt_text_selector(text, exact))
864 }
865
866 /// Returns a locator that matches elements by their title attribute.
867 ///
868 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-title>
869 pub fn get_by_title(&self, text: &str, exact: bool) -> Locator {
870 self.locator(get_by_title_selector(text, exact))
871 }
872
873 /// Returns a locator that matches elements by their test ID attribute.
874 ///
875 /// By default, uses the `data-testid` attribute. Call
876 /// `playwright.selectors().set_test_id_attribute()` to change the attribute name.
877 ///
878 /// Always uses exact matching (case-sensitive).
879 ///
880 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-test-id>
881 pub fn get_by_test_id(&self, test_id: &str) -> Locator {
882 use crate::server::channel_owner::ChannelOwner as _;
883 let attr = self.frame.connection().selectors().test_id_attribute();
884 self.locator(get_by_test_id_selector_with_attr(test_id, &attr))
885 }
886
887 /// Returns a locator that matches elements by their ARIA role.
888 ///
889 /// This is the recommended way to locate elements, as it matches the way
890 /// users and assistive technology perceive the page.
891 ///
892 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-role>
893 pub fn get_by_role(&self, role: AriaRole, options: Option<GetByRoleOptions>) -> Locator {
894 self.locator(get_by_role_selector(role, options))
895 }
896
897 /// Creates a sub-locator within this locator's subtree.
898 ///
899 /// See: <https://playwright.dev/docs/api/class-locator#locator-locator>
900 pub fn locator(&self, selector: impl Into<String>) -> Locator {
901 Locator::new(
902 Arc::clone(&self.frame),
903 format!("{} >> {}", self.selector, selector.into()),
904 self.page.clone(),
905 )
906 }
907
908 /// Narrows this locator according to the filter options.
909 ///
910 /// Can be chained to apply multiple filters in sequence.
911 ///
912 /// # Example
913 ///
914 /// ```no_run
915 /// use playwright_rs::{Playwright, FilterOptions};
916 ///
917 /// # #[tokio::main]
918 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
919 /// let playwright = Playwright::launch().await?;
920 /// let browser = playwright.chromium().launch().await?;
921 /// let page = browser.new_page().await?;
922 ///
923 /// // Filter rows to those containing "Apple"
924 /// let rows = page.locator("tr");
925 /// let apple_row = rows.filter(FilterOptions::default().has_text("Apple"));
926 /// # browser.close().await?;
927 /// # Ok(())
928 /// # }
929 /// ```
930 ///
931 /// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
932 pub fn filter(&self, options: FilterOptions) -> Locator {
933 let mut selector = self.selector.clone();
934
935 if let Some(text) = &options.has_text {
936 let escaped = escape_for_selector(text, false);
937 selector = format!("{} >> internal:has-text={}", selector, escaped);
938 }
939
940 if let Some(text) = &options.has_not_text {
941 let escaped = escape_for_selector(text, false);
942 selector = format!("{} >> internal:has-not-text={}", selector, escaped);
943 }
944
945 if let Some(locator) = &options.has {
946 let inner = serde_json::to_string(&locator.selector)
947 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
948 selector = format!("{} >> internal:has={}", selector, inner);
949 }
950
951 if let Some(locator) = &options.has_not {
952 let inner = serde_json::to_string(&locator.selector)
953 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
954 selector = format!("{} >> internal:has-not={}", selector, inner);
955 }
956
957 Locator::new(Arc::clone(&self.frame), selector, self.page.clone())
958 }
959
960 /// Creates a locator matching elements that satisfy **both** this locator and `locator`.
961 ///
962 /// Note: named `and_` because `and` is a Rust keyword.
963 ///
964 /// # Example
965 ///
966 /// ```no_run
967 /// use playwright_rs::Playwright;
968 ///
969 /// # #[tokio::main]
970 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
971 /// let playwright = Playwright::launch().await?;
972 /// let browser = playwright.chromium().launch().await?;
973 /// let page = browser.new_page().await?;
974 ///
975 /// // Find a button that also has a specific title
976 /// let button = page.locator("button");
977 /// let titled = page.locator("[title='Subscribe']");
978 /// let subscribe_btn = button.and_(&titled);
979 /// # browser.close().await?;
980 /// # Ok(())
981 /// # }
982 /// ```
983 ///
984 /// See: <https://playwright.dev/docs/api/class-locator#locator-and>
985 pub fn and_(&self, locator: &Locator) -> Locator {
986 let inner = serde_json::to_string(&locator.selector)
987 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
988 Locator::new(
989 Arc::clone(&self.frame),
990 format!("{} >> internal:and={}", self.selector, inner),
991 self.page.clone(),
992 )
993 }
994
995 /// Creates a locator matching elements that satisfy **either** this locator or `locator`.
996 ///
997 /// Note: named `or_` because `or` is a Rust keyword.
998 ///
999 /// # Example
1000 ///
1001 /// ```no_run
1002 /// use playwright_rs::Playwright;
1003 ///
1004 /// # #[tokio::main]
1005 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1006 /// let playwright = Playwright::launch().await?;
1007 /// let browser = playwright.chromium().launch().await?;
1008 /// let page = browser.new_page().await?;
1009 ///
1010 /// // Find any element that is either a button or a link
1011 /// let buttons = page.locator("button");
1012 /// let links = page.locator("a");
1013 /// let interactive = buttons.or_(&links);
1014 /// # browser.close().await?;
1015 /// # Ok(())
1016 /// # }
1017 /// ```
1018 ///
1019 /// See: <https://playwright.dev/docs/api/class-locator#locator-or>
1020 pub fn or_(&self, locator: &Locator) -> Locator {
1021 let inner = serde_json::to_string(&locator.selector)
1022 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
1023 Locator::new(
1024 Arc::clone(&self.frame),
1025 format!("{} >> internal:or={}", self.selector, inner),
1026 self.page.clone(),
1027 )
1028 }
1029
1030 /// Returns the number of elements matching this locator.
1031 ///
1032 /// See: <https://playwright.dev/docs/api/class-locator#locator-count>
1033 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, count = tracing::field::Empty))]
1034 pub async fn count(&self) -> Result<usize> {
1035 let n = self
1036 .frame
1037 .locator_count(&self.selector)
1038 .await
1039 .map_err(|e| self.wrap_error_with_selector(e))?;
1040 tracing::Span::current().record("count", n);
1041 Ok(n)
1042 }
1043
1044 /// Returns an array of locators, one for each matching element.
1045 ///
1046 /// Note: `all()` does not wait for elements to match the locator,
1047 /// and instead immediately returns whatever is in the DOM.
1048 ///
1049 /// See: <https://playwright.dev/docs/api/class-locator#locator-all>
1050 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1051 pub async fn all(&self) -> Result<Vec<Locator>> {
1052 let count = self.count().await?;
1053 Ok((0..count).map(|i| self.nth(i as i32)).collect())
1054 }
1055
1056 /// Returns the text content of the element.
1057 ///
1058 /// See: <https://playwright.dev/docs/api/class-locator#locator-text-content>
1059 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1060 pub async fn text_content(&self) -> Result<Option<String>> {
1061 self.frame
1062 .locator_text_content(&self.selector)
1063 .await
1064 .map_err(|e| self.wrap_error_with_selector(e))
1065 }
1066
1067 /// Returns the inner text of the element (visible text).
1068 ///
1069 /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-text>
1070 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1071 pub async fn inner_text(&self) -> Result<String> {
1072 self.frame
1073 .locator_inner_text(&self.selector)
1074 .await
1075 .map_err(|e| self.wrap_error_with_selector(e))
1076 }
1077
1078 /// Returns the inner HTML of the element.
1079 ///
1080 /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-html>
1081 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1082 pub async fn inner_html(&self) -> Result<String> {
1083 self.frame
1084 .locator_inner_html(&self.selector)
1085 .await
1086 .map_err(|e| self.wrap_error_with_selector(e))
1087 }
1088
1089 /// Returns the value of the specified attribute.
1090 ///
1091 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-attribute>
1092 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, name = %name))]
1093 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
1094 self.frame
1095 .locator_get_attribute(&self.selector, name)
1096 .await
1097 .map_err(|e| self.wrap_error_with_selector(e))
1098 }
1099
1100 /// Returns whether the element is visible.
1101 ///
1102 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-visible>
1103 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1104 pub async fn is_visible(&self) -> Result<bool> {
1105 self.frame
1106 .locator_is_visible(&self.selector)
1107 .await
1108 .map_err(|e| self.wrap_error_with_selector(e))
1109 }
1110
1111 /// Returns whether the element is enabled.
1112 ///
1113 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-enabled>
1114 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1115 pub async fn is_enabled(&self) -> Result<bool> {
1116 self.frame
1117 .locator_is_enabled(&self.selector)
1118 .await
1119 .map_err(|e| self.wrap_error_with_selector(e))
1120 }
1121
1122 /// Returns whether the checkbox or radio button is checked.
1123 ///
1124 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-checked>
1125 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1126 pub async fn is_checked(&self) -> Result<bool> {
1127 self.frame
1128 .locator_is_checked(&self.selector)
1129 .await
1130 .map_err(|e| self.wrap_error_with_selector(e))
1131 }
1132
1133 /// Returns whether the element is editable.
1134 ///
1135 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-editable>
1136 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1137 pub async fn is_editable(&self) -> Result<bool> {
1138 self.frame
1139 .locator_is_editable(&self.selector)
1140 .await
1141 .map_err(|e| self.wrap_error_with_selector(e))
1142 }
1143
1144 /// Returns whether the element is hidden.
1145 ///
1146 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-hidden>
1147 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1148 pub async fn is_hidden(&self) -> Result<bool> {
1149 self.frame
1150 .locator_is_hidden(&self.selector)
1151 .await
1152 .map_err(|e| self.wrap_error_with_selector(e))
1153 }
1154
1155 /// Returns whether the element is disabled.
1156 ///
1157 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-disabled>
1158 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1159 pub async fn is_disabled(&self) -> Result<bool> {
1160 self.frame
1161 .locator_is_disabled(&self.selector)
1162 .await
1163 .map_err(|e| self.wrap_error_with_selector(e))
1164 }
1165
1166 /// Returns whether the element is focused (currently has focus).
1167 ///
1168 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-focused>
1169 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1170 pub async fn is_focused(&self) -> Result<bool> {
1171 self.frame
1172 .locator_is_focused(&self.selector)
1173 .await
1174 .map_err(|e| self.wrap_error_with_selector(e))
1175 }
1176
1177 // Action methods
1178
1179 /// Clicks the element.
1180 ///
1181 /// See: <https://playwright.dev/docs/api/class-locator#locator-click>
1182 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1183 pub async fn click(
1184 &self,
1185 options: impl Into<Option<crate::protocol::ClickOptions>>,
1186 ) -> Result<()> {
1187 let options = options.into();
1188 self.frame
1189 .locator_click(&self.selector, Some(self.with_timeout(options)))
1190 .await
1191 .map_err(|e| self.wrap_error_with_selector(e))
1192 }
1193
1194 /// Ensures an options struct has the page's default timeout when none is explicitly set.
1195 fn with_timeout<T: HasTimeout + Default>(&self, options: Option<T>) -> T {
1196 let mut opts = options.unwrap_or_default();
1197 if opts.timeout_ref().is_none() {
1198 *opts.timeout_ref_mut() = Some(self.page.default_timeout_ms());
1199 }
1200 opts
1201 }
1202
1203 /// Wraps an error with selector context for better error messages.
1204 fn wrap_error_with_selector(&self, error: crate::error::Error) -> crate::error::Error {
1205 match &error {
1206 crate::error::Error::ProtocolError(msg) => {
1207 // Add selector context to protocol errors (timeouts, etc.)
1208 crate::error::Error::ProtocolError(format!("{} [selector: {}]", msg, self.selector))
1209 }
1210 crate::error::Error::Timeout(msg) => {
1211 crate::error::Error::Timeout(format!("{} [selector: {}]", msg, self.selector))
1212 }
1213 _ => error, // Other errors pass through unchanged
1214 }
1215 }
1216
1217 /// Double clicks the element.
1218 ///
1219 /// See: <https://playwright.dev/docs/api/class-locator#locator-dblclick>
1220 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1221 pub async fn dblclick(
1222 &self,
1223 options: impl Into<Option<crate::protocol::ClickOptions>>,
1224 ) -> Result<()> {
1225 let options = options.into();
1226 self.frame
1227 .locator_dblclick(&self.selector, Some(self.with_timeout(options)))
1228 .await
1229 .map_err(|e| self.wrap_error_with_selector(e))
1230 }
1231
1232 /// Fills the element with text.
1233 ///
1234 /// See: <https://playwright.dev/docs/api/class-locator#locator-fill>
1235 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1236 pub async fn fill(
1237 &self,
1238 text: &str,
1239 options: impl Into<Option<crate::protocol::FillOptions>>,
1240 ) -> Result<()> {
1241 let options = options.into();
1242 self.frame
1243 .locator_fill(&self.selector, text, Some(self.with_timeout(options)))
1244 .await
1245 .map_err(|e| self.wrap_error_with_selector(e))
1246 }
1247
1248 /// Clears the element's value.
1249 ///
1250 /// See: <https://playwright.dev/docs/api/class-locator#locator-clear>
1251 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1252 pub async fn clear(
1253 &self,
1254 options: impl Into<Option<crate::protocol::FillOptions>>,
1255 ) -> Result<()> {
1256 let options = options.into();
1257 self.frame
1258 .locator_clear(&self.selector, Some(self.with_timeout(options)))
1259 .await
1260 .map_err(|e| self.wrap_error_with_selector(e))
1261 }
1262
1263 /// Presses a key on the element.
1264 ///
1265 /// See: <https://playwright.dev/docs/api/class-locator#locator-press>
1266 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1267 pub async fn press(
1268 &self,
1269 key: &str,
1270 options: impl Into<Option<crate::protocol::PressOptions>>,
1271 ) -> Result<()> {
1272 let options = options.into();
1273 self.frame
1274 .locator_press(&self.selector, key, Some(self.with_timeout(options)))
1275 .await
1276 .map_err(|e| self.wrap_error_with_selector(e))
1277 }
1278
1279 /// Sets focus on the element.
1280 ///
1281 /// Calls the element's `focus()` method. Used to move keyboard focus to a
1282 /// specific element for subsequent keyboard interactions.
1283 ///
1284 /// See: <https://playwright.dev/docs/api/class-locator#locator-focus>
1285 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1286 pub async fn focus(&self) -> Result<()> {
1287 self.frame
1288 .locator_focus(&self.selector)
1289 .await
1290 .map_err(|e| self.wrap_error_with_selector(e))
1291 }
1292
1293 /// Removes focus from the element.
1294 ///
1295 /// Calls the element's `blur()` method. Moves keyboard focus away from the element.
1296 ///
1297 /// See: <https://playwright.dev/docs/api/class-locator#locator-blur>
1298 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1299 pub async fn blur(&self) -> Result<()> {
1300 self.frame
1301 .locator_blur(&self.selector)
1302 .await
1303 .map_err(|e| self.wrap_error_with_selector(e))
1304 }
1305
1306 /// Types `text` into the element character by character, as though it was typed
1307 /// on a real keyboard.
1308 ///
1309 /// Use this method when you need to simulate keystrokes with individual key events
1310 /// (e.g., for autocomplete widgets). For simply setting a field value, prefer
1311 /// [`Locator::fill()`].
1312 ///
1313 /// # Arguments
1314 ///
1315 /// * `text` - Text to type into the element
1316 /// * `options` - Optional [`PressSequentiallyOptions`](crate::protocol::PressSequentiallyOptions) (e.g., `delay` between key presses)
1317 ///
1318 /// See: <https://playwright.dev/docs/api/class-locator#locator-press-sequentially>
1319 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1320 pub async fn press_sequentially(
1321 &self,
1322 text: &str,
1323 options: impl Into<Option<crate::protocol::PressSequentiallyOptions>>,
1324 ) -> Result<()> {
1325 let options = options.into();
1326 self.frame
1327 .locator_press_sequentially(&self.selector, text, options)
1328 .await
1329 .map_err(|e| self.wrap_error_with_selector(e))
1330 }
1331
1332 /// Returns the `innerText` values of all elements matching this locator.
1333 ///
1334 /// Unlike [`Locator::inner_text()`] (which uses strict mode and requires exactly one match),
1335 /// `all_inner_texts()` returns text from all matching elements.
1336 ///
1337 /// See: <https://playwright.dev/docs/api/class-locator#locator-all-inner-texts>
1338 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1339 pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
1340 self.frame
1341 .locator_all_inner_texts(&self.selector)
1342 .await
1343 .map_err(|e| self.wrap_error_with_selector(e))
1344 }
1345
1346 /// Returns the `textContent` values of all elements matching this locator.
1347 ///
1348 /// Unlike [`Locator::text_content()`] (which uses strict mode and requires exactly one match),
1349 /// `all_text_contents()` returns text from all matching elements.
1350 ///
1351 /// See: <https://playwright.dev/docs/api/class-locator#locator-all-text-contents>
1352 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1353 pub async fn all_text_contents(&self) -> Result<Vec<String>> {
1354 self.frame
1355 .locator_all_text_contents(&self.selector)
1356 .await
1357 .map_err(|e| self.wrap_error_with_selector(e))
1358 }
1359
1360 /// Ensures the checkbox or radio button is checked.
1361 ///
1362 /// This method is idempotent - if already checked, does nothing.
1363 ///
1364 /// See: <https://playwright.dev/docs/api/class-locator#locator-check>
1365 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1366 pub async fn check(
1367 &self,
1368 options: impl Into<Option<crate::protocol::CheckOptions>>,
1369 ) -> Result<()> {
1370 let options = options.into();
1371 self.frame
1372 .locator_check(&self.selector, Some(self.with_timeout(options)))
1373 .await
1374 .map_err(|e| self.wrap_error_with_selector(e))
1375 }
1376
1377 /// Ensures the checkbox is unchecked.
1378 ///
1379 /// This method is idempotent - if already unchecked, does nothing.
1380 ///
1381 /// See: <https://playwright.dev/docs/api/class-locator#locator-uncheck>
1382 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1383 pub async fn uncheck(
1384 &self,
1385 options: impl Into<Option<crate::protocol::CheckOptions>>,
1386 ) -> Result<()> {
1387 let options = options.into();
1388 self.frame
1389 .locator_uncheck(&self.selector, Some(self.with_timeout(options)))
1390 .await
1391 .map_err(|e| self.wrap_error_with_selector(e))
1392 }
1393
1394 /// Sets the checkbox or radio button to the specified checked state.
1395 ///
1396 /// This is a convenience method that calls `check()` if `checked` is true,
1397 /// or `uncheck()` if `checked` is false.
1398 ///
1399 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-checked>
1400 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1401 pub async fn set_checked(
1402 &self,
1403 checked: bool,
1404 options: impl Into<Option<crate::protocol::CheckOptions>>,
1405 ) -> Result<()> {
1406 let options = options.into();
1407 if checked {
1408 self.check(options).await
1409 } else {
1410 self.uncheck(options).await
1411 }
1412 }
1413
1414 /// Hovers the mouse over the element.
1415 ///
1416 /// See: <https://playwright.dev/docs/api/class-locator#locator-hover>
1417 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1418 pub async fn hover(
1419 &self,
1420 options: impl Into<Option<crate::protocol::HoverOptions>>,
1421 ) -> Result<()> {
1422 let options = options.into();
1423 self.frame
1424 .locator_hover(&self.selector, Some(self.with_timeout(options)))
1425 .await
1426 .map_err(|e| self.wrap_error_with_selector(e))
1427 }
1428
1429 /// Returns the value of the input, textarea, or select element.
1430 ///
1431 /// See: <https://playwright.dev/docs/api/class-locator#locator-input-value>
1432 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1433 pub async fn input_value(&self, _options: impl Into<Option<()>>) -> Result<String> {
1434 self.frame
1435 .locator_input_value(&self.selector)
1436 .await
1437 .map_err(|e| self.wrap_error_with_selector(e))
1438 }
1439
1440 /// Selects one or more options in a select element.
1441 ///
1442 /// Returns an array of option values that have been successfully selected.
1443 ///
1444 /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1445 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1446 pub async fn select_option(
1447 &self,
1448 value: impl Into<crate::protocol::SelectOption>,
1449 options: impl Into<Option<crate::protocol::SelectOptions>>,
1450 ) -> Result<Vec<String>> {
1451 let options = options.into();
1452 self.frame
1453 .locator_select_option(
1454 &self.selector,
1455 value.into(),
1456 Some(self.with_timeout(options)),
1457 )
1458 .await
1459 .map_err(|e| self.wrap_error_with_selector(e))
1460 }
1461
1462 /// Selects multiple options in a select element.
1463 ///
1464 /// Returns an array of option values that have been successfully selected.
1465 ///
1466 /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1467 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1468 pub async fn select_option_multiple(
1469 &self,
1470 values: &[impl Into<crate::protocol::SelectOption> + Clone],
1471 options: impl Into<Option<crate::protocol::SelectOptions>>,
1472 ) -> Result<Vec<String>> {
1473 let options = options.into();
1474 let select_options: Vec<crate::protocol::SelectOption> =
1475 values.iter().map(|v| v.clone().into()).collect();
1476 self.frame
1477 .locator_select_option_multiple(
1478 &self.selector,
1479 select_options,
1480 Some(self.with_timeout(options)),
1481 )
1482 .await
1483 .map_err(|e| self.wrap_error_with_selector(e))
1484 }
1485
1486 /// Sets the file path(s) to upload to a file input element.
1487 ///
1488 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1489 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1490 pub async fn set_input_files(
1491 &self,
1492 file: &std::path::PathBuf,
1493 _options: impl Into<Option<()>>,
1494 ) -> Result<()> {
1495 self.frame
1496 .locator_set_input_files(&self.selector, file)
1497 .await
1498 .map_err(|e| self.wrap_error_with_selector(e))
1499 }
1500
1501 /// Sets multiple file paths to upload to a file input element.
1502 ///
1503 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1504 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1505 pub async fn set_input_files_multiple(
1506 &self,
1507 files: &[&std::path::PathBuf],
1508 _options: impl Into<Option<()>>,
1509 ) -> Result<()> {
1510 self.frame
1511 .locator_set_input_files_multiple(&self.selector, files)
1512 .await
1513 .map_err(|e| self.wrap_error_with_selector(e))
1514 }
1515
1516 /// Sets a file to upload using FilePayload (explicit name, mimeType, buffer).
1517 ///
1518 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1519 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1520 pub async fn set_input_files_payload(
1521 &self,
1522 file: crate::protocol::FilePayload,
1523 _options: impl Into<Option<()>>,
1524 ) -> Result<()> {
1525 self.frame
1526 .locator_set_input_files_payload(&self.selector, file)
1527 .await
1528 .map_err(|e| self.wrap_error_with_selector(e))
1529 }
1530
1531 /// Sets multiple files to upload using FilePayload.
1532 ///
1533 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1534 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1535 pub async fn set_input_files_payload_multiple(
1536 &self,
1537 files: &[crate::protocol::FilePayload],
1538 _options: impl Into<Option<()>>,
1539 ) -> Result<()> {
1540 self.frame
1541 .locator_set_input_files_payload_multiple(&self.selector, files)
1542 .await
1543 .map_err(|e| self.wrap_error_with_selector(e))
1544 }
1545
1546 /// Dispatches a DOM event on the element.
1547 ///
1548 /// Unlike clicking or typing, `dispatch_event` directly sends the event without
1549 /// performing any actionability checks. It still waits for the element to be present
1550 /// in the DOM.
1551 ///
1552 /// # Arguments
1553 ///
1554 /// * `type_` - The event type to dispatch, e.g. `"click"`, `"focus"`, `"myevent"`.
1555 /// * `event_init` - Optional event initializer properties (e.g. `{"detail": "value"}` for
1556 /// `CustomEvent`). Corresponds to the second argument of `new Event(type, init)`.
1557 ///
1558 /// # Errors
1559 ///
1560 /// Returns an error if:
1561 /// - The element is not found within the timeout
1562 /// - The protocol call fails
1563 ///
1564 /// See: <https://playwright.dev/docs/api/class-locator#locator-dispatch-event>
1565 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1566 pub async fn dispatch_event(
1567 &self,
1568 type_: &str,
1569 event_init: Option<serde_json::Value>,
1570 ) -> Result<()> {
1571 self.frame
1572 .locator_dispatch_event(&self.selector, type_, event_init)
1573 .await
1574 .map_err(|e| self.wrap_error_with_selector(e))
1575 }
1576
1577 /// Returns the bounding box of the element, or `None` if the element is not visible.
1578 ///
1579 /// The bounding box is in pixels, relative to the top-left corner of the page.
1580 /// Returns `None` when the element has `display: none` or is otherwise not part of
1581 /// the layout.
1582 ///
1583 /// # Errors
1584 ///
1585 /// Returns an error if:
1586 /// - The element is not found within the timeout
1587 /// - The protocol call fails
1588 ///
1589 /// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
1590 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1591 pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
1592 self.frame
1593 .locator_bounding_box(&self.selector)
1594 .await
1595 .map_err(|e| self.wrap_error_with_selector(e))
1596 }
1597
1598 /// Scrolls the element into view if it is not already visible in the viewport.
1599 ///
1600 /// This is an alias for calling `element.scrollIntoView()` in the browser.
1601 ///
1602 /// # Errors
1603 ///
1604 /// Returns an error if:
1605 /// - The element is not found within the timeout
1606 /// - The protocol call fails
1607 ///
1608 /// See: <https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed>
1609 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1610 pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
1611 self.frame
1612 .locator_scroll_into_view_if_needed(&self.selector)
1613 .await
1614 .map_err(|e| self.wrap_error_with_selector(e))
1615 }
1616
1617 /// Takes a screenshot of the element and returns the image bytes.
1618 ///
1619 /// This method uses strict mode - it will fail if the selector matches multiple elements.
1620 /// Use `first()`, `last()`, or `nth()` to refine the selector to a single element.
1621 ///
1622 /// See: <https://playwright.dev/docs/api/class-locator#locator-screenshot>
1623 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector, bytes_len = tracing::field::Empty))]
1624 pub async fn screenshot(
1625 &self,
1626 options: impl Into<Option<crate::protocol::ScreenshotOptions>>,
1627 ) -> Result<Vec<u8>> {
1628 let options = options.into();
1629 // Query for the element using strict mode (should return exactly one)
1630 let element = self
1631 .frame
1632 .query_selector(&self.selector)
1633 .await
1634 .map_err(|e| self.wrap_error_with_selector(e))?
1635 .ok_or_else(|| {
1636 crate::error::Error::ElementNotFound(format!(
1637 "Element not found: {}",
1638 self.selector
1639 ))
1640 })?;
1641
1642 // Delegate to ElementHandle.screenshot() with default timeout injected
1643 let bytes = element
1644 .screenshot(Some(self.with_timeout(options)))
1645 .await
1646 .map_err(|e| self.wrap_error_with_selector(e))?;
1647 tracing::Span::current().record("bytes_len", bytes.len());
1648 Ok(bytes)
1649 }
1650
1651 /// Performs a touch-tap on the element.
1652 ///
1653 /// This method dispatches a `touchstart` and `touchend` event on the element.
1654 /// For touch support to work, the browser context must be created with
1655 /// `has_touch: true`.
1656 ///
1657 /// # Arguments
1658 ///
1659 /// * `options` - Optional [`TapOptions`](crate::protocol::TapOptions) (force, modifiers, position, timeout, trial)
1660 ///
1661 /// # Errors
1662 ///
1663 /// Returns an error if:
1664 /// - The element is not found within the timeout
1665 /// - Actionability checks fail (unless `force: true`)
1666 /// - The browser context was not created with `has_touch: true`
1667 ///
1668 /// See: <https://playwright.dev/docs/api/class-locator#locator-tap>
1669 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1670 pub async fn tap(&self, options: impl Into<Option<crate::protocol::TapOptions>>) -> Result<()> {
1671 let options = options.into();
1672 self.frame
1673 .locator_tap(&self.selector, Some(self.with_timeout(options)))
1674 .await
1675 .map_err(|e| self.wrap_error_with_selector(e))
1676 }
1677
1678 /// Drags this element to the `target` element.
1679 ///
1680 /// Both this locator and `target` must resolve to elements in the same frame.
1681 /// Playwright performs a series of mouse events (move, press, move to target, release)
1682 /// to simulate the drag, so it drives real pointer-event chains, including
1683 /// UIs that call `setPointerCapture` in their `pointerdown` handler.
1684 ///
1685 /// `source_position` and `target_position` are offsets from the respective
1686 /// element's top-left corner. Setting `target_position` with a containing
1687 /// element (a canvas or stage) as `target` turns this into a drag to a
1688 /// coordinate rather than onto an element, which also covers "drag by a
1689 /// delta": compute the drop point from the source's position within the
1690 /// container. Prefer this over a held-button
1691 /// [`Mouse::move_to`](crate::protocol::Mouse::move_to) sequence, which can
1692 /// hang on headless Linux (see the note on that method).
1693 ///
1694 /// # Arguments
1695 ///
1696 /// * `target` - The locator of the element to drag onto
1697 /// * `options` - Optional [`DragToOptions`](crate::protocol::DragToOptions) (force, no_wait_after, timeout, trial,
1698 /// source_position, target_position)
1699 ///
1700 /// # Example
1701 ///
1702 /// ```no_run
1703 /// # use playwright_rs::{Playwright, DragToOptions, Position};
1704 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1705 /// # let pw = Playwright::launch().await?;
1706 /// # let page = pw.chromium().launch().await?.new_page().await?;
1707 /// let handle = page.locator(".crop-handle");
1708 /// let stage = page.locator("#stage");
1709 ///
1710 /// // Drop the handle 180px right, 120px down from the stage's top-left corner.
1711 /// let opts = DragToOptions::builder()
1712 /// .target_position(Position { x: 180.0, y: 120.0 })
1713 /// .build();
1714 /// handle.drag_to(&stage, Some(opts)).await?;
1715 /// # Ok(())
1716 /// # }
1717 /// ```
1718 ///
1719 /// # Errors
1720 ///
1721 /// Returns an error if:
1722 /// - Either element is not found within the timeout
1723 /// - Actionability checks fail (unless `force: true`)
1724 /// - The protocol call fails
1725 ///
1726 /// See: <https://playwright.dev/docs/api/class-locator#locator-drag-to>
1727 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1728 pub async fn drag_to(
1729 &self,
1730 target: &Locator,
1731 options: impl Into<Option<crate::protocol::DragToOptions>>,
1732 ) -> Result<()> {
1733 let options = options.into();
1734 self.frame
1735 .locator_drag_to(
1736 &self.selector,
1737 &target.selector,
1738 Some(self.with_timeout(options)),
1739 )
1740 .await
1741 .map_err(|e| self.wrap_error_with_selector(e))
1742 }
1743
1744 /// Drops files and/or data onto this element (external drag-and-drop).
1745 ///
1746 /// Simulates dragging files or data from outside the page onto the element,
1747 /// such as an upload drop zone, by dispatching `dragenter`/`dragover`/`drop`
1748 /// with a synthetic `DataTransfer`. Set `files` and/or `data` on the
1749 /// [`DropOptions`](crate::protocol::DropOptions). This is distinct from
1750 /// [`drag_to`](Self::drag_to), which drags one element onto another within
1751 /// the page.
1752 ///
1753 /// # Arguments
1754 ///
1755 /// * `options` - [`DropOptions`](crate::protocol::DropOptions) carrying the
1756 /// files / data to drop, plus optional `position` and `timeout`.
1757 ///
1758 /// # Errors
1759 ///
1760 /// Returns an error if:
1761 /// - The element is not found within the timeout
1762 /// - Actionability checks fail
1763 /// - The protocol call fails (e.g. neither files nor data were provided)
1764 ///
1765 /// See: <https://playwright.dev/docs/api/class-locator#locator-drop>
1766 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1767 pub async fn drop(&self, options: crate::protocol::DropOptions) -> Result<()> {
1768 self.frame
1769 .locator_drop(&self.selector, self.with_timeout(Some(options)))
1770 .await
1771 .map_err(|e| self.wrap_error_with_selector(e))
1772 }
1773
1774 /// Waits until the element satisfies the given state condition.
1775 ///
1776 /// If no state is specified, waits for the element to be `visible` (the default).
1777 ///
1778 /// This method is useful for waiting for lazy-rendered elements or elements that
1779 /// appear/disappear based on user interaction or async data loading.
1780 ///
1781 /// # Arguments
1782 ///
1783 /// * `options` - Optional [`WaitForOptions`](crate::protocol::WaitForOptions) specifying the `state` to wait for
1784 /// (`Visible`, `Hidden`, `Attached`, or `Detached`) and a `timeout` in milliseconds.
1785 ///
1786 /// # Errors
1787 ///
1788 /// Returns an error if the element does not satisfy the expected state within the timeout.
1789 ///
1790 /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for>
1791 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1792 pub async fn wait_for(
1793 &self,
1794 options: impl Into<Option<crate::protocol::WaitForOptions>>,
1795 ) -> Result<()> {
1796 let options = options.into();
1797 self.frame
1798 .locator_wait_for(&self.selector, Some(self.with_timeout(options)))
1799 .await
1800 .map_err(|e| self.wrap_error_with_selector(e))
1801 }
1802
1803 /// Evaluates a JavaScript expression in the scope of the matched element.
1804 ///
1805 /// The element is passed as the first argument to the expression. The expression
1806 /// can be any JavaScript function or expression that returns a JSON-serializable value.
1807 ///
1808 /// # Arguments
1809 ///
1810 /// * `expression` - JavaScript expression or function, e.g. `"(el) => el.textContent"`
1811 /// * `arg` - Optional argument passed as the second argument to the function
1812 ///
1813 /// # Errors
1814 ///
1815 /// Returns an error if:
1816 /// - The element is not found within the timeout
1817 /// - The JavaScript expression throws an error
1818 /// - The return value is not JSON-serializable
1819 ///
1820 /// # Example
1821 ///
1822 /// ```no_run
1823 /// use playwright_rs::Playwright;
1824 ///
1825 /// # #[tokio::main]
1826 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1827 /// let playwright = Playwright::launch().await?;
1828 /// let browser = playwright.chromium().launch().await?;
1829 /// let page = browser.new_page().await?;
1830 /// let _ = page.goto("data:text/html,<h1>Hello</h1>", None).await;
1831 ///
1832 /// let heading = page.locator("h1");
1833 /// let text: String = heading.evaluate("(el) => el.textContent", None::<()>).await?;
1834 /// assert_eq!(text, "Hello");
1835 ///
1836 /// // With an argument
1837 /// let result: String = heading
1838 /// .evaluate("(el, suffix) => el.textContent + suffix", Some("!"))
1839 /// .await?;
1840 /// assert_eq!(result, "Hello!");
1841 /// # browser.close().await?;
1842 /// # Ok(())
1843 /// # }
1844 /// ```
1845 ///
1846 /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate>
1847 #[tracing::instrument(level = "info", skip_all, fields(selector = %self.selector))]
1848 pub async fn evaluate<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1849 where
1850 R: serde::de::DeserializeOwned,
1851 T: serde::Serialize,
1852 {
1853 let raw = self
1854 .frame
1855 .locator_evaluate(&self.selector, expression, arg)
1856 .await
1857 .map_err(|e| self.wrap_error_with_selector(e))?;
1858 serde_json::from_value(raw).map_err(|e| {
1859 crate::error::Error::ProtocolError(format!(
1860 "evaluate result deserialization failed: {}",
1861 e
1862 ))
1863 })
1864 }
1865
1866 /// Evaluates a JavaScript expression in the scope of all elements matching this locator.
1867 ///
1868 /// The array of all matched elements is passed as the first argument to the expression.
1869 /// Unlike [`evaluate()`](Self::evaluate), this does not use strict mode — all matching
1870 /// elements are collected and passed as an array.
1871 ///
1872 /// # Arguments
1873 ///
1874 /// * `expression` - JavaScript function that receives an array of elements
1875 /// * `arg` - Optional argument passed as the second argument to the function
1876 ///
1877 /// # Errors
1878 ///
1879 /// Returns an error if:
1880 /// - The JavaScript expression throws an error
1881 /// - The return value is not JSON-serializable
1882 ///
1883 /// # Example
1884 ///
1885 /// ```no_run
1886 /// use playwright_rs::Playwright;
1887 ///
1888 /// # #[tokio::main]
1889 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1890 /// let playwright = Playwright::launch().await?;
1891 /// let browser = playwright.chromium().launch().await?;
1892 /// let page = browser.new_page().await?;
1893 /// let _ = page.goto(
1894 /// "data:text/html,<li class='item'>A</li><li class='item'>B</li>",
1895 /// None
1896 /// ).await;
1897 ///
1898 /// let items = page.locator(".item");
1899 /// let texts: Vec<String> = items
1900 /// .evaluate_all("(elements) => elements.map(e => e.textContent)", None::<()>)
1901 /// .await?;
1902 /// assert_eq!(texts, vec!["A", "B"]);
1903 /// # browser.close().await?;
1904 /// # Ok(())
1905 /// # }
1906 /// ```
1907 ///
1908 /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate-all>
1909 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1910 pub async fn evaluate_all<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1911 where
1912 R: serde::de::DeserializeOwned,
1913 T: serde::Serialize,
1914 {
1915 let raw = self
1916 .frame
1917 .locator_evaluate_all(&self.selector, expression, arg)
1918 .await
1919 .map_err(|e| self.wrap_error_with_selector(e))?;
1920 serde_json::from_value(raw).map_err(|e| {
1921 crate::error::Error::ProtocolError(format!(
1922 "evaluate_all result deserialization failed: {}",
1923 e
1924 ))
1925 })
1926 }
1927
1928 /// Returns the ARIA accessibility tree snapshot as a YAML string.
1929 ///
1930 /// The snapshot describes the accessible roles, names, and properties of the matched
1931 /// element and its descendants. This is useful for writing stable accessibility assertions
1932 /// that are independent of CSS classes or DOM structure.
1933 ///
1934 /// # Errors
1935 ///
1936 /// Returns an error if:
1937 /// - The element is not found within the timeout
1938 /// - The protocol call fails
1939 ///
1940 /// See: <https://playwright.dev/docs/api/class-locator#locator-aria-snapshot>
1941 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector, mode = tracing::field::Empty))]
1942 pub async fn aria_snapshot(
1943 &self,
1944 options: impl Into<Option<crate::protocol::AriaSnapshotOptions>>,
1945 ) -> Result<String> {
1946 let options = options.into();
1947 self.frame
1948 .locator_aria_snapshot(&self.selector, options.as_ref())
1949 .await
1950 .map_err(|e| self.wrap_error_with_selector(e))
1951 }
1952
1953 /// Returns a new locator whose selector has been resolved to a
1954 /// best-practices canonical form — preferring test-ids, then ARIA
1955 /// roles, then accessible text. The resolved locator points at the
1956 /// same element(s) as `self` but uses a more robust selector that
1957 /// is less coupled to CSS classes or DOM structure. Useful as a
1958 /// building block for codegen helpers that want the "most stable
1959 /// selector for this element" primitive.
1960 ///
1961 /// See the module-level example for usage.
1962 ///
1963 /// # Errors
1964 ///
1965 /// Returns an error if:
1966 /// - No element matches the original selector
1967 /// - The protocol call fails
1968 ///
1969 /// See: <https://playwright.dev/docs/api/class-locator#locator-normalize>
1970 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
1971 pub async fn normalize(&self) -> Result<Locator> {
1972 let resolved = self
1973 .frame
1974 .frame_resolve_selector(&self.selector)
1975 .await
1976 .map_err(|e| self.wrap_error_with_selector(e))?;
1977 Ok(Locator {
1978 frame: Arc::clone(&self.frame),
1979 selector: resolved,
1980 page: self.page.clone(),
1981 })
1982 }
1983
1984 /// Returns a new Locator with an attached description for traces and error messages.
1985 ///
1986 /// The description does not affect element matching — it is purely informational,
1987 /// appearing in trace viewer labels and error messages to make them more readable.
1988 ///
1989 /// Appends `>> internal:describe="description"` to the selector, matching
1990 /// playwright-python's behavior exactly.
1991 ///
1992 /// See: <https://playwright.dev/docs/api/class-locator#locator-describe>
1993 pub fn describe(&self, description: &str) -> Locator {
1994 let escaped =
1995 serde_json::to_string(description).unwrap_or_else(|_| format!("\"{}\"", description));
1996 Locator::new(
1997 Arc::clone(&self.frame),
1998 format!("{} >> internal:describe={}", self.selector, escaped),
1999 self.page.clone(),
2000 )
2001 }
2002
2003 /// Highlights the matched element in the browser for visual debugging.
2004 ///
2005 /// Draws a colored overlay over the element for a short period. This is a
2006 /// debugging tool and has no effect on test assertions or element state.
2007 ///
2008 /// # Errors
2009 ///
2010 /// Returns an error if:
2011 /// - The element is not found within the timeout
2012 /// - The protocol call fails
2013 ///
2014 /// See: <https://playwright.dev/docs/api/class-locator#locator-highlight>
2015 #[tracing::instrument(level = "debug", skip_all, fields(selector = %self.selector))]
2016 pub async fn highlight(&self, options: impl Into<Option<HighlightOptions>>) -> Result<()> {
2017 let options = options.into();
2018 let style = options.and_then(|o| o.style);
2019 self.frame
2020 .locator_highlight(&self.selector, style.as_deref())
2021 .await
2022 .map_err(|e| self.wrap_error_with_selector(e))
2023 }
2024
2025 /// Returns a [`FrameLocator`](crate::protocol::FrameLocator) for the content of an
2026 /// `<iframe>` element matched by this locator.
2027 ///
2028 /// This is a client-side operation — it creates a `FrameLocator` scoped to the matched
2029 /// iframe element, allowing you to interact with elements inside the iframe using the
2030 /// standard `FrameLocator` API.
2031 ///
2032 /// Equivalent to `page.frame_locator(selector)`, but starting from an existing `Locator`.
2033 ///
2034 /// See: <https://playwright.dev/docs/api/class-locator#locator-content-frame>
2035 pub fn content_frame(&self) -> crate::protocol::FrameLocator {
2036 crate::protocol::FrameLocator::new(
2037 Arc::clone(&self.frame),
2038 self.selector.clone(),
2039 self.page.clone(),
2040 )
2041 }
2042}
2043
2044impl std::fmt::Debug for Locator {
2045 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2046 f.debug_struct("Locator")
2047 .field("selector", &self.selector)
2048 .finish()
2049 }
2050}
2051
2052#[cfg(test)]
2053mod tests {
2054 use super::*;
2055
2056 #[test]
2057 fn test_escape_for_selector_case_insensitive() {
2058 assert_eq!(escape_for_selector("hello", false), "\"hello\"i");
2059 }
2060
2061 #[test]
2062 fn test_escape_for_selector_exact() {
2063 assert_eq!(escape_for_selector("hello", true), "\"hello\"s");
2064 }
2065
2066 #[test]
2067 fn test_escape_for_selector_with_quotes() {
2068 assert_eq!(
2069 escape_for_selector("say \"hi\"", false),
2070 "\"say \\\"hi\\\"\"i"
2071 );
2072 }
2073
2074 #[test]
2075 fn test_get_by_text_selector_case_insensitive() {
2076 assert_eq!(
2077 get_by_text_selector("Click me", false),
2078 "internal:text=\"Click me\"i"
2079 );
2080 }
2081
2082 #[test]
2083 fn test_get_by_text_selector_exact() {
2084 assert_eq!(
2085 get_by_text_selector("Click me", true),
2086 "internal:text=\"Click me\"s"
2087 );
2088 }
2089
2090 #[test]
2091 fn test_get_by_label_selector() {
2092 assert_eq!(
2093 get_by_label_selector("Email", false),
2094 "internal:label=\"Email\"i"
2095 );
2096 }
2097
2098 #[test]
2099 fn test_get_by_placeholder_selector() {
2100 assert_eq!(
2101 get_by_placeholder_selector("Enter name", false),
2102 "internal:attr=[placeholder=\"Enter name\"i]"
2103 );
2104 }
2105
2106 #[test]
2107 fn test_get_by_alt_text_selector() {
2108 assert_eq!(
2109 get_by_alt_text_selector("Logo", true),
2110 "internal:attr=[alt=\"Logo\"s]"
2111 );
2112 }
2113
2114 #[test]
2115 fn test_get_by_title_selector() {
2116 assert_eq!(
2117 get_by_title_selector("Help", false),
2118 "internal:attr=[title=\"Help\"i]"
2119 );
2120 }
2121
2122 #[test]
2123 fn test_get_by_test_id_selector() {
2124 assert_eq!(
2125 get_by_test_id_selector("submit-btn"),
2126 "internal:testid=[data-testid=\"submit-btn\"s]"
2127 );
2128 }
2129
2130 #[test]
2131 fn test_escape_for_attribute_selector_case_insensitive() {
2132 assert_eq!(
2133 escape_for_attribute_selector("Submit", false),
2134 "\"Submit\"i"
2135 );
2136 }
2137
2138 #[test]
2139 fn test_escape_for_attribute_selector_exact() {
2140 assert_eq!(escape_for_attribute_selector("Submit", true), "\"Submit\"s");
2141 }
2142
2143 #[test]
2144 fn test_escape_for_attribute_selector_escapes_quotes() {
2145 assert_eq!(
2146 escape_for_attribute_selector("Say \"hello\"", false),
2147 "\"Say \\\"hello\\\"\"i"
2148 );
2149 }
2150
2151 #[test]
2152 fn test_escape_for_attribute_selector_escapes_backslashes() {
2153 assert_eq!(
2154 escape_for_attribute_selector("path\\to", true),
2155 "\"path\\\\to\"s"
2156 );
2157 }
2158
2159 #[test]
2160 fn test_get_by_role_selector_role_only() {
2161 assert_eq!(
2162 get_by_role_selector(AriaRole::Button, None),
2163 "internal:role=button"
2164 );
2165 }
2166
2167 #[test]
2168 fn test_get_by_role_selector_with_name() {
2169 let opts = GetByRoleOptions::default().name("Submit");
2170 assert_eq!(
2171 get_by_role_selector(AriaRole::Button, Some(opts)),
2172 "internal:role=button[name=\"Submit\"i]"
2173 );
2174 }
2175
2176 #[test]
2177 fn test_filter_options_setters() {
2178 let opts = FilterOptions::default().has_text("a").has_not_text("b");
2179 assert_eq!(opts.has_text.as_deref(), Some("a"));
2180 assert_eq!(opts.has_not_text.as_deref(), Some("b"));
2181 }
2182
2183 #[test]
2184 fn test_get_by_role_selector_with_description() {
2185 let opts = GetByRoleOptions::default().description("Close dialog");
2186 assert_eq!(
2187 get_by_role_selector(AriaRole::Button, Some(opts)),
2188 "internal:role=button[description=\"Close dialog\"i]"
2189 );
2190 let exact = GetByRoleOptions::default().description("Close").exact(true);
2191 assert_eq!(
2192 get_by_role_selector(AriaRole::Button, Some(exact)),
2193 "internal:role=button[description=\"Close\"s]"
2194 );
2195 }
2196
2197 #[test]
2198 fn test_get_by_role_selector_with_name_exact() {
2199 let opts = GetByRoleOptions::default().name("Submit").exact(true);
2200 assert_eq!(
2201 get_by_role_selector(AriaRole::Button, Some(opts)),
2202 "internal:role=button[name=\"Submit\"s]"
2203 );
2204 }
2205
2206 #[test]
2207 fn test_get_by_role_selector_with_checked() {
2208 let opts = GetByRoleOptions::default().checked(true);
2209 assert_eq!(
2210 get_by_role_selector(AriaRole::Checkbox, Some(opts)),
2211 "internal:role=checkbox[checked=true]"
2212 );
2213 }
2214
2215 #[test]
2216 fn test_get_by_role_selector_with_level() {
2217 let opts = GetByRoleOptions::default().level(2);
2218 assert_eq!(
2219 get_by_role_selector(AriaRole::Heading, Some(opts)),
2220 "internal:role=heading[level=2]"
2221 );
2222 }
2223
2224 #[test]
2225 fn test_get_by_role_selector_with_disabled() {
2226 let opts = GetByRoleOptions::default().disabled(true);
2227 assert_eq!(
2228 get_by_role_selector(AriaRole::Button, Some(opts)),
2229 "internal:role=button[disabled=true]"
2230 );
2231 }
2232
2233 #[test]
2234 fn test_get_by_role_selector_with_selected() {
2235 let opts = GetByRoleOptions::default().selected(true);
2236 assert_eq!(
2237 get_by_role_selector(AriaRole::Option, Some(opts)),
2238 "internal:role=option[selected=true]"
2239 );
2240 }
2241
2242 #[test]
2243 fn test_get_by_role_selector_with_expanded() {
2244 let opts = GetByRoleOptions::default().expanded(true);
2245 assert_eq!(
2246 get_by_role_selector(AriaRole::Button, Some(opts)),
2247 "internal:role=button[expanded=true]"
2248 );
2249 }
2250
2251 #[test]
2252 fn test_get_by_role_selector_include_hidden() {
2253 let opts = GetByRoleOptions::default().include_hidden(true);
2254 assert_eq!(
2255 get_by_role_selector(AriaRole::Button, Some(opts)),
2256 "internal:role=button[include-hidden=true]"
2257 );
2258 }
2259
2260 #[test]
2261 fn test_get_by_role_selector_property_order() {
2262 // All properties: checked, disabled, selected, expanded, include-hidden, level, name, pressed
2263 let opts = GetByRoleOptions::default()
2264 .pressed(true)
2265 .name("OK")
2266 .checked(false)
2267 .disabled(true);
2268 assert_eq!(
2269 get_by_role_selector(AriaRole::Button, Some(opts)),
2270 "internal:role=button[checked=false][disabled=true][name=\"OK\"i][pressed=true]"
2271 );
2272 }
2273
2274 #[test]
2275 fn test_get_by_role_selector_name_with_special_chars() {
2276 let opts = GetByRoleOptions::default()
2277 .name("Click \"here\" now")
2278 .exact(true);
2279 assert_eq!(
2280 get_by_role_selector(AriaRole::Link, Some(opts)),
2281 "internal:role=link[name=\"Click \\\"here\\\" now\"s]"
2282 );
2283 }
2284
2285 #[test]
2286 fn test_aria_role_as_str() {
2287 assert_eq!(AriaRole::Button.as_str(), "button");
2288 assert_eq!(AriaRole::Heading.as_str(), "heading");
2289 assert_eq!(AriaRole::Link.as_str(), "link");
2290 assert_eq!(AriaRole::Checkbox.as_str(), "checkbox");
2291 assert_eq!(AriaRole::Alert.as_str(), "alert");
2292 assert_eq!(AriaRole::Navigation.as_str(), "navigation");
2293 assert_eq!(AriaRole::Progressbar.as_str(), "progressbar");
2294 assert_eq!(AriaRole::Treeitem.as_str(), "treeitem");
2295 }
2296}