viewpoint_core/page/locator/builders/
mod.rs1use std::time::Duration;
7
8use viewpoint_cdp::protocol::input::{DispatchKeyEventParams, DispatchMouseEventParams, MouseButton};
9use tracing::{debug, instrument};
10
11use super::Locator;
12use crate::error::LocatorError;
13
14#[derive(Debug)]
41pub struct ClickBuilder<'l, 'a> {
42 locator: &'l Locator<'a>,
43 position: Option<(f64, f64)>,
45 button: MouseButton,
47 modifiers: i32,
49 force: bool,
51 click_count: i32,
53}
54
55impl<'l, 'a> ClickBuilder<'l, 'a> {
56 pub(crate) fn new(locator: &'l Locator<'a>) -> Self {
57 Self {
58 locator,
59 position: None,
60 button: MouseButton::Left,
61 modifiers: 0,
62 force: false,
63 click_count: 1,
64 }
65 }
66
67 #[must_use]
71 pub fn position(mut self, x: f64, y: f64) -> Self {
72 self.position = Some((x, y));
73 self
74 }
75
76 #[must_use]
78 pub fn button(mut self, button: MouseButton) -> Self {
79 self.button = button;
80 self
81 }
82
83 #[must_use]
87 pub fn modifiers(mut self, modifiers: i32) -> Self {
88 self.modifiers = modifiers;
89 self
90 }
91
92 #[must_use]
97 pub fn force(mut self, force: bool) -> Self {
98 self.force = force;
99 self
100 }
101
102 #[must_use]
104 pub(crate) fn click_count(mut self, count: i32) -> Self {
105 self.click_count = count;
106 self
107 }
108
109 #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
111 pub async fn send(self) -> Result<(), LocatorError> {
112 let (x, y) = if self.force {
113 let info = self.locator.query_element_info().await?;
114 if !info.found {
115 return Err(LocatorError::NotFound(format!("{:?}", self.locator.selector)));
116 }
117
118 if let Some((offset_x, offset_y)) = self.position {
119 (info.x.unwrap_or(0.0) + offset_x, info.y.unwrap_or(0.0) + offset_y)
120 } else {
121 (
122 info.x.unwrap_or(0.0) + info.width.unwrap_or(0.0) / 2.0,
123 info.y.unwrap_or(0.0) + info.height.unwrap_or(0.0) / 2.0,
124 )
125 }
126 } else {
127 let info = self.locator.wait_for_actionable().await?;
128
129 if let Some((offset_x, offset_y)) = self.position {
130 (
131 info.x.expect("visible element has x") + offset_x,
132 info.y.expect("visible element has y") + offset_y,
133 )
134 } else {
135 (
136 info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0,
137 info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0,
138 )
139 }
140 };
141
142 debug!(x, y, button = ?self.button, modifiers = self.modifiers, click_count = self.click_count, "Clicking element");
143
144 let mut move_event = DispatchMouseEventParams::mouse_move(x, y);
146 if self.modifiers != 0 {
147 move_event.modifiers = Some(self.modifiers);
148 }
149 self.locator.dispatch_mouse_event(move_event).await?;
150
151 let mut down_event = DispatchMouseEventParams::mouse_down(x, y, self.button);
153 if self.modifiers != 0 {
154 down_event.modifiers = Some(self.modifiers);
155 }
156 down_event.click_count = Some(self.click_count);
157 self.locator.dispatch_mouse_event(down_event).await?;
158
159 let mut up_event = DispatchMouseEventParams::mouse_up(x, y, self.button);
161 if self.modifiers != 0 {
162 up_event.modifiers = Some(self.modifiers);
163 }
164 up_event.click_count = Some(self.click_count);
165 self.locator.dispatch_mouse_event(up_event).await?;
166
167 Ok(())
168 }
169}
170
171impl<'l> std::future::IntoFuture for ClickBuilder<'l, '_> {
172 type Output = Result<(), LocatorError>;
173 type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'l>>;
174
175 fn into_future(self) -> Self::IntoFuture {
176 Box::pin(self.send())
177 }
178}
179
180#[derive(Debug)]
188pub struct TypeBuilder<'l, 'a> {
189 locator: &'l Locator<'a>,
190 text: String,
191 delay: Option<Duration>,
192}
193
194impl<'l, 'a> TypeBuilder<'l, 'a> {
195 pub(crate) fn new(locator: &'l Locator<'a>, text: &str) -> Self {
196 Self {
197 locator,
198 text: text.to_string(),
199 delay: None,
200 }
201 }
202
203 #[must_use]
205 pub fn delay(mut self, delay: Duration) -> Self {
206 self.delay = Some(delay);
207 self
208 }
209
210 #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
212 pub async fn send(self) -> Result<(), LocatorError> {
213 self.locator.wait_for_actionable().await?;
214
215 debug!(text = %self.text, delay = ?self.delay, "Typing text");
216
217 self.locator.focus_element().await?;
218
219 for ch in self.text.chars() {
220 let char_str = ch.to_string();
221 self.locator.dispatch_key_event(DispatchKeyEventParams::char(&char_str)).await?;
222
223 if let Some(delay) = self.delay {
224 tokio::time::sleep(delay).await;
225 }
226 }
227
228 Ok(())
229 }
230}
231
232impl<'l> std::future::IntoFuture for TypeBuilder<'l, '_> {
233 type Output = Result<(), LocatorError>;
234 type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'l>>;
235
236 fn into_future(self) -> Self::IntoFuture {
237 Box::pin(self.send())
238 }
239}
240
241#[derive(Debug)]
249pub struct HoverBuilder<'l, 'a> {
250 locator: &'l Locator<'a>,
251 position: Option<(f64, f64)>,
252 modifiers: i32,
253 force: bool,
254}
255
256impl<'l, 'a> HoverBuilder<'l, 'a> {
257 pub(crate) fn new(locator: &'l Locator<'a>) -> Self {
258 Self {
259 locator,
260 position: None,
261 modifiers: 0,
262 force: false,
263 }
264 }
265
266 #[must_use]
268 pub fn position(mut self, x: f64, y: f64) -> Self {
269 self.position = Some((x, y));
270 self
271 }
272
273 #[must_use]
275 pub fn modifiers(mut self, modifiers: i32) -> Self {
276 self.modifiers = modifiers;
277 self
278 }
279
280 #[must_use]
282 pub fn force(mut self, force: bool) -> Self {
283 self.force = force;
284 self
285 }
286
287 #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
289 pub async fn send(self) -> Result<(), LocatorError> {
290 let (x, y) = if self.force {
291 let info = self.locator.query_element_info().await?;
292 if !info.found {
293 return Err(LocatorError::NotFound(format!("{:?}", self.locator.selector)));
294 }
295
296 if let Some((offset_x, offset_y)) = self.position {
297 (info.x.unwrap_or(0.0) + offset_x, info.y.unwrap_or(0.0) + offset_y)
298 } else {
299 (
300 info.x.unwrap_or(0.0) + info.width.unwrap_or(0.0) / 2.0,
301 info.y.unwrap_or(0.0) + info.height.unwrap_or(0.0) / 2.0,
302 )
303 }
304 } else {
305 let info = self.locator.wait_for_actionable().await?;
306
307 if let Some((offset_x, offset_y)) = self.position {
308 (
309 info.x.expect("visible element has x") + offset_x,
310 info.y.expect("visible element has y") + offset_y,
311 )
312 } else {
313 (
314 info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0,
315 info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0,
316 )
317 }
318 };
319
320 debug!(x, y, modifiers = self.modifiers, "Hovering over element");
321
322 let mut move_event = DispatchMouseEventParams::mouse_move(x, y);
323 if self.modifiers != 0 {
324 move_event.modifiers = Some(self.modifiers);
325 }
326 self.locator.dispatch_mouse_event(move_event).await?;
327
328 Ok(())
329 }
330}
331
332impl<'l> std::future::IntoFuture for HoverBuilder<'l, '_> {
333 type Output = Result<(), LocatorError>;
334 type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'l>>;
335
336 fn into_future(self) -> Self::IntoFuture {
337 Box::pin(self.send())
338 }
339}
340
341#[derive(Debug)]
349pub struct TapBuilder<'l, 'a> {
350 locator: &'l Locator<'a>,
351 position: Option<(f64, f64)>,
352 force: bool,
353 modifiers: i32,
354}
355
356impl<'l, 'a> TapBuilder<'l, 'a> {
357 pub(crate) fn new(locator: &'l Locator<'a>) -> Self {
358 Self {
359 locator,
360 position: None,
361 force: false,
362 modifiers: 0,
363 }
364 }
365
366 #[must_use]
368 pub fn position(mut self, x: f64, y: f64) -> Self {
369 self.position = Some((x, y));
370 self
371 }
372
373 #[must_use]
375 pub fn force(mut self, force: bool) -> Self {
376 self.force = force;
377 self
378 }
379
380 #[must_use]
382 pub fn modifiers(mut self, modifiers: i32) -> Self {
383 self.modifiers = modifiers;
384 self
385 }
386
387 #[instrument(level = "debug", skip(self), fields(selector = ?self.locator.selector))]
389 pub async fn send(self) -> Result<(), LocatorError> {
390 let (x, y) = if self.force {
391 let info = self.locator.query_element_info().await?;
392 if !info.found {
393 return Err(LocatorError::NotFound(format!("{:?}", self.locator.selector)));
394 }
395
396 if let Some((offset_x, offset_y)) = self.position {
397 (info.x.unwrap_or(0.0) + offset_x, info.y.unwrap_or(0.0) + offset_y)
398 } else {
399 (
400 info.x.unwrap_or(0.0) + info.width.unwrap_or(0.0) / 2.0,
401 info.y.unwrap_or(0.0) + info.height.unwrap_or(0.0) / 2.0,
402 )
403 }
404 } else {
405 let info = self.locator.wait_for_actionable().await?;
406
407 if let Some((offset_x, offset_y)) = self.position {
408 (
409 info.x.expect("visible element has x") + offset_x,
410 info.y.expect("visible element has y") + offset_y,
411 )
412 } else {
413 (
414 info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0,
415 info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0,
416 )
417 }
418 };
419
420 debug!(x, y, modifiers = self.modifiers, "Tapping element");
421
422 if self.modifiers != 0 {
423 self.locator.page.touchscreen().tap_with_modifiers(x, y, self.modifiers).await
424 } else {
425 self.locator.page.touchscreen().tap(x, y).await
426 }
427 }
428}