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