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 std::sync::Arc;
21
22/// Locator represents a way to find element(s) on the page at any given moment.
23///
24/// Locators are lazy - they don't execute queries until an action is performed.
25/// This enables auto-waiting and retry-ability for robust test automation.
26///
27/// # Examples
28///
29/// ```ignore
30/// use playwright_rs::protocol::{Playwright, SelectOption};
31///
32/// #[tokio::main]
33/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
34///     let playwright = Playwright::launch().await?;
35///     let browser = playwright.chromium().launch().await?;
36///     let page = browser.new_page().await?;
37///
38///     // Demonstrate set_checked() - checkbox interaction
39///     let _ = page.goto(
40///         "data:text/html,<input type='checkbox' id='cb'>",
41///         None
42///     ).await;
43///     let checkbox = page.locator("#cb").await;
44///     checkbox.set_checked(true, None).await?;
45///     assert!(checkbox.is_checked().await?);
46///     checkbox.set_checked(false, None).await?;
47///     assert!(!checkbox.is_checked().await?);
48///
49///     // Demonstrate select_option() - select by value, label, and index
50///     let _ = page.goto(
51///         "data:text/html,<select id='fruits'>\
52///             <option value='apple'>Apple</option>\
53///             <option value='banana'>Banana</option>\
54///             <option value='cherry'>Cherry</option>\
55///         </select>",
56///         None
57///     ).await;
58///     let select = page.locator("#fruits").await;
59///     select.select_option("banana", None).await?;
60///     assert_eq!(select.input_value(None).await?, "banana");
61///     select.select_option(SelectOption::Label("Apple".to_string()), None).await?;
62///     assert_eq!(select.input_value(None).await?, "apple");
63///     select.select_option(SelectOption::Index(2), None).await?;
64///     assert_eq!(select.input_value(None).await?, "cherry");
65///
66///     // Demonstrate select_option_multiple() - multi-select
67///     let _ = page.goto(
68///         "data:text/html,<select id='colors' multiple>\
69///             <option value='red'>Red</option>\
70///             <option value='green'>Green</option>\
71///             <option value='blue'>Blue</option>\
72///             <option value='yellow'>Yellow</option>\
73///         </select>",
74///         None
75///     ).await;
76///     let multi = page.locator("#colors").await;
77///     let selected = multi.select_option_multiple(&["red", "blue"], None).await?;
78///     assert_eq!(selected.len(), 2);
79///     assert!(selected.contains(&"red".to_string()));
80///     assert!(selected.contains(&"blue".to_string()));
81///
82///     // Demonstrate screenshot() - element screenshot
83///     let _ = page.goto(
84///         "data:text/html,<h1 id='title'>Hello World</h1>",
85///         None
86///     ).await;
87///     let heading = page.locator("#title").await;
88///     let screenshot = heading.screenshot(None).await?;
89///     assert!(!screenshot.is_empty());
90///
91///     browser.close().await?;
92///     Ok(())
93/// }
94/// ```
95///
96/// See: <https://playwright.dev/docs/api/class-locator>
97#[derive(Clone)]
98pub struct Locator {
99    frame: Arc<Frame>,
100    selector: String,
101}
102
103impl Locator {
104    /// Creates a new Locator (internal use only)
105    ///
106    /// Use `page.locator()` or `frame.locator()` to create locators in application code.
107    pub(crate) fn new(frame: Arc<Frame>, selector: String) -> Self {
108        Self { frame, selector }
109    }
110
111    /// Returns the selector string for this locator
112    pub fn selector(&self) -> &str {
113        &self.selector
114    }
115
116    /// Creates a locator for the first matching element.
117    ///
118    /// See: <https://playwright.dev/docs/api/class-locator#locator-first>
119    pub fn first(&self) -> Locator {
120        Locator::new(
121            Arc::clone(&self.frame),
122            format!("{} >> nth=0", self.selector),
123        )
124    }
125
126    /// Creates a locator for the last matching element.
127    ///
128    /// See: <https://playwright.dev/docs/api/class-locator#locator-last>
129    pub fn last(&self) -> Locator {
130        Locator::new(
131            Arc::clone(&self.frame),
132            format!("{} >> nth=-1", self.selector),
133        )
134    }
135
136    /// Creates a locator for the nth matching element (0-indexed).
137    ///
138    /// See: <https://playwright.dev/docs/api/class-locator#locator-nth>
139    pub fn nth(&self, index: i32) -> Locator {
140        Locator::new(
141            Arc::clone(&self.frame),
142            format!("{} >> nth={}", self.selector, index),
143        )
144    }
145
146    /// Creates a sub-locator within this locator's subtree.
147    ///
148    /// See: <https://playwright.dev/docs/api/class-locator#locator-locator>
149    pub fn locator(&self, selector: &str) -> Locator {
150        Locator::new(
151            Arc::clone(&self.frame),
152            format!("{} >> {}", self.selector, selector),
153        )
154    }
155
156    /// Returns the number of elements matching this locator.
157    ///
158    /// See: <https://playwright.dev/docs/api/class-locator#locator-count>
159    pub async fn count(&self) -> Result<usize> {
160        self.frame.locator_count(&self.selector).await
161    }
162
163    /// Returns the text content of the element.
164    ///
165    /// See: <https://playwright.dev/docs/api/class-locator#locator-text-content>
166    pub async fn text_content(&self) -> Result<Option<String>> {
167        self.frame.locator_text_content(&self.selector).await
168    }
169
170    /// Returns the inner text of the element (visible text).
171    ///
172    /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-text>
173    pub async fn inner_text(&self) -> Result<String> {
174        self.frame.locator_inner_text(&self.selector).await
175    }
176
177    /// Returns the inner HTML of the element.
178    ///
179    /// See: <https://playwright.dev/docs/api/class-locator#locator-inner-html>
180    pub async fn inner_html(&self) -> Result<String> {
181        self.frame.locator_inner_html(&self.selector).await
182    }
183
184    /// Returns the value of the specified attribute.
185    ///
186    /// See: <https://playwright.dev/docs/api/class-locator#locator-get-attribute>
187    pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
188        self.frame.locator_get_attribute(&self.selector, name).await
189    }
190
191    /// Returns whether the element is visible.
192    ///
193    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-visible>
194    pub async fn is_visible(&self) -> Result<bool> {
195        self.frame.locator_is_visible(&self.selector).await
196    }
197
198    /// Returns whether the element is enabled.
199    ///
200    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-enabled>
201    pub async fn is_enabled(&self) -> Result<bool> {
202        self.frame.locator_is_enabled(&self.selector).await
203    }
204
205    /// Returns whether the checkbox or radio button is checked.
206    ///
207    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-checked>
208    pub async fn is_checked(&self) -> Result<bool> {
209        self.frame.locator_is_checked(&self.selector).await
210    }
211
212    /// Returns whether the element is editable.
213    ///
214    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-editable>
215    pub async fn is_editable(&self) -> Result<bool> {
216        self.frame.locator_is_editable(&self.selector).await
217    }
218
219    /// Returns whether the element is focused (currently has focus).
220    ///
221    /// See: <https://playwright.dev/docs/api/class-locator#locator-is-focused>
222    pub async fn is_focused(&self) -> Result<bool> {
223        self.frame.locator_is_focused(&self.selector).await
224    }
225
226    // Action methods
227
228    /// Clicks the element.
229    ///
230    /// See: <https://playwright.dev/docs/api/class-locator#locator-click>
231    pub async fn click(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
232        self.frame
233            .locator_click(&self.selector, options)
234            .await
235            .map_err(|e| self.wrap_error_with_selector(e))
236    }
237
238    /// Wraps an error with selector context for better error messages.
239    fn wrap_error_with_selector(&self, error: crate::error::Error) -> crate::error::Error {
240        match &error {
241            crate::error::Error::ProtocolError(msg) => {
242                // Add selector context to protocol errors (timeouts, etc.)
243                crate::error::Error::ProtocolError(format!("{} [selector: {}]", msg, self.selector))
244            }
245            crate::error::Error::Timeout(msg) => {
246                crate::error::Error::Timeout(format!("{} [selector: {}]", msg, self.selector))
247            }
248            _ => error, // Other errors pass through unchanged
249        }
250    }
251
252    /// Double clicks the element.
253    ///
254    /// See: <https://playwright.dev/docs/api/class-locator#locator-dblclick>
255    pub async fn dblclick(&self, options: Option<crate::protocol::ClickOptions>) -> Result<()> {
256        self.frame.locator_dblclick(&self.selector, options).await
257    }
258
259    /// Fills the element with text.
260    ///
261    /// See: <https://playwright.dev/docs/api/class-locator#locator-fill>
262    pub async fn fill(
263        &self,
264        text: &str,
265        options: Option<crate::protocol::FillOptions>,
266    ) -> Result<()> {
267        self.frame.locator_fill(&self.selector, text, options).await
268    }
269
270    /// Clears the element's value.
271    ///
272    /// See: <https://playwright.dev/docs/api/class-locator#locator-clear>
273    pub async fn clear(&self, options: Option<crate::protocol::FillOptions>) -> Result<()> {
274        self.frame.locator_clear(&self.selector, options).await
275    }
276
277    /// Presses a key on the element.
278    ///
279    /// See: <https://playwright.dev/docs/api/class-locator#locator-press>
280    pub async fn press(
281        &self,
282        key: &str,
283        options: Option<crate::protocol::PressOptions>,
284    ) -> Result<()> {
285        self.frame.locator_press(&self.selector, key, options).await
286    }
287
288    /// Ensures the checkbox or radio button is checked.
289    ///
290    /// This method is idempotent - if already checked, does nothing.
291    ///
292    /// See: <https://playwright.dev/docs/api/class-locator#locator-check>
293    pub async fn check(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
294        self.frame.locator_check(&self.selector, options).await
295    }
296
297    /// Ensures the checkbox is unchecked.
298    ///
299    /// This method is idempotent - if already unchecked, does nothing.
300    ///
301    /// See: <https://playwright.dev/docs/api/class-locator#locator-uncheck>
302    pub async fn uncheck(&self, options: Option<crate::protocol::CheckOptions>) -> Result<()> {
303        self.frame.locator_uncheck(&self.selector, options).await
304    }
305
306    /// Sets the checkbox or radio button to the specified checked state.
307    ///
308    /// This is a convenience method that calls `check()` if `checked` is true,
309    /// or `uncheck()` if `checked` is false.
310    ///
311    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-checked>
312    pub async fn set_checked(
313        &self,
314        checked: bool,
315        options: Option<crate::protocol::CheckOptions>,
316    ) -> Result<()> {
317        if checked {
318            self.check(options).await
319        } else {
320            self.uncheck(options).await
321        }
322    }
323
324    /// Hovers the mouse over the element.
325    ///
326    /// See: <https://playwright.dev/docs/api/class-locator#locator-hover>
327    pub async fn hover(&self, options: Option<crate::protocol::HoverOptions>) -> Result<()> {
328        self.frame.locator_hover(&self.selector, options).await
329    }
330
331    /// Returns the value of the input, textarea, or select element.
332    ///
333    /// See: <https://playwright.dev/docs/api/class-locator#locator-input-value>
334    pub async fn input_value(&self, _options: Option<()>) -> Result<String> {
335        self.frame.locator_input_value(&self.selector).await
336    }
337
338    /// Selects one or more options in a select element.
339    ///
340    /// Returns an array of option values that have been successfully selected.
341    ///
342    /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
343    pub async fn select_option(
344        &self,
345        value: impl Into<crate::protocol::SelectOption>,
346        options: Option<crate::protocol::SelectOptions>,
347    ) -> Result<Vec<String>> {
348        self.frame
349            .locator_select_option(&self.selector, value.into(), options)
350            .await
351    }
352
353    /// Selects multiple options in a select element.
354    ///
355    /// Returns an array of option values that have been successfully selected.
356    ///
357    /// See: <https://playwright.dev/docs/api/class-locator#locator-select-option>
358    pub async fn select_option_multiple(
359        &self,
360        values: &[impl Into<crate::protocol::SelectOption> + Clone],
361        options: Option<crate::protocol::SelectOptions>,
362    ) -> Result<Vec<String>> {
363        let select_options: Vec<crate::protocol::SelectOption> =
364            values.iter().map(|v| v.clone().into()).collect();
365        self.frame
366            .locator_select_option_multiple(&self.selector, select_options, options)
367            .await
368    }
369
370    /// Sets the file path(s) to upload to a file input element.
371    ///
372    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
373    pub async fn set_input_files(
374        &self,
375        file: &std::path::PathBuf,
376        _options: Option<()>,
377    ) -> Result<()> {
378        self.frame
379            .locator_set_input_files(&self.selector, file)
380            .await
381    }
382
383    /// Sets multiple file paths to upload to a file input element.
384    ///
385    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
386    pub async fn set_input_files_multiple(
387        &self,
388        files: &[&std::path::PathBuf],
389        _options: Option<()>,
390    ) -> Result<()> {
391        self.frame
392            .locator_set_input_files_multiple(&self.selector, files)
393            .await
394    }
395
396    /// Sets a file to upload using FilePayload (explicit name, mimeType, buffer).
397    ///
398    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
399    pub async fn set_input_files_payload(
400        &self,
401        file: crate::protocol::FilePayload,
402        _options: Option<()>,
403    ) -> Result<()> {
404        self.frame
405            .locator_set_input_files_payload(&self.selector, file)
406            .await
407    }
408
409    /// Sets multiple files to upload using FilePayload.
410    ///
411    /// See: <https://playwright.dev/docs/api/class-locator#locator-set-input-files>
412    pub async fn set_input_files_payload_multiple(
413        &self,
414        files: &[crate::protocol::FilePayload],
415        _options: Option<()>,
416    ) -> Result<()> {
417        self.frame
418            .locator_set_input_files_payload_multiple(&self.selector, files)
419            .await
420    }
421
422    /// Takes a screenshot of the element and returns the image bytes.
423    ///
424    /// This method uses strict mode - it will fail if the selector matches multiple elements.
425    /// Use `first()`, `last()`, or `nth()` to refine the selector to a single element.
426    ///
427    /// See: <https://playwright.dev/docs/api/class-locator#locator-screenshot>
428    pub async fn screenshot(
429        &self,
430        options: Option<crate::protocol::ScreenshotOptions>,
431    ) -> Result<Vec<u8>> {
432        // Query for the element using strict mode (should return exactly one)
433        let element = self
434            .frame
435            .query_selector(&self.selector)
436            .await?
437            .ok_or_else(|| {
438                crate::error::Error::ElementNotFound(format!(
439                    "Element not found: {}",
440                    self.selector
441                ))
442            })?;
443
444        // Delegate to ElementHandle.screenshot()
445        element.screenshot(options).await
446    }
447}
448
449impl std::fmt::Debug for Locator {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        f.debug_struct("Locator")
452            .field("selector", &self.selector)
453            .finish()
454    }
455}