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