viewpoint_core/page/mouse_drag/
mod.rs1use tracing::{debug, instrument};
4
5use super::Page;
6use crate::error::LocatorError;
7
8#[derive(Debug)]
26pub struct DragAndDropBuilder<'a> {
27 page: &'a Page,
28 source: String,
29 target: String,
30 source_position: Option<(f64, f64)>,
31 target_position: Option<(f64, f64)>,
32 steps: u32,
33}
34
35impl<'a> DragAndDropBuilder<'a> {
36 pub(crate) fn new(page: &'a Page, source: String, target: String) -> Self {
38 Self {
39 page,
40 source,
41 target,
42 source_position: None,
43 target_position: None,
44 steps: 1,
45 }
46 }
47
48 #[must_use]
52 pub fn source_position(mut self, x: f64, y: f64) -> Self {
53 self.source_position = Some((x, y));
54 self
55 }
56
57 #[must_use]
61 pub fn target_position(mut self, x: f64, y: f64) -> Self {
62 self.target_position = Some((x, y));
63 self
64 }
65
66 #[must_use]
68 pub fn steps(mut self, steps: u32) -> Self {
69 self.steps = steps.max(1);
70 self
71 }
72
73 #[instrument(level = "debug", skip(self), fields(source = %self.source, target = %self.target))]
75 pub async fn send(self) -> Result<(), LocatorError> {
76 let source_box = self.get_element_box(&self.source).await?;
78
79 let target_box = self.get_element_box(&self.target).await?;
81
82 let (source_x, source_y) = if let Some((ox, oy)) = self.source_position {
84 (source_box.0 + ox, source_box.1 + oy)
85 } else {
86 (
88 source_box.0 + source_box.2 / 2.0,
89 source_box.1 + source_box.3 / 2.0,
90 )
91 };
92
93 let (target_x, target_y) = if let Some((ox, oy)) = self.target_position {
95 (target_box.0 + ox, target_box.1 + oy)
96 } else {
97 (
99 target_box.0 + target_box.2 / 2.0,
100 target_box.1 + target_box.3 / 2.0,
101 )
102 };
103
104 debug!(
105 "Dragging from ({}, {}) to ({}, {})",
106 source_x, source_y, target_x, target_y
107 );
108
109 self.page.mouse().move_(source_x, source_y).send().await?;
111 self.page.mouse().down().send().await?;
112 self.page
113 .mouse()
114 .move_(target_x, target_y)
115 .steps(self.steps)
116 .send()
117 .await?;
118 self.page.mouse().up().send().await?;
119
120 Ok(())
121 }
122
123 async fn get_element_box(&self, selector: &str) -> Result<(f64, f64, f64, f64), LocatorError> {
125 let js = format!(
126 r"(function() {{
127 const el = document.querySelector({selector});
128 if (!el) return null;
129 const rect = el.getBoundingClientRect();
130 return {{ x: rect.x, y: rect.y, width: rect.width, height: rect.height }};
131 }})()",
132 selector = crate::page::locator::selector::js_string_literal(selector)
133 );
134
135 let result = self.evaluate_js(&js).await?;
136
137 if result.is_null() {
138 return Err(LocatorError::NotFound(selector.to_string()));
139 }
140
141 let x = result
142 .get("x")
143 .and_then(serde_json::Value::as_f64)
144 .unwrap_or(0.0);
145 let y = result
146 .get("y")
147 .and_then(serde_json::Value::as_f64)
148 .unwrap_or(0.0);
149 let width = result
150 .get("width")
151 .and_then(serde_json::Value::as_f64)
152 .unwrap_or(0.0);
153 let height = result
154 .get("height")
155 .and_then(serde_json::Value::as_f64)
156 .unwrap_or(0.0);
157
158 Ok((x, y, width, height))
159 }
160
161 async fn evaluate_js(&self, expression: &str) -> Result<serde_json::Value, LocatorError> {
163 if self.page.is_closed() {
164 return Err(LocatorError::PageClosed);
165 }
166
167 let params = viewpoint_cdp::protocol::runtime::EvaluateParams {
168 expression: expression.to_string(),
169 object_group: None,
170 include_command_line_api: None,
171 silent: Some(true),
172 context_id: None,
173 return_by_value: Some(true),
174 await_promise: Some(false),
175 };
176
177 let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
178 .page
179 .connection()
180 .send_command(
181 "Runtime.evaluate",
182 Some(params),
183 Some(self.page.session_id()),
184 )
185 .await?;
186
187 if let Some(exception) = result.exception_details {
188 return Err(LocatorError::EvaluationError(exception.text));
189 }
190
191 result
192 .result
193 .value
194 .ok_or_else(|| LocatorError::EvaluationError("No result value".to_string()))
195 }
196}