1use std::time::Duration;
4
5use viewpoint_cdp::protocol::input::{
6 DispatchKeyEventParams, DispatchMouseEventParams, InsertTextParams, MouseButton,
7};
8use viewpoint_cdp::protocol::runtime::EvaluateParams;
9use serde::Deserialize;
10use tracing::{debug, instrument};
11
12use super::selector::js_string_literal;
13use super::Locator;
14use crate::error::LocatorError;
15
16#[derive(Debug, Clone, Deserialize)]
18#[serde(rename_all = "camelCase")]
19#[allow(dead_code)] struct ElementInfo {
21 found: bool,
23 count: usize,
25 visible: Option<bool>,
27 enabled: Option<bool>,
29 x: Option<f64>,
31 y: Option<f64>,
32 width: Option<f64>,
33 height: Option<f64>,
34 text: Option<String>,
36 tag_name: Option<String>,
38}
39
40impl Locator<'_> {
41 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
57 pub async fn click(&self) -> Result<(), LocatorError> {
58 let info = self.wait_for_actionable().await?;
59
60 let x = info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0;
63 let y = info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0;
64
65 debug!(x, y, "Clicking element");
66
67 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_move(x, y))
69 .await?;
70
71 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_down(x, y, MouseButton::Left))
73 .await?;
74
75 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_up(x, y, MouseButton::Left))
77 .await?;
78
79 Ok(())
80 }
81
82 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
93 pub async fn dblclick(&self) -> Result<(), LocatorError> {
94 let info = self.wait_for_actionable().await?;
95
96 let x = info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0;
97 let y = info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0;
98
99 debug!(x, y, "Double-clicking element");
100
101 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_move(x, y))
103 .await?;
104 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_down(x, y, MouseButton::Left))
105 .await?;
106 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_up(x, y, MouseButton::Left))
107 .await?;
108
109 let mut down = DispatchMouseEventParams::mouse_down(x, y, MouseButton::Left);
111 down.click_count = Some(2);
112 self.dispatch_mouse_event(down).await?;
113
114 let mut up = DispatchMouseEventParams::mouse_up(x, y, MouseButton::Left);
115 up.click_count = Some(2);
116 self.dispatch_mouse_event(up).await?;
117
118 Ok(())
119 }
120
121 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
129 pub async fn fill(&self, text: &str) -> Result<(), LocatorError> {
130 let _info = self.wait_for_actionable().await?;
131
132 debug!(text, "Filling element");
133
134 self.focus_element().await?;
136
137 self.dispatch_key_event(DispatchKeyEventParams::key_down("a"))
139 .await?;
140 let mut select_all = DispatchKeyEventParams::key_down("a");
142 select_all.modifiers = Some(viewpoint_cdp::protocol::input::modifiers::CTRL);
143 self.dispatch_key_event(select_all).await?;
144
145 self.dispatch_key_event(DispatchKeyEventParams::key_down("Backspace"))
147 .await?;
148
149 self.insert_text(text).await?;
151
152 Ok(())
153 }
154
155 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
163 pub async fn type_text(&self, text: &str) -> Result<(), LocatorError> {
164 self.wait_for_actionable().await?;
165
166 debug!(text, "Typing text");
167
168 self.focus_element().await?;
170
171 for ch in text.chars() {
173 let char_str = ch.to_string();
174 self.dispatch_key_event(DispatchKeyEventParams::char(&char_str))
175 .await?;
176 }
177
178 Ok(())
179 }
180
181 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
189 pub async fn press(&self, key: &str) -> Result<(), LocatorError> {
190 self.wait_for_actionable().await?;
191
192 debug!(key, "Pressing key");
193
194 self.focus_element().await?;
196
197 let parts: Vec<&str> = key.split('+').collect();
199 let actual_key = parts.last().unwrap_or(&key);
200
201 let mut modifiers = 0;
202 for part in &parts[..parts.len().saturating_sub(1)] {
203 match part.to_lowercase().as_str() {
204 "control" | "ctrl" => {
205 modifiers |= viewpoint_cdp::protocol::input::modifiers::CTRL;
206 }
207 "alt" => modifiers |= viewpoint_cdp::protocol::input::modifiers::ALT,
208 "shift" => modifiers |= viewpoint_cdp::protocol::input::modifiers::SHIFT,
209 "meta" | "cmd" => modifiers |= viewpoint_cdp::protocol::input::modifiers::META,
210 _ => {}
211 }
212 }
213
214 let mut key_down = DispatchKeyEventParams::key_down(actual_key);
216 if modifiers != 0 {
217 key_down.modifiers = Some(modifiers);
218 }
219 self.dispatch_key_event(key_down).await?;
220
221 let mut key_up = DispatchKeyEventParams::key_up(actual_key);
223 if modifiers != 0 {
224 key_up.modifiers = Some(modifiers);
225 }
226 self.dispatch_key_event(key_up).await?;
227
228 Ok(())
229 }
230
231 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
242 pub async fn hover(&self) -> Result<(), LocatorError> {
243 let info = self.wait_for_actionable().await?;
244
245 let x = info.x.expect("visible element has x") + info.width.expect("visible element has width") / 2.0;
246 let y = info.y.expect("visible element has y") + info.height.expect("visible element has height") / 2.0;
247
248 debug!(x, y, "Hovering over element");
249
250 self.dispatch_mouse_event(DispatchMouseEventParams::mouse_move(x, y))
251 .await?;
252
253 Ok(())
254 }
255
256 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
262 pub async fn focus(&self) -> Result<(), LocatorError> {
263 self.wait_for_actionable().await?;
264
265 debug!("Focusing element");
266 self.focus_element().await?;
267
268 Ok(())
269 }
270
271 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
277 pub async fn clear(&self) -> Result<(), LocatorError> {
278 self.wait_for_actionable().await?;
279
280 debug!("Clearing element");
281
282 self.focus_element().await?;
284
285 let mut select_all = DispatchKeyEventParams::key_down("a");
286 select_all.modifiers = Some(viewpoint_cdp::protocol::input::modifiers::CTRL);
287 self.dispatch_key_event(select_all).await?;
288
289 self.dispatch_key_event(DispatchKeyEventParams::key_down("Backspace"))
290 .await?;
291
292 Ok(())
293 }
294
295 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
301 pub async fn check(&self) -> Result<(), LocatorError> {
302 let is_checked = self.is_checked().await?;
303
304 if is_checked {
305 debug!("Element already checked");
306 } else {
307 debug!("Checking element");
308 self.click().await?;
309 }
310
311 Ok(())
312 }
313
314 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
320 pub async fn uncheck(&self) -> Result<(), LocatorError> {
321 let is_checked = self.is_checked().await?;
322
323 if is_checked {
324 debug!("Unchecking element");
325 self.click().await?;
326 } else {
327 debug!("Element already unchecked");
328 }
329
330 Ok(())
331 }
332
333 #[instrument(level = "debug", skip(self), fields(selector = ?self.selector))]
355 pub async fn select_option(&self, option: &str) -> Result<(), LocatorError> {
356 self.wait_for_actionable().await?;
357
358 debug!(option, "Selecting option");
359
360 let js = format!(
362 r"(function() {{
363 const elements = {selector};
364 if (elements.length === 0) return {{ success: false, error: 'Element not found' }};
365
366 const select = elements[0];
367 if (select.tagName.toLowerCase() !== 'select') {{
368 return {{ success: false, error: 'Element is not a select' }};
369 }}
370
371 const optionValue = {option};
372
373 // Try to find by value first
374 for (let i = 0; i < select.options.length; i++) {{
375 if (select.options[i].value === optionValue) {{
376 select.selectedIndex = i;
377 select.dispatchEvent(new Event('change', {{ bubbles: true }}));
378 return {{ success: true, selectedIndex: i, selectedValue: select.options[i].value }};
379 }}
380 }}
381
382 // Try to find by text content
383 for (let i = 0; i < select.options.length; i++) {{
384 if (select.options[i].text === optionValue ||
385 select.options[i].textContent.trim() === optionValue) {{
386 select.selectedIndex = i;
387 select.dispatchEvent(new Event('change', {{ bubbles: true }}));
388 return {{ success: true, selectedIndex: i, selectedValue: select.options[i].value }};
389 }}
390 }}
391
392 return {{ success: false, error: 'Option not found: ' + optionValue }};
393 }})()",
394 selector = self.selector.to_js_expression(),
395 option = js_string_literal(option)
396 );
397
398 let result = self.evaluate_js(&js).await?;
399
400 let success = result
401 .get("success")
402 .and_then(serde_json::Value::as_bool)
403 .unwrap_or(false);
404 if !success {
405 let error = result
406 .get("error")
407 .and_then(|v| v.as_str())
408 .unwrap_or("Unknown error");
409 return Err(LocatorError::EvaluationError(error.to_string()));
410 }
411
412 Ok(())
413 }
414
415 #[instrument(level = "debug", skip(self, options), fields(selector = ?self.selector))]
425 pub async fn select_options(&self, options: &[&str]) -> Result<(), LocatorError> {
426 self.wait_for_actionable().await?;
427
428 debug!(?options, "Selecting multiple options");
429
430 let options_js: Vec<String> = options.iter().map(|o| js_string_literal(o)).collect();
432 let options_array = format!("[{}]", options_js.join(", "));
433
434 let js = format!(
435 r"(function() {{
436 const elements = {selector};
437 if (elements.length === 0) return {{ success: false, error: 'Element not found' }};
438
439 const select = elements[0];
440 if (select.tagName.toLowerCase() !== 'select') {{
441 return {{ success: false, error: 'Element is not a select' }};
442 }}
443
444 const optionValues = {options_array};
445 const selectedIndices = [];
446
447 // Clear current selection if not multiple
448 if (!select.multiple) {{
449 return {{ success: false, error: 'select_options requires a <select multiple>' }};
450 }}
451
452 // Deselect all first
453 for (let i = 0; i < select.options.length; i++) {{
454 select.options[i].selected = false;
455 }}
456
457 // Select each requested option
458 for (const optionValue of optionValues) {{
459 let found = false;
460
461 // Try to find by value
462 for (let i = 0; i < select.options.length; i++) {{
463 if (select.options[i].value === optionValue) {{
464 select.options[i].selected = true;
465 selectedIndices.push(i);
466 found = true;
467 break;
468 }}
469 }}
470
471 // Try to find by text if not found by value
472 if (!found) {{
473 for (let i = 0; i < select.options.length; i++) {{
474 if (select.options[i].text === optionValue ||
475 select.options[i].textContent.trim() === optionValue) {{
476 select.options[i].selected = true;
477 selectedIndices.push(i);
478 found = true;
479 break;
480 }}
481 }}
482 }}
483
484 if (!found) {{
485 return {{ success: false, error: 'Option not found: ' + optionValue }};
486 }}
487 }}
488
489 select.dispatchEvent(new Event('change', {{ bubbles: true }}));
490 return {{ success: true, selectedIndices: selectedIndices }};
491 }})()",
492 selector = self.selector.to_js_expression(),
493 options_array = options_array
494 );
495
496 let result = self.evaluate_js(&js).await?;
497
498 let success = result
499 .get("success")
500 .and_then(serde_json::Value::as_bool)
501 .unwrap_or(false);
502 if !success {
503 let error = result
504 .get("error")
505 .and_then(|v| v.as_str())
506 .unwrap_or("Unknown error");
507 return Err(LocatorError::EvaluationError(error.to_string()));
508 }
509
510 Ok(())
511 }
512
513 pub async fn text_content(&self) -> Result<Option<String>, LocatorError> {
519 let info = self.query_element_info().await?;
520 Ok(info.text)
521 }
522
523 pub async fn is_visible(&self) -> Result<bool, LocatorError> {
529 let info = self.query_element_info().await?;
530 Ok(info.visible.unwrap_or(false))
531 }
532
533 pub async fn is_checked(&self) -> Result<bool, LocatorError> {
539 let js = format!(
540 r"(function() {{
541 const elements = {};
542 if (elements.length === 0) return {{ found: false, checked: false }};
543 const el = elements[0];
544 return {{ found: true, checked: el.checked || false }};
545 }})()",
546 self.selector.to_js_expression()
547 );
548
549 let result = self.evaluate_js(&js).await?;
550 let checked: bool = result
551 .get("checked")
552 .and_then(serde_json::Value::as_bool)
553 .unwrap_or(false);
554 Ok(checked)
555 }
556
557 pub async fn count(&self) -> Result<usize, LocatorError> {
563 let info = self.query_element_info().await?;
564 Ok(info.count)
565 }
566
567 async fn wait_for_actionable(&self) -> Result<ElementInfo, LocatorError> {
573 let start = std::time::Instant::now();
574 let timeout = self.options.timeout;
575
576 loop {
577 let info = self.query_element_info().await?;
578
579 if !info.found {
580 if start.elapsed() >= timeout {
581 return Err(LocatorError::NotFound(format!("{:?}", self.selector)));
582 }
583 tokio::time::sleep(Duration::from_millis(100)).await;
584 continue;
585 }
586
587 if !info.visible.unwrap_or(false) {
588 if start.elapsed() >= timeout {
589 return Err(LocatorError::NotVisible);
590 }
591 tokio::time::sleep(Duration::from_millis(100)).await;
592 continue;
593 }
594
595 return Ok(info);
597 }
598 }
599
600 async fn query_element_info(&self) -> Result<ElementInfo, LocatorError> {
602 let js = format!(
603 r"(function() {{
604 const elements = Array.from({});
605 if (elements.length === 0) {{
606 return {{ found: false, count: 0 }};
607 }}
608 const el = elements[0];
609 const rect = el.getBoundingClientRect();
610 const style = window.getComputedStyle(el);
611 const visible = rect.width > 0 && rect.height > 0 &&
612 style.visibility !== 'hidden' &&
613 style.display !== 'none' &&
614 parseFloat(style.opacity) > 0;
615 return {{
616 found: true,
617 count: elements.length,
618 visible: visible,
619 enabled: !el.disabled,
620 x: rect.x,
621 y: rect.y,
622 width: rect.width,
623 height: rect.height,
624 text: el.textContent,
625 tagName: el.tagName.toLowerCase()
626 }};
627 }})()",
628 self.selector.to_js_expression()
629 );
630
631 let result = self.evaluate_js(&js).await?;
632 let info: ElementInfo = serde_json::from_value(result)
633 .map_err(|e| LocatorError::EvaluationError(e.to_string()))?;
634 Ok(info)
635 }
636
637 async fn focus_element(&self) -> Result<(), LocatorError> {
639 let js = format!(
640 r"(function() {{
641 const elements = {};
642 if (elements.length > 0) {{
643 elements[0].focus();
644 return true;
645 }}
646 return false;
647 }})()",
648 self.selector.to_js_expression()
649 );
650
651 self.evaluate_js(&js).await?;
652 Ok(())
653 }
654
655 async fn evaluate_js(&self, expression: &str) -> Result<serde_json::Value, LocatorError> {
657 if self.page.is_closed() {
658 return Err(LocatorError::PageClosed);
659 }
660
661 let params = EvaluateParams {
662 expression: expression.to_string(),
663 object_group: None,
664 include_command_line_api: None,
665 silent: Some(true),
666 context_id: None,
667 return_by_value: Some(true),
668 await_promise: Some(false),
669 };
670
671 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
672 .page
673 .connection()
674 .send_command("Runtime.evaluate", Some(params), Some(self.page.session_id()))
675 .await?;
676
677 if let Some(exception) = result.exception_details {
678 return Err(LocatorError::EvaluationError(exception.text));
679 }
680
681 result
682 .result
683 .value
684 .ok_or_else(|| LocatorError::EvaluationError("No result value".to_string()))
685 }
686
687 async fn dispatch_mouse_event(
689 &self,
690 params: DispatchMouseEventParams,
691 ) -> Result<(), LocatorError> {
692 self.page
693 .connection()
694 .send_command::<_, serde_json::Value>(
695 "Input.dispatchMouseEvent",
696 Some(params),
697 Some(self.page.session_id()),
698 )
699 .await?;
700 Ok(())
701 }
702
703 async fn dispatch_key_event(
705 &self,
706 params: DispatchKeyEventParams,
707 ) -> Result<(), LocatorError> {
708 self.page
709 .connection()
710 .send_command::<_, serde_json::Value>(
711 "Input.dispatchKeyEvent",
712 Some(params),
713 Some(self.page.session_id()),
714 )
715 .await?;
716 Ok(())
717 }
718
719 async fn insert_text(&self, text: &str) -> Result<(), LocatorError> {
721 self.page
722 .connection()
723 .send_command::<_, serde_json::Value>(
724 "Input.insertText",
725 Some(InsertTextParams {
726 text: text.to_string(),
727 }),
728 Some(self.page.session_id()),
729 )
730 .await?;
731 Ok(())
732 }
733}