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