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