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