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 /// Creates a [`FrameLocator`](crate::protocol::FrameLocator) scoped within this locator's subtree.
549 ///
550 /// The `selector` identifies an iframe element within the locator's scope.
551 ///
552 /// See: <https://playwright.dev/docs/api/class-locator#locator-frame-locator>
553 pub fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
554 crate::protocol::FrameLocator::new(
555 Arc::clone(&self.frame),
556 format!("{} >> {}", self.selector, selector),
557 self.page.clone(),
558 )
559 }
560
561 /// Returns the Page this locator belongs to.
562 ///
563 /// Each locator is bound to the page that created it. Chained locators (via
564 /// `first()`, `last()`, `nth()`, `locator()`, `filter()`, etc.) all return
565 /// the same owning page. This matches the behavior of `locator.page` in
566 /// other Playwright language bindings.
567 ///
568 /// # Example
569 ///
570 /// ```ignore
571 /// # use playwright_rs::Playwright;
572 /// # #[tokio::main]
573 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
574 /// let playwright = Playwright::launch().await?;
575 /// let browser = playwright.chromium().launch().await?;
576 /// let page = browser.new_page().await?;
577 /// page.goto("https://example.com", None).await?;
578 ///
579 /// let locator = page.locator("h1").await;
580 /// let locator_page = locator.page()?;
581 /// assert_eq!(locator_page.url(), page.url());
582 /// # Ok(())
583 /// # }
584 /// ```
585 ///
586 /// See: <https://playwright.dev/docs/api/class-locator#locator-page>
587 pub fn page(&self) -> Result<crate::protocol::Page> {
588 Ok(self.page.clone())
589 }
590
591 /// Evaluate a JavaScript expression in the frame context.
592 ///
593 /// Used internally for injecting CSS (e.g., disabling animations) before screenshot assertions.
594 pub(crate) async fn evaluate_js<T: serde::Serialize>(
595 &self,
596 expression: &str,
597 _arg: Option<T>,
598 ) -> Result<()> {
599 self.frame
600 .frame_evaluate_expression(expression)
601 .await
602 .map_err(|e| self.wrap_error_with_selector(e))
603 }
604
605 /// Creates a locator for the first matching element.
606 ///
607 /// See: <https://playwright.dev/docs/api/class-locator#locator-first>
608 pub fn first(&self) -> Locator {
609 Locator::new(
610 Arc::clone(&self.frame),
611 format!("{} >> nth=0", self.selector),
612 self.page.clone(),
613 )
614 }
615
616 /// Creates a locator for the last matching element.
617 ///
618 /// See: <https://playwright.dev/docs/api/class-locator#locator-last>
619 pub fn last(&self) -> Locator {
620 Locator::new(
621 Arc::clone(&self.frame),
622 format!("{} >> nth=-1", self.selector),
623 self.page.clone(),
624 )
625 }
626
627 /// Creates a locator for the nth matching element (0-indexed).
628 ///
629 /// See: <https://playwright.dev/docs/api/class-locator#locator-nth>
630 pub fn nth(&self, index: i32) -> Locator {
631 Locator::new(
632 Arc::clone(&self.frame),
633 format!("{} >> nth={}", self.selector, index),
634 self.page.clone(),
635 )
636 }
637
638 /// Returns a locator that matches elements containing the given text.
639 ///
640 /// By default, matching is case-insensitive and searches for a substring.
641 /// Set `exact` to `true` for case-sensitive exact matching.
642 ///
643 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-text>
644 pub fn get_by_text(&self, text: &str, exact: bool) -> Locator {
645 self.locator(&get_by_text_selector(text, exact))
646 }
647
648 /// Returns a locator that matches elements by their associated label text.
649 ///
650 /// Targets form controls (`input`, `textarea`, `select`) linked via `<label>`,
651 /// `aria-label`, or `aria-labelledby`.
652 ///
653 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-label>
654 pub fn get_by_label(&self, text: &str, exact: bool) -> Locator {
655 self.locator(&get_by_label_selector(text, exact))
656 }
657
658 /// Returns a locator that matches elements by their placeholder text.
659 ///
660 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-placeholder>
661 pub fn get_by_placeholder(&self, text: &str, exact: bool) -> Locator {
662 self.locator(&get_by_placeholder_selector(text, exact))
663 }
664
665 /// Returns a locator that matches elements by their alt text.
666 ///
667 /// Typically used for `<img>` elements.
668 ///
669 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-alt-text>
670 pub fn get_by_alt_text(&self, text: &str, exact: bool) -> Locator {
671 self.locator(&get_by_alt_text_selector(text, exact))
672 }
673
674 /// Returns a locator that matches elements by their title attribute.
675 ///
676 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-title>
677 pub fn get_by_title(&self, text: &str, exact: bool) -> Locator {
678 self.locator(&get_by_title_selector(text, exact))
679 }
680
681 /// Returns a locator that matches elements by their test ID attribute.
682 ///
683 /// By default, uses the `data-testid` attribute. Call
684 /// `playwright.selectors().set_test_id_attribute()` to change the attribute name.
685 ///
686 /// Always uses exact matching (case-sensitive).
687 ///
688 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-test-id>
689 pub fn get_by_test_id(&self, test_id: &str) -> Locator {
690 use crate::server::channel_owner::ChannelOwner as _;
691 let attr = self.frame.connection().selectors().test_id_attribute();
692 self.locator(&get_by_test_id_selector_with_attr(test_id, &attr))
693 }
694
695 /// Returns a locator that matches elements by their ARIA role.
696 ///
697 /// This is the recommended way to locate elements, as it matches the way
698 /// users and assistive technology perceive the page.
699 ///
700 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-by-role>
701 pub fn get_by_role(&self, role: AriaRole, options: Option<GetByRoleOptions>) -> Locator {
702 self.locator(&get_by_role_selector(role, options))
703 }
704
705 /// Creates a sub-locator within this locator's subtree.
706 ///
707 /// See: <https://playwright.dev/docs/api/class-locator#locator-locator>
708 pub fn locator(&self, selector: &str) -> Locator {
709 Locator::new(
710 Arc::clone(&self.frame),
711 format!("{} >> {}", self.selector, selector),
712 self.page.clone(),
713 )
714 }
715
716 /// Narrows this locator according to the filter options.
717 ///
718 /// Can be chained to apply multiple filters in sequence.
719 ///
720 /// # Example
721 ///
722 /// ```ignore
723 /// use playwright_rs::{Playwright, FilterOptions};
724 ///
725 /// # #[tokio::main]
726 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
727 /// let playwright = Playwright::launch().await?;
728 /// let browser = playwright.chromium().launch().await?;
729 /// let page = browser.new_page().await?;
730 ///
731 /// // Filter rows to those containing "Apple"
732 /// let rows = page.locator("tr").await;
733 /// let apple_row = rows.filter(FilterOptions {
734 /// has_text: Some("Apple".to_string()),
735 /// ..Default::default()
736 /// });
737 /// # browser.close().await?;
738 /// # Ok(())
739 /// # }
740 /// ```
741 ///
742 /// See: <https://playwright.dev/docs/api/class-locator#locator-filter>
743 pub fn filter(&self, options: FilterOptions) -> Locator {
744 let mut selector = self.selector.clone();
745
746 if let Some(text) = &options.has_text {
747 let escaped = escape_for_selector(text, false);
748 selector = format!("{} >> internal:has-text={}", selector, escaped);
749 }
750
751 if let Some(text) = &options.has_not_text {
752 let escaped = escape_for_selector(text, false);
753 selector = format!("{} >> internal:has-not-text={}", selector, escaped);
754 }
755
756 if let Some(locator) = &options.has {
757 let inner = serde_json::to_string(&locator.selector)
758 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
759 selector = format!("{} >> internal:has={}", selector, inner);
760 }
761
762 if let Some(locator) = &options.has_not {
763 let inner = serde_json::to_string(&locator.selector)
764 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
765 selector = format!("{} >> internal:has-not={}", selector, inner);
766 }
767
768 Locator::new(Arc::clone(&self.frame), selector, self.page.clone())
769 }
770
771 /// Creates a locator matching elements that satisfy **both** this locator and `locator`.
772 ///
773 /// Note: named `and_` because `and` is a Rust keyword.
774 ///
775 /// # Example
776 ///
777 /// ```ignore
778 /// use playwright_rs::Playwright;
779 ///
780 /// # #[tokio::main]
781 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
782 /// let playwright = Playwright::launch().await?;
783 /// let browser = playwright.chromium().launch().await?;
784 /// let page = browser.new_page().await?;
785 ///
786 /// // Find a button that also has a specific title
787 /// let button = page.locator("button").await;
788 /// let titled = page.locator("[title='Subscribe']").await;
789 /// let subscribe_btn = button.and_(&titled);
790 /// # browser.close().await?;
791 /// # Ok(())
792 /// # }
793 /// ```
794 ///
795 /// See: <https://playwright.dev/docs/api/class-locator#locator-and>
796 pub fn and_(&self, locator: &Locator) -> Locator {
797 let inner = serde_json::to_string(&locator.selector)
798 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
799 Locator::new(
800 Arc::clone(&self.frame),
801 format!("{} >> internal:and={}", self.selector, inner),
802 self.page.clone(),
803 )
804 }
805
806 /// Creates a locator matching elements that satisfy **either** this locator or `locator`.
807 ///
808 /// Note: named `or_` because `or` is a Rust keyword.
809 ///
810 /// # Example
811 ///
812 /// ```ignore
813 /// use playwright_rs::Playwright;
814 ///
815 /// # #[tokio::main]
816 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
817 /// let playwright = Playwright::launch().await?;
818 /// let browser = playwright.chromium().launch().await?;
819 /// let page = browser.new_page().await?;
820 ///
821 /// // Find any element that is either a button or a link
822 /// let buttons = page.locator("button").await;
823 /// let links = page.locator("a").await;
824 /// let interactive = buttons.or_(&links);
825 /// # browser.close().await?;
826 /// # Ok(())
827 /// # }
828 /// ```
829 ///
830 /// See: <https://playwright.dev/docs/api/class-locator#locator-or>
831 pub fn or_(&self, locator: &Locator) -> Locator {
832 let inner = serde_json::to_string(&locator.selector)
833 .unwrap_or_else(|_| format!("\"{}\"", locator.selector));
834 Locator::new(
835 Arc::clone(&self.frame),
836 format!("{} >> internal:or={}", self.selector, inner),
837 self.page.clone(),
838 )
839 }
840
841 /// Returns the number of elements matching this locator.
842 ///
843 /// See: <https://playwright.dev/docs/api/class-locator#locator-count>
844 pub async fn count(&self) -> Result<usize> {
845 self.frame
846 .locator_count(&self.selector)
847 .await
848 .map_err(|e| self.wrap_error_with_selector(e))
849 }
850
851 /// Returns an array of locators, one for each matching element.
852 ///
853 /// Note: `all()` does not wait for elements to match the locator,
854 /// and instead immediately returns whatever is in the DOM.
855 ///
856 /// See: <https://playwright.dev/docs/api/class-locator#locator-all>
857 pub async fn all(&self) -> Result<Vec<Locator>> {
858 let count = self.count().await?;
859 Ok((0..count).map(|i| self.nth(i as i32)).collect())
860 }
861
862 /// Returns the text content of the element.
863 ///
864 /// See: <https://playwright.dev/docs/api/class-locator#locator-text-content>
865 pub async fn text_content(&self) -> Result<Option<String>> {
866 self.frame
867 .locator_text_content(&self.selector)
868 .await
869 .map_err(|e| self.wrap_error_with_selector(e))
870 }
871
872 /// Returns the inner text of the element (visible text).
873 ///
874 /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-text>
875 pub async fn inner_text(&self) -> Result<String> {
876 self.frame
877 .locator_inner_text(&self.selector)
878 .await
879 .map_err(|e| self.wrap_error_with_selector(e))
880 }
881
882 /// Returns the inner HTML of the element.
883 ///
884 /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-html>
885 pub async fn inner_html(&self) -> Result<String> {
886 self.frame
887 .locator_inner_html(&self.selector)
888 .await
889 .map_err(|e| self.wrap_error_with_selector(e))
890 }
891
892 /// Returns the value of the specified attribute.
893 ///
894 /// See: <https://playwright.dev/docs/api/class-locator#locator-get-attribute>
895 pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
896 self.frame
897 .locator_get_attribute(&self.selector, name)
898 .await
899 .map_err(|e| self.wrap_error_with_selector(e))
900 }
901
902 /// Returns whether the element is visible.
903 ///
904 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-visible>
905 pub async fn is_visible(&self) -> Result<bool> {
906 self.frame
907 .locator_is_visible(&self.selector)
908 .await
909 .map_err(|e| self.wrap_error_with_selector(e))
910 }
911
912 /// Returns whether the element is enabled.
913 ///
914 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-enabled>
915 pub async fn is_enabled(&self) -> Result<bool> {
916 self.frame
917 .locator_is_enabled(&self.selector)
918 .await
919 .map_err(|e| self.wrap_error_with_selector(e))
920 }
921
922 /// Returns whether the checkbox or radio button is checked.
923 ///
924 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-checked>
925 pub async fn is_checked(&self) -> Result<bool> {
926 self.frame
927 .locator_is_checked(&self.selector)
928 .await
929 .map_err(|e| self.wrap_error_with_selector(e))
930 }
931
932 /// Returns whether the element is editable.
933 ///
934 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-editable>
935 pub async fn is_editable(&self) -> Result<bool> {
936 self.frame
937 .locator_is_editable(&self.selector)
938 .await
939 .map_err(|e| self.wrap_error_with_selector(e))
940 }
941
942 /// Returns whether the element is hidden.
943 ///
944 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-hidden>
945 pub async fn is_hidden(&self) -> Result<bool> {
946 self.frame
947 .locator_is_hidden(&self.selector)
948 .await
949 .map_err(|e| self.wrap_error_with_selector(e))
950 }
951
952 /// Returns whether the element is disabled.
953 ///
954 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-disabled>
955 pub async fn is_disabled(&self) -> Result<bool> {
956 self.frame
957 .locator_is_disabled(&self.selector)
958 .await
959 .map_err(|e| self.wrap_error_with_selector(e))
960 }
961
962 /// Returns whether the element is focused (currently has focus).
963 ///
964 /// See: <https://playwright.dev/docs/api/class-locator#locator-is-focused>
965 pub async fn is_focused(&self) -> Result<bool> {
966 self.frame
967 .locator_is_focused(&self.selector)
968 .await
969 .map_err(|e| self.wrap_error_with_selector(e))
970 }
971
972 // Action methods
973
974 /// Clicks the element.
975 ///
976 /// See: <https://playwright.dev/docs/api/class-locator#locator-click>
977 pub async fn click(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
978 self.frame
979 .locator_click(&self.selector, Some(self.with_timeout(options)))
980 .await
981 .map_err(|e| self.wrap_error_with_selector(e))
982 }
983
984 /// Ensures an options struct has the page's default timeout when none is explicitly set.
985 fn with_timeout<T: HasTimeout + Default>(&self, options: Option<T>) -> T {
986 let mut opts = options.unwrap_or_default();
987 if opts.timeout_ref().is_none() {
988 *opts.timeout_ref_mut() = Some(self.page.default_timeout_ms());
989 }
990 opts
991 }
992
993 /// Wraps an error with selector context for better error messages.
994 fn wrap_error_with_selector(&self, error: crate::error::Error) -> crate::error::Error {
995 match &error {
996 crate::error::Error::ProtocolError(msg) => {
997 // Add selector context to protocol errors (timeouts, etc.)
998 crate::error::Error::ProtocolError(format!("{} [selector: {}]", msg, self.selector))
999 }
1000 crate::error::Error::Timeout(msg) => {
1001 crate::error::Error::Timeout(format!("{} [selector: {}]", msg, self.selector))
1002 }
1003 _ => error, // Other errors pass through unchanged
1004 }
1005 }
1006
1007 /// Double clicks the element.
1008 ///
1009 /// See: <https://playwright.dev/docs/api/class-locator#locator-dblclick>
1010 pub async fn dblclick(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
1011 self.frame
1012 .locator_dblclick(&self.selector, Some(self.with_timeout(options)))
1013 .await
1014 .map_err(|e| self.wrap_error_with_selector(e))
1015 }
1016
1017 /// Fills the element with text.
1018 ///
1019 /// See: <https://playwright.dev/docs/api/class-locator#locator-fill>
1020 pub async fn fill(
1021 &self,
1022 text: &str,
1023 options: Option<crate::protocol::FillOptions>,
1024 ) -> Result<()> {
1025 self.frame
1026 .locator_fill(&self.selector, text, Some(self.with_timeout(options)))
1027 .await
1028 .map_err(|e| self.wrap_error_with_selector(e))
1029 }
1030
1031 /// Clears the element's value.
1032 ///
1033 /// See: <https://playwright.dev/docs/api/class-locator#locator-clear>
1034 pub async fn clear(&self, options: Option<crate::protocol::FillOptions>) -> Result<()> {
1035 self.frame
1036 .locator_clear(&self.selector, Some(self.with_timeout(options)))
1037 .await
1038 .map_err(|e| self.wrap_error_with_selector(e))
1039 }
1040
1041 /// Presses a key on the element.
1042 ///
1043 /// See: <https://playwright.dev/docs/api/class-locator#locator-press>
1044 pub async fn press(
1045 &self,
1046 key: &str,
1047 options: Option<crate::protocol::PressOptions>,
1048 ) -> Result<()> {
1049 self.frame
1050 .locator_press(&self.selector, key, Some(self.with_timeout(options)))
1051 .await
1052 .map_err(|e| self.wrap_error_with_selector(e))
1053 }
1054
1055 /// Sets focus on the element.
1056 ///
1057 /// Calls the element's `focus()` method. Used to move keyboard focus to a
1058 /// specific element for subsequent keyboard interactions.
1059 ///
1060 /// See: <https://playwright.dev/docs/api/class-locator#locator-focus>
1061 pub async fn focus(&self) -> Result<()> {
1062 self.frame
1063 .locator_focus(&self.selector)
1064 .await
1065 .map_err(|e| self.wrap_error_with_selector(e))
1066 }
1067
1068 /// Removes focus from the element.
1069 ///
1070 /// Calls the element's `blur()` method. Moves keyboard focus away from the element.
1071 ///
1072 /// See: <https://playwright.dev/docs/api/class-locator#locator-blur>
1073 pub async fn blur(&self) -> Result<()> {
1074 self.frame
1075 .locator_blur(&self.selector)
1076 .await
1077 .map_err(|e| self.wrap_error_with_selector(e))
1078 }
1079
1080 /// Types `text` into the element character by character, as though it was typed
1081 /// on a real keyboard.
1082 ///
1083 /// Use this method when you need to simulate keystrokes with individual key events
1084 /// (e.g., for autocomplete widgets). For simply setting a field value, prefer
1085 /// [`Locator::fill()`].
1086 ///
1087 /// # Arguments
1088 ///
1089 /// * `text` - Text to type into the element
1090 /// * `options` - Optional [`PressSequentiallyOptions`](crate::protocol::PressSequentiallyOptions) (e.g., `delay` between key presses)
1091 ///
1092 /// See: <https://playwright.dev/docs/api/class-locator#locator-press-sequentially>
1093 pub async fn press_sequentially(
1094 &self,
1095 text: &str,
1096 options: Option<crate::protocol::PressSequentiallyOptions>,
1097 ) -> Result<()> {
1098 self.frame
1099 .locator_press_sequentially(&self.selector, text, options)
1100 .await
1101 .map_err(|e| self.wrap_error_with_selector(e))
1102 }
1103
1104 /// Returns the `innerText` values of all elements matching this locator.
1105 ///
1106 /// Unlike [`Locator::inner_text()`] (which uses strict mode and requires exactly one match),
1107 /// `all_inner_texts()` returns text from all matching elements.
1108 ///
1109 /// See: <https://playwright.dev/docs/api/class-locator#locator-all-inner-texts>
1110 pub async fn all_inner_texts(&self) -> Result<Vec<String>> {
1111 self.frame
1112 .locator_all_inner_texts(&self.selector)
1113 .await
1114 .map_err(|e| self.wrap_error_with_selector(e))
1115 }
1116
1117 /// Returns the `textContent` values of all elements matching this locator.
1118 ///
1119 /// Unlike [`Locator::text_content()`] (which uses strict mode and requires exactly one match),
1120 /// `all_text_contents()` returns text from all matching elements.
1121 ///
1122 /// See: <https://playwright.dev/docs/api/class-locator#locator-all-text-contents>
1123 pub async fn all_text_contents(&self) -> Result<Vec<String>> {
1124 self.frame
1125 .locator_all_text_contents(&self.selector)
1126 .await
1127 .map_err(|e| self.wrap_error_with_selector(e))
1128 }
1129
1130 /// Ensures the checkbox or radio button is checked.
1131 ///
1132 /// This method is idempotent - if already checked, does nothing.
1133 ///
1134 /// See: <https://playwright.dev/docs/api/class-locator#locator-check>
1135 pub async fn check(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
1136 self.frame
1137 .locator_check(&self.selector, Some(self.with_timeout(options)))
1138 .await
1139 .map_err(|e| self.wrap_error_with_selector(e))
1140 }
1141
1142 /// Ensures the checkbox is unchecked.
1143 ///
1144 /// This method is idempotent - if already unchecked, does nothing.
1145 ///
1146 /// See: <https://playwright.dev/docs/api/class-locator#locator-uncheck>
1147 pub async fn uncheck(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
1148 self.frame
1149 .locator_uncheck(&self.selector, Some(self.with_timeout(options)))
1150 .await
1151 .map_err(|e| self.wrap_error_with_selector(e))
1152 }
1153
1154 /// Sets the checkbox or radio button to the specified checked state.
1155 ///
1156 /// This is a convenience method that calls `check()` if `checked` is true,
1157 /// or `uncheck()` if `checked` is false.
1158 ///
1159 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-checked>
1160 pub async fn set_checked(
1161 &self,
1162 checked: bool,
1163 options: Option<crate::protocol::CheckOptions>,
1164 ) -> Result<()> {
1165 if checked {
1166 self.check(options).await
1167 } else {
1168 self.uncheck(options).await
1169 }
1170 }
1171
1172 /// Hovers the mouse over the element.
1173 ///
1174 /// See: <https://playwright.dev/docs/api/class-locator#locator-hover>
1175 pub async fn hover(&self, options: Option<crate::protocol::HoverOptions>) -> Result<()> {
1176 self.frame
1177 .locator_hover(&self.selector, Some(self.with_timeout(options)))
1178 .await
1179 .map_err(|e| self.wrap_error_with_selector(e))
1180 }
1181
1182 /// Returns the value of the input, textarea, or select element.
1183 ///
1184 /// See: <https://playwright.dev/docs/api/class-locator#locator-input-value>
1185 pub async fn input_value(&self, _options: Option<()>) -> Result<String> {
1186 self.frame
1187 .locator_input_value(&self.selector)
1188 .await
1189 .map_err(|e| self.wrap_error_with_selector(e))
1190 }
1191
1192 /// Selects one or more options in a select element.
1193 ///
1194 /// Returns an array of option values that have been successfully selected.
1195 ///
1196 /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1197 pub async fn select_option(
1198 &self,
1199 value: impl Into<crate::protocol::SelectOption>,
1200 options: Option<crate::protocol::SelectOptions>,
1201 ) -> Result<Vec<String>> {
1202 self.frame
1203 .locator_select_option(
1204 &self.selector,
1205 value.into(),
1206 Some(self.with_timeout(options)),
1207 )
1208 .await
1209 .map_err(|e| self.wrap_error_with_selector(e))
1210 }
1211
1212 /// Selects multiple options in a select element.
1213 ///
1214 /// Returns an array of option values that have been successfully selected.
1215 ///
1216 /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
1217 pub async fn select_option_multiple(
1218 &self,
1219 values: &[impl Into<crate::protocol::SelectOption> + Clone],
1220 options: Option<crate::protocol::SelectOptions>,
1221 ) -> Result<Vec<String>> {
1222 let select_options: Vec<crate::protocol::SelectOption> =
1223 values.iter().map(|v| v.clone().into()).collect();
1224 self.frame
1225 .locator_select_option_multiple(
1226 &self.selector,
1227 select_options,
1228 Some(self.with_timeout(options)),
1229 )
1230 .await
1231 .map_err(|e| self.wrap_error_with_selector(e))
1232 }
1233
1234 /// Sets the file path(s) to upload to a file input element.
1235 ///
1236 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1237 pub async fn set_input_files(
1238 &self,
1239 file: &std::path::PathBuf,
1240 _options: Option<()>,
1241 ) -> Result<()> {
1242 self.frame
1243 .locator_set_input_files(&self.selector, file)
1244 .await
1245 .map_err(|e| self.wrap_error_with_selector(e))
1246 }
1247
1248 /// Sets multiple file paths to upload to a file input element.
1249 ///
1250 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1251 pub async fn set_input_files_multiple(
1252 &self,
1253 files: &[&std::path::PathBuf],
1254 _options: Option<()>,
1255 ) -> Result<()> {
1256 self.frame
1257 .locator_set_input_files_multiple(&self.selector, files)
1258 .await
1259 .map_err(|e| self.wrap_error_with_selector(e))
1260 }
1261
1262 /// Sets a file to upload using FilePayload (explicit name, mimeType, buffer).
1263 ///
1264 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1265 pub async fn set_input_files_payload(
1266 &self,
1267 file: crate::protocol::FilePayload,
1268 _options: Option<()>,
1269 ) -> Result<()> {
1270 self.frame
1271 .locator_set_input_files_payload(&self.selector, file)
1272 .await
1273 .map_err(|e| self.wrap_error_with_selector(e))
1274 }
1275
1276 /// Sets multiple files to upload using FilePayload.
1277 ///
1278 /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
1279 pub async fn set_input_files_payload_multiple(
1280 &self,
1281 files: &[crate::protocol::FilePayload],
1282 _options: Option<()>,
1283 ) -> Result<()> {
1284 self.frame
1285 .locator_set_input_files_payload_multiple(&self.selector, files)
1286 .await
1287 .map_err(|e| self.wrap_error_with_selector(e))
1288 }
1289
1290 /// Dispatches a DOM event on the element.
1291 ///
1292 /// Unlike clicking or typing, `dispatch_event` directly sends the event without
1293 /// performing any actionability checks. It still waits for the element to be present
1294 /// in the DOM.
1295 ///
1296 /// # Arguments
1297 ///
1298 /// * `type_` - The event type to dispatch, e.g. `"click"`, `"focus"`, `"myevent"`.
1299 /// * `event_init` - Optional event initializer properties (e.g. `{"detail": "value"}` for
1300 /// `CustomEvent`). Corresponds to the second argument of `new Event(type, init)`.
1301 ///
1302 /// # Errors
1303 ///
1304 /// Returns an error if:
1305 /// - The element is not found within the timeout
1306 /// - The protocol call fails
1307 ///
1308 /// See: <https://playwright.dev/docs/api/class-locator#locator-dispatch-event>
1309 pub async fn dispatch_event(
1310 &self,
1311 type_: &str,
1312 event_init: Option<serde_json::Value>,
1313 ) -> Result<()> {
1314 self.frame
1315 .locator_dispatch_event(&self.selector, type_, event_init)
1316 .await
1317 .map_err(|e| self.wrap_error_with_selector(e))
1318 }
1319
1320 /// Returns the bounding box of the element, or `None` if the element is not visible.
1321 ///
1322 /// The bounding box is in pixels, relative to the top-left corner of the page.
1323 /// Returns `None` when the element has `display: none` or is otherwise not part of
1324 /// the layout.
1325 ///
1326 /// # Errors
1327 ///
1328 /// Returns an error if:
1329 /// - The element is not found within the timeout
1330 /// - The protocol call fails
1331 ///
1332 /// See: <https://playwright.dev/docs/api/class-locator#locator-bounding-box>
1333 pub async fn bounding_box(&self) -> Result<Option<BoundingBox>> {
1334 self.frame
1335 .locator_bounding_box(&self.selector)
1336 .await
1337 .map_err(|e| self.wrap_error_with_selector(e))
1338 }
1339
1340 /// Scrolls the element into view if it is not already visible in the viewport.
1341 ///
1342 /// This is an alias for calling `element.scrollIntoView()` in the browser.
1343 ///
1344 /// # Errors
1345 ///
1346 /// Returns an error if:
1347 /// - The element is not found within the timeout
1348 /// - The protocol call fails
1349 ///
1350 /// See: <https://playwright.dev/docs/api/class-locator#locator-scroll-into-view-if-needed>
1351 pub async fn scroll_into_view_if_needed(&self) -> Result<()> {
1352 self.frame
1353 .locator_scroll_into_view_if_needed(&self.selector)
1354 .await
1355 .map_err(|e| self.wrap_error_with_selector(e))
1356 }
1357
1358 /// Takes a screenshot of the element and returns the image bytes.
1359 ///
1360 /// This method uses strict mode - it will fail if the selector matches multiple elements.
1361 /// Use `first()`, `last()`, or `nth()` to refine the selector to a single element.
1362 ///
1363 /// See: <https://playwright.dev/docs/api/class-locator#locator-screenshot>
1364 pub async fn screenshot(
1365 &self,
1366 options: Option<crate::protocol::ScreenshotOptions>,
1367 ) -> Result<Vec<u8>> {
1368 // Query for the element using strict mode (should return exactly one)
1369 let element = self
1370 .frame
1371 .query_selector(&self.selector)
1372 .await
1373 .map_err(|e| self.wrap_error_with_selector(e))?
1374 .ok_or_else(|| {
1375 crate::error::Error::ElementNotFound(format!(
1376 "Element not found: {}",
1377 self.selector
1378 ))
1379 })?;
1380
1381 // Delegate to ElementHandle.screenshot() with default timeout injected
1382 element
1383 .screenshot(Some(self.with_timeout(options)))
1384 .await
1385 .map_err(|e| self.wrap_error_with_selector(e))
1386 }
1387
1388 /// Performs a touch-tap on the element.
1389 ///
1390 /// This method dispatches a `touchstart` and `touchend` event on the element.
1391 /// For touch support to work, the browser context must be created with
1392 /// `has_touch: true`.
1393 ///
1394 /// # Arguments
1395 ///
1396 /// * `options` - Optional [`TapOptions`](crate::protocol::TapOptions) (force, modifiers, position, timeout, trial)
1397 ///
1398 /// # Errors
1399 ///
1400 /// Returns an error if:
1401 /// - The element is not found within the timeout
1402 /// - Actionability checks fail (unless `force: true`)
1403 /// - The browser context was not created with `has_touch: true`
1404 ///
1405 /// See: <https://playwright.dev/docs/api/class-locator#locator-tap>
1406 pub async fn tap(&self, options: Option<crate::protocol::TapOptions>) -> Result<()> {
1407 self.frame
1408 .locator_tap(&self.selector, Some(self.with_timeout(options)))
1409 .await
1410 .map_err(|e| self.wrap_error_with_selector(e))
1411 }
1412
1413 /// Drags this element to the `target` element.
1414 ///
1415 /// Both this locator and `target` must resolve to elements in the same frame.
1416 /// Playwright performs a series of mouse events (move, press, move to target, release)
1417 /// to simulate the drag.
1418 ///
1419 /// # Arguments
1420 ///
1421 /// * `target` - The locator of the element to drag onto
1422 /// * `options` - Optional [`DragToOptions`](crate::protocol::DragToOptions) (force, no_wait_after, timeout, trial,
1423 /// source_position, target_position)
1424 ///
1425 /// # Errors
1426 ///
1427 /// Returns an error if:
1428 /// - Either element is not found within the timeout
1429 /// - Actionability checks fail (unless `force: true`)
1430 /// - The protocol call fails
1431 ///
1432 /// See: <https://playwright.dev/docs/api/class-locator#locator-drag-to>
1433 pub async fn drag_to(
1434 &self,
1435 target: &Locator,
1436 options: Option<crate::protocol::DragToOptions>,
1437 ) -> Result<()> {
1438 self.frame
1439 .locator_drag_to(
1440 &self.selector,
1441 &target.selector,
1442 Some(self.with_timeout(options)),
1443 )
1444 .await
1445 .map_err(|e| self.wrap_error_with_selector(e))
1446 }
1447
1448 /// Waits until the element satisfies the given state condition.
1449 ///
1450 /// If no state is specified, waits for the element to be `visible` (the default).
1451 ///
1452 /// This method is useful for waiting for lazy-rendered elements or elements that
1453 /// appear/disappear based on user interaction or async data loading.
1454 ///
1455 /// # Arguments
1456 ///
1457 /// * `options` - Optional [`WaitForOptions`](crate::protocol::WaitForOptions) specifying the `state` to wait for
1458 /// (`Visible`, `Hidden`, `Attached`, or `Detached`) and a `timeout` in milliseconds.
1459 ///
1460 /// # Errors
1461 ///
1462 /// Returns an error if the element does not satisfy the expected state within the timeout.
1463 ///
1464 /// See: <https://playwright.dev/docs/api/class-locator#locator-wait-for>
1465 pub async fn wait_for(&self, options: Option<crate::protocol::WaitForOptions>) -> Result<()> {
1466 self.frame
1467 .locator_wait_for(&self.selector, Some(self.with_timeout(options)))
1468 .await
1469 .map_err(|e| self.wrap_error_with_selector(e))
1470 }
1471
1472 /// Evaluates a JavaScript expression in the scope of the matched element.
1473 ///
1474 /// The element is passed as the first argument to the expression. The expression
1475 /// can be any JavaScript function or expression that returns a JSON-serializable value.
1476 ///
1477 /// # Arguments
1478 ///
1479 /// * `expression` - JavaScript expression or function, e.g. `"(el) => el.textContent"`
1480 /// * `arg` - Optional argument passed as the second argument to the function
1481 ///
1482 /// # Errors
1483 ///
1484 /// Returns an error if:
1485 /// - The element is not found within the timeout
1486 /// - The JavaScript expression throws an error
1487 /// - The return value is not JSON-serializable
1488 ///
1489 /// # Example
1490 ///
1491 /// ```ignore
1492 /// use playwright_rs::Playwright;
1493 ///
1494 /// # #[tokio::main]
1495 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1496 /// let playwright = Playwright::launch().await?;
1497 /// let browser = playwright.chromium().launch().await?;
1498 /// let page = browser.new_page().await?;
1499 /// let _ = page.goto("data:text/html,<h1>Hello</h1>", None).await;
1500 ///
1501 /// let heading = page.locator("h1").await;
1502 /// let text: String = heading.evaluate("(el) => el.textContent", None::<()>).await?;
1503 /// assert_eq!(text, "Hello");
1504 ///
1505 /// // With an argument
1506 /// let result: String = heading
1507 /// .evaluate("(el, suffix) => el.textContent + suffix", Some("!"))
1508 /// .await?;
1509 /// assert_eq!(result, "Hello!");
1510 /// # browser.close().await?;
1511 /// # Ok(())
1512 /// # }
1513 /// ```
1514 ///
1515 /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate>
1516 pub async fn evaluate<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1517 where
1518 R: serde::de::DeserializeOwned,
1519 T: serde::Serialize,
1520 {
1521 let raw = self
1522 .frame
1523 .locator_evaluate(&self.selector, expression, arg)
1524 .await
1525 .map_err(|e| self.wrap_error_with_selector(e))?;
1526 serde_json::from_value(raw).map_err(|e| {
1527 crate::error::Error::ProtocolError(format!(
1528 "evaluate result deserialization failed: {}",
1529 e
1530 ))
1531 })
1532 }
1533
1534 /// Evaluates a JavaScript expression in the scope of all elements matching this locator.
1535 ///
1536 /// The array of all matched elements is passed as the first argument to the expression.
1537 /// Unlike [`evaluate()`](Self::evaluate), this does not use strict mode — all matching
1538 /// elements are collected and passed as an array.
1539 ///
1540 /// # Arguments
1541 ///
1542 /// * `expression` - JavaScript function that receives an array of elements
1543 /// * `arg` - Optional argument passed as the second argument to the function
1544 ///
1545 /// # Errors
1546 ///
1547 /// Returns an error if:
1548 /// - The JavaScript expression throws an error
1549 /// - The return value is not JSON-serializable
1550 ///
1551 /// # Example
1552 ///
1553 /// ```ignore
1554 /// use playwright_rs::Playwright;
1555 ///
1556 /// # #[tokio::main]
1557 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1558 /// let playwright = Playwright::launch().await?;
1559 /// let browser = playwright.chromium().launch().await?;
1560 /// let page = browser.new_page().await?;
1561 /// let _ = page.goto(
1562 /// "data:text/html,<li class='item'>A</li><li class='item'>B</li>",
1563 /// None
1564 /// ).await;
1565 ///
1566 /// let items = page.locator(".item").await;
1567 /// let texts: Vec<String> = items
1568 /// .evaluate_all("(elements) => elements.map(e => e.textContent)", None::<()>)
1569 /// .await?;
1570 /// assert_eq!(texts, vec!["A", "B"]);
1571 /// # browser.close().await?;
1572 /// # Ok(())
1573 /// # }
1574 /// ```
1575 ///
1576 /// See: <https://playwright.dev/docs/api/class-locator#locator-evaluate-all>
1577 pub async fn evaluate_all<R, T>(&self, expression: &str, arg: Option<T>) -> Result<R>
1578 where
1579 R: serde::de::DeserializeOwned,
1580 T: serde::Serialize,
1581 {
1582 let raw = self
1583 .frame
1584 .locator_evaluate_all(&self.selector, expression, arg)
1585 .await
1586 .map_err(|e| self.wrap_error_with_selector(e))?;
1587 serde_json::from_value(raw).map_err(|e| {
1588 crate::error::Error::ProtocolError(format!(
1589 "evaluate_all result deserialization failed: {}",
1590 e
1591 ))
1592 })
1593 }
1594}
1595
1596impl std::fmt::Debug for Locator {
1597 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1598 f.debug_struct("Locator")
1599 .field("selector", &self.selector)
1600 .finish()
1601 }
1602}
1603
1604#[cfg(test)]
1605mod tests {
1606 use super::*;
1607
1608 #[test]
1609 fn test_escape_for_selector_case_insensitive() {
1610 assert_eq!(escape_for_selector("hello", false), "\"hello\"i");
1611 }
1612
1613 #[test]
1614 fn test_escape_for_selector_exact() {
1615 assert_eq!(escape_for_selector("hello", true), "\"hello\"s");
1616 }
1617
1618 #[test]
1619 fn test_escape_for_selector_with_quotes() {
1620 assert_eq!(
1621 escape_for_selector("say \"hi\"", false),
1622 "\"say \\\"hi\\\"\"i"
1623 );
1624 }
1625
1626 #[test]
1627 fn test_get_by_text_selector_case_insensitive() {
1628 assert_eq!(
1629 get_by_text_selector("Click me", false),
1630 "internal:text=\"Click me\"i"
1631 );
1632 }
1633
1634 #[test]
1635 fn test_get_by_text_selector_exact() {
1636 assert_eq!(
1637 get_by_text_selector("Click me", true),
1638 "internal:text=\"Click me\"s"
1639 );
1640 }
1641
1642 #[test]
1643 fn test_get_by_label_selector() {
1644 assert_eq!(
1645 get_by_label_selector("Email", false),
1646 "internal:label=\"Email\"i"
1647 );
1648 }
1649
1650 #[test]
1651 fn test_get_by_placeholder_selector() {
1652 assert_eq!(
1653 get_by_placeholder_selector("Enter name", false),
1654 "internal:attr=[placeholder=\"Enter name\"i]"
1655 );
1656 }
1657
1658 #[test]
1659 fn test_get_by_alt_text_selector() {
1660 assert_eq!(
1661 get_by_alt_text_selector("Logo", true),
1662 "internal:attr=[alt=\"Logo\"s]"
1663 );
1664 }
1665
1666 #[test]
1667 fn test_get_by_title_selector() {
1668 assert_eq!(
1669 get_by_title_selector("Help", false),
1670 "internal:attr=[title=\"Help\"i]"
1671 );
1672 }
1673
1674 #[test]
1675 fn test_get_by_test_id_selector() {
1676 assert_eq!(
1677 get_by_test_id_selector("submit-btn"),
1678 "internal:testid=[data-testid=\"submit-btn\"s]"
1679 );
1680 }
1681
1682 #[test]
1683 fn test_escape_for_attribute_selector_case_insensitive() {
1684 assert_eq!(
1685 escape_for_attribute_selector("Submit", false),
1686 "\"Submit\"i"
1687 );
1688 }
1689
1690 #[test]
1691 fn test_escape_for_attribute_selector_exact() {
1692 assert_eq!(escape_for_attribute_selector("Submit", true), "\"Submit\"s");
1693 }
1694
1695 #[test]
1696 fn test_escape_for_attribute_selector_escapes_quotes() {
1697 assert_eq!(
1698 escape_for_attribute_selector("Say \"hello\"", false),
1699 "\"Say \\\"hello\\\"\"i"
1700 );
1701 }
1702
1703 #[test]
1704 fn test_escape_for_attribute_selector_escapes_backslashes() {
1705 assert_eq!(
1706 escape_for_attribute_selector("path\\to", true),
1707 "\"path\\\\to\"s"
1708 );
1709 }
1710
1711 #[test]
1712 fn test_get_by_role_selector_role_only() {
1713 assert_eq!(
1714 get_by_role_selector(AriaRole::Button, None),
1715 "internal:role=button"
1716 );
1717 }
1718
1719 #[test]
1720 fn test_get_by_role_selector_with_name() {
1721 let opts = GetByRoleOptions {
1722 name: Some("Submit".to_string()),
1723 ..Default::default()
1724 };
1725 assert_eq!(
1726 get_by_role_selector(AriaRole::Button, Some(opts)),
1727 "internal:role=button[name=\"Submit\"i]"
1728 );
1729 }
1730
1731 #[test]
1732 fn test_get_by_role_selector_with_name_exact() {
1733 let opts = GetByRoleOptions {
1734 name: Some("Submit".to_string()),
1735 exact: Some(true),
1736 ..Default::default()
1737 };
1738 assert_eq!(
1739 get_by_role_selector(AriaRole::Button, Some(opts)),
1740 "internal:role=button[name=\"Submit\"s]"
1741 );
1742 }
1743
1744 #[test]
1745 fn test_get_by_role_selector_with_checked() {
1746 let opts = GetByRoleOptions {
1747 checked: Some(true),
1748 ..Default::default()
1749 };
1750 assert_eq!(
1751 get_by_role_selector(AriaRole::Checkbox, Some(opts)),
1752 "internal:role=checkbox[checked=true]"
1753 );
1754 }
1755
1756 #[test]
1757 fn test_get_by_role_selector_with_level() {
1758 let opts = GetByRoleOptions {
1759 level: Some(2),
1760 ..Default::default()
1761 };
1762 assert_eq!(
1763 get_by_role_selector(AriaRole::Heading, Some(opts)),
1764 "internal:role=heading[level=2]"
1765 );
1766 }
1767
1768 #[test]
1769 fn test_get_by_role_selector_with_disabled() {
1770 let opts = GetByRoleOptions {
1771 disabled: Some(true),
1772 ..Default::default()
1773 };
1774 assert_eq!(
1775 get_by_role_selector(AriaRole::Button, Some(opts)),
1776 "internal:role=button[disabled=true]"
1777 );
1778 }
1779
1780 #[test]
1781 fn test_get_by_role_selector_include_hidden() {
1782 let opts = GetByRoleOptions {
1783 include_hidden: Some(true),
1784 ..Default::default()
1785 };
1786 assert_eq!(
1787 get_by_role_selector(AriaRole::Button, Some(opts)),
1788 "internal:role=button[include-hidden=true]"
1789 );
1790 }
1791
1792 #[test]
1793 fn test_get_by_role_selector_property_order() {
1794 // All properties: checked, disabled, selected, expanded, include-hidden, level, name, pressed
1795 let opts = GetByRoleOptions {
1796 pressed: Some(true),
1797 name: Some("OK".to_string()),
1798 checked: Some(false),
1799 disabled: Some(true),
1800 ..Default::default()
1801 };
1802 assert_eq!(
1803 get_by_role_selector(AriaRole::Button, Some(opts)),
1804 "internal:role=button[checked=false][disabled=true][name=\"OK\"i][pressed=true]"
1805 );
1806 }
1807
1808 #[test]
1809 fn test_get_by_role_selector_name_with_special_chars() {
1810 let opts = GetByRoleOptions {
1811 name: Some("Click \"here\" now".to_string()),
1812 exact: Some(true),
1813 ..Default::default()
1814 };
1815 assert_eq!(
1816 get_by_role_selector(AriaRole::Link, Some(opts)),
1817 "internal:role=link[name=\"Click \\\"here\\\" now\"s]"
1818 );
1819 }
1820
1821 #[test]
1822 fn test_aria_role_as_str() {
1823 assert_eq!(AriaRole::Button.as_str(), "button");
1824 assert_eq!(AriaRole::Heading.as_str(), "heading");
1825 assert_eq!(AriaRole::Link.as_str(), "link");
1826 assert_eq!(AriaRole::Checkbox.as_str(), "checkbox");
1827 assert_eq!(AriaRole::Alert.as_str(), "alert");
1828 assert_eq!(AriaRole::Navigation.as_str(), "navigation");
1829 assert_eq!(AriaRole::Progressbar.as_str(), "progressbar");
1830 assert_eq!(AriaRole::Treeitem.as_str(), "treeitem");
1831 }
1832}