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