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}