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