viewpoint_core/page/locator/actions/mod.rs
1//! Locator actions for element interaction.
2
3use viewpoint_cdp::protocol::input::{DispatchKeyEventParams, DispatchMouseEventParams, MouseButton};
4use tracing::{debug, instrument};
5
6use super::builders::{ClickBuilder, HoverBuilder, TapBuilder, TypeBuilder};
7use super::Locator;
8use crate::error::LocatorError;
9
10impl<'a> Locator<'a> {
11 /// Click the element.
12 ///
13 /// Returns a builder that can be configured with additional options, or awaited
14 /// directly for a simple click.
15 ///
16 /// # Examples
17 ///
18 /// ```no_run
19 /// use viewpoint_core::Page;
20 /// use viewpoint_cdp::protocol::input::{MouseButton, modifiers};
21 ///
22 /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
23 /// // Simple click - await directly
24 /// page.locator("button").click().await?;
25 ///
26 /// // Click with options
27 /// page.locator("button").click()
28 /// .position(10.0, 5.0)
29 /// .button(MouseButton::Right)
30 /// .modifiers(modifiers::SHIFT)
31 /// .send().await?;
32 ///
33 /// // Force click without waiting for actionability
34 /// page.locator("button").click().force(true).await?;
35 /// # Ok(())
36 /// # }
37 /// ```
38 ///
39 /// # Options
40 ///
41 /// - [`ClickBuilder::position`] - Click at offset from element's top-left corner
42 /// - [`ClickBuilder::button`] - Use a different mouse button (right, middle)
43 /// - [`ClickBuilder::modifiers`] - Hold modifier keys (Shift, Ctrl, Alt)
44 /// - [`ClickBuilder::force`] - Skip actionability checks
45 pub fn click(&self) -> ClickBuilder<'_, 'a> {
46 ClickBuilder::new(self)
47 }
48
49 /// Double-click the element.
50 ///
51 /// # Errors
52 ///
53 /// Returns an error if the element cannot be clicked.
54 ///
55 /// # Panics
56 ///
57 /// Panics if a visible element lacks bounding box coordinates. This should
58 /// never occur as `wait_for_actionable` ensures visibility before returning.
59 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
60 pub async fn dblclick(&self) -> Result<(), LocatorError> {
61 let info = self.wait_for_actionable().await?;
62
63 let x = info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0;
64 let y = info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0;
65
66 debug!(x, y, "Double-clicking element");
67
68 // First click
69 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_move(x, y))
70 .await?;
71 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_down(x, y, MouseButton::Left))
72 .await?;
73 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_up(x, y, MouseButton::Left))
74 .await?;
75
76 // Second click
77 let mut down = DispatchMouseEventParams::mouse_down(x, y, MouseButton::Left);
78 down.click_count = Some(2);
79 self.dispatch_mouse_event(down).await?;
80
81 let mut up = DispatchMouseEventParams::mouse_up(x, y, MouseButton::Left);
82 up.click_count = Some(2);
83 self.dispatch_mouse_event(up).await?;
84
85 Ok(())
86 }
87
88 /// Fill the element with text (clears existing content first).
89 ///
90 /// This is for input and textarea elements.
91 ///
92 /// # Errors
93 ///
94 /// Returns an error if the element cannot be focused or text cannot be inserted.
95 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
96 pub async fn fill(&self, text: &str) -> Result<(), LocatorError> {
97 let _info = self.wait_for_actionable().await?;
98
99 debug!(text, "Filling element");
100
101 // Focus the element
102 self.focus_element().await?;
103
104 // Select all and delete (clear)
105 self.dispatch_key_event(DispatchKeyEventParams::key_down("a"))
106 .await?;
107 // Send Ctrl+A
108 let mut select_all = DispatchKeyEventParams::key_down("a");
109 select_all.modifiers = Some(viewpoint_cdp::protocol::input::modifiers::CTRL);
110 self.dispatch_key_event(select_all).await?;
111
112 // Delete selected text
113 self.dispatch_key_event(DispatchKeyEventParams::key_down("Backspace"))
114 .await?;
115
116 // Insert the new text
117 self.insert_text(text).await?;
118
119 Ok(())
120 }
121
122 /// Type text character by character.
123 ///
124 /// Unlike `fill`, this types each character with keydown/keyup events.
125 /// Returns a builder that can be configured with additional options, or awaited
126 /// directly for simple typing.
127 ///
128 /// # Examples
129 ///
130 /// ```no_run
131 /// use std::time::Duration;
132 /// use viewpoint_core::Page;
133 ///
134 /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
135 /// // Simple type - await directly
136 /// page.locator("input").type_text("hello").await?;
137 ///
138 /// // Type with delay between characters
139 /// page.locator("input").type_text("hello")
140 /// .delay(Duration::from_millis(100))
141 /// .send().await?;
142 /// # Ok(())
143 /// # }
144 /// ```
145 ///
146 /// # Options
147 ///
148 /// - [`TypeBuilder::delay`] - Add delay between character keystrokes
149 pub fn type_text(&self, text: &str) -> TypeBuilder<'_, 'a> {
150 TypeBuilder::new(self, text)
151 }
152
153 /// Press a key or key combination.
154 ///
155 /// Examples: "Enter", "Backspace", "Control+a", "Shift+Tab"
156 ///
157 /// # Errors
158 ///
159 /// Returns an error if the element cannot be focused or the key cannot be pressed.
160 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
161 pub async fn press(&self, key: &str) -> Result<(), LocatorError> {
162 self.wait_for_actionable().await?;
163
164 debug!(key, "Pressing key");
165
166 // Focus the element
167 self.focus_element().await?;
168
169 // Parse modifiers and key
170 let parts: Vec<&str> = key.split('+').collect();
171 let actual_key = parts.last().unwrap_or(&key);
172
173 let mut modifiers = 0;
174 for part in &parts[..parts.len().saturating_sub(1)] {
175 match part.to_lowercase().as_str() {
176 "control" | "ctrl" => {
177 modifiers |= viewpoint_cdp::protocol::input::modifiers::CTRL;
178 }
179 "alt" => modifiers |= viewpoint_cdp::protocol::input::modifiers::ALT,
180 "shift" => modifiers |= viewpoint_cdp::protocol::input::modifiers::SHIFT,
181 "meta" | "cmd" => modifiers |= viewpoint_cdp::protocol::input::modifiers::META,
182 _ => {}
183 }
184 }
185
186 // Key down
187 let mut key_down = DispatchKeyEventParams::key_down(actual_key);
188 if modifiers != 0 {
189 key_down.modifiers = Some(modifiers);
190 }
191 self.dispatch_key_event(key_down).await?;
192
193 // Key up
194 let mut key_up = DispatchKeyEventParams::key_up(actual_key);
195 if modifiers != 0 {
196 key_up.modifiers = Some(modifiers);
197 }
198 self.dispatch_key_event(key_up).await?;
199
200 Ok(())
201 }
202
203 /// Hover over the element.
204 ///
205 /// Returns a builder that can be configured with additional options, or awaited
206 /// directly for a simple hover.
207 ///
208 /// # Examples
209 ///
210 /// ```no_run
211 /// use viewpoint_core::Page;
212 ///
213 /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
214 /// // Simple hover - await directly
215 /// page.locator("button").hover().await?;
216 ///
217 /// // Hover with position offset
218 /// page.locator("button").hover()
219 /// .position(10.0, 5.0)
220 /// .send().await?;
221 ///
222 /// // Force hover without waiting for actionability
223 /// page.locator("button").hover().force(true).await?;
224 /// # Ok(())
225 /// # }
226 /// ```
227 ///
228 /// # Options
229 ///
230 /// - [`HoverBuilder::position`] - Hover at offset from element's top-left corner
231 /// - [`HoverBuilder::modifiers`] - Hold modifier keys during hover
232 /// - [`HoverBuilder::force`] - Skip actionability checks
233 pub fn hover(&self) -> HoverBuilder<'_, 'a> {
234 HoverBuilder::new(self)
235 }
236
237 /// Focus the element.
238 ///
239 /// # Errors
240 ///
241 /// Returns an error if the element cannot be found or focused.
242 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
243 pub async fn focus(&self) -> Result<(), LocatorError> {
244 self.wait_for_actionable().await?;
245
246 debug!("Focusing element");
247 self.focus_element().await?;
248
249 Ok(())
250 }
251
252 /// Clear the element's content.
253 ///
254 /// # Errors
255 ///
256 /// Returns an error if the element cannot be cleared.
257 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
258 pub async fn clear(&self) -> Result<(), LocatorError> {
259 self.wait_for_actionable().await?;
260
261 debug!("Clearing element");
262
263 // Focus and select all, then delete
264 self.focus_element().await?;
265
266 let mut select_all = DispatchKeyEventParams::key_down("a");
267 select_all.modifiers = Some(viewpoint_cdp::protocol::input::modifiers::CTRL);
268 self.dispatch_key_event(select_all).await?;
269
270 self.dispatch_key_event(DispatchKeyEventParams::key_down("Backspace"))
271 .await?;
272
273 Ok(())
274 }
275
276 /// Check a checkbox or radio button.
277 ///
278 /// # Errors
279 ///
280 /// Returns an error if the element cannot be checked.
281 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
282 pub async fn check(&self) -> Result<(), LocatorError> {
283 let is_checked = self.is_checked().await?;
284
285 if is_checked {
286 debug!("Element already checked");
287 } else {
288 debug!("Checking element");
289 self.click().await?;
290 }
291
292 Ok(())
293 }
294
295 /// Uncheck a checkbox.
296 ///
297 /// # Errors
298 ///
299 /// Returns an error if the element cannot be unchecked.
300 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
301 pub async fn uncheck(&self) -> Result<(), LocatorError> {
302 let is_checked = self.is_checked().await?;
303
304 if is_checked {
305 debug!("Unchecking element");
306 self.click().await?;
307 } else {
308 debug!("Element already unchecked");
309 }
310
311 Ok(())
312 }
313
314 /// Tap on the element (touch event).
315 ///
316 /// Requires touch to be enabled via `page.touchscreen().enable()`.
317 ///
318 /// Returns a builder to configure tap options.
319 ///
320 /// # Example
321 ///
322 /// ```no_run
323 /// use viewpoint_core::Page;
324 /// use viewpoint_cdp::protocol::input::modifiers;
325 ///
326 /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
327 /// // Simple tap
328 /// page.locator("button").tap().send().await?;
329 ///
330 /// // Tap with position offset
331 /// page.locator("button").tap().position(10.0, 5.0).send().await?;
332 ///
333 /// // Tap with modifiers
334 /// page.locator("button").tap().modifiers(modifiers::SHIFT).send().await?;
335 ///
336 /// // Force tap without waiting for actionability
337 /// page.locator("button").tap().force(true).send().await?;
338 /// # Ok(())
339 /// # }
340 /// ```
341 pub fn tap(&self) -> TapBuilder<'_, 'a> {
342 TapBuilder::new(self)
343 }
344
345 /// Drag this element to another locator.
346 ///
347 /// # Arguments
348 ///
349 /// * `target` - The target locator to drag to.
350 ///
351 /// # Example
352 ///
353 /// ```no_run
354 /// use viewpoint_core::Page;
355 ///
356 /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
357 /// let source = page.locator("#draggable");
358 /// let target = page.locator("#droppable");
359 /// source.drag_to(&target).await?;
360 /// # Ok(())
361 /// # }
362 /// ```
363 #[instrument(level = "debug", skip(self, target), fields(selector = ?self.selector))]
364 pub async fn drag_to(&self, target: &Locator<'_>) -> Result<(), LocatorError> {
365 self.drag_to_with_options(target, None, None, 1).await
366 }
367
368 /// Drag this element to another locator with options.
369 ///
370 /// # Arguments
371 ///
372 /// * `target` - The target locator to drag to.
373 /// * `source_position` - Optional offset from source element's top-left corner.
374 /// * `target_position` - Optional offset from target element's top-left corner.
375 /// * `steps` - Number of intermediate steps for smooth dragging.
376 #[instrument(level = "debug", skip(self, target))]
377 pub async fn drag_to_with_options(
378 &self,
379 target: &Locator<'_>,
380 source_position: Option<(f64, f64)>,
381 target_position: Option<(f64, f64)>,
382 steps: u32,
383 ) -> Result<(), LocatorError> {
384 // Get source element info
385 let source_info = self.wait_for_actionable().await?;
386 let (source_x, source_y) = if let Some((ox, oy)) = source_position {
387 (source_info.x.expect("x") + ox, source_info.y.expect("y") + oy)
388 } else {
389 (
390 source_info.x.expect("x") + source_info.width.expect("width") / 2.0,
391 source_info.y.expect("y") + source_info.height.expect("height") / 2.0,
392 )
393 };
394
395 // Get target element info
396 let target_info = target.wait_for_actionable().await?;
397 let (target_x, target_y) = if let Some((ox, oy)) = target_position {
398 (target_info.x.expect("x") + ox, target_info.y.expect("y") + oy)
399 } else {
400 (
401 target_info.x.expect("x") + target_info.width.expect("width") / 2.0,
402 target_info.y.expect("y") + target_info.height.expect("height") / 2.0,
403 )
404 };
405
406 debug!(
407 "Dragging from ({}, {}) to ({}, {})",
408 source_x, source_y, target_x, target_y
409 );
410
411 // Perform drag operation
412 self.page.mouse().move_(source_x, source_y).send().await?;
413 self.page.mouse().down().send().await?;
414 self.page.mouse().move_(target_x, target_y).steps(steps).send().await?;
415 self.page.mouse().up().send().await?;
416
417 Ok(())
418 }
419
420 /// Take a screenshot of this element.
421 ///
422 /// Returns a builder to configure screenshot options.
423 ///
424 /// # Example
425 ///
426 /// ```no_run
427 /// use viewpoint_core::Page;
428 ///
429 /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
430 /// // Capture element screenshot
431 /// let bytes = page.locator("button").screenshot().capture().await?;
432 ///
433 /// // Capture and save to file
434 /// page.locator("button")
435 /// .screenshot()
436 /// .path("button.png")
437 /// .capture()
438 /// .await?;
439 /// # Ok(())
440 /// # }
441 /// ```
442 pub fn screenshot(&self) -> crate::page::screenshot_element::ElementScreenshotBuilder<'_, '_> {
443 crate::page::screenshot_element::ElementScreenshotBuilder::new(self)
444 }
445}