viewpoint_core/page/mouse_drag/
mod.rs

1//! Drag and drop operations for mouse.
2
3use tracing::{debug, instrument};
4
5use super::Page;
6use crate::error::LocatorError;
7
8/// Builder for drag and drop operations.
9///
10/// Created via [`Page::drag_and_drop`].
11///
12/// # Example
13///
14/// ```ignore
15/// // Simple drag and drop
16/// page.drag_and_drop("#source", "#target").send().await?;
17///
18/// // With position options
19/// page.drag_and_drop("#source", "#target")
20///     .source_position(10.0, 10.0)
21///     .target_position(5.0, 5.0)
22///     .send()
23///     .await?;
24/// ```
25#[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    /// Create a new drag and drop builder.
37    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    /// Set the position within the source element to start dragging from.
49    ///
50    /// Coordinates are relative to the element's top-left corner.
51    #[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    /// Set the position within the target element to drop at.
58    ///
59    /// Coordinates are relative to the element's top-left corner.
60    #[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    /// Set the number of intermediate steps for smooth dragging.
67    #[must_use]
68    pub fn steps(mut self, steps: u32) -> Self {
69        self.steps = steps.max(1);
70        self
71    }
72
73    /// Execute the drag and drop operation.
74    #[instrument(level = "debug", skip(self), fields(source = %self.source, target = %self.target))]
75    pub async fn send(self) -> Result<(), LocatorError> {
76        // Get source element bounding box
77        let source_box = self.get_element_box(&self.source).await?;
78
79        // Get target element bounding box
80        let target_box = self.get_element_box(&self.target).await?;
81
82        // Calculate source coordinates
83        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            // Use center
87            (
88                source_box.0 + source_box.2 / 2.0,
89                source_box.1 + source_box.3 / 2.0,
90            )
91        };
92
93        // Calculate target coordinates
94        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            // Use center
98            (
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        // Perform drag operation
110        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    /// Get the bounding box of an element (x, y, width, height).
124    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    /// Evaluate JavaScript and return the result.
162    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}