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