viewpoint_core/page/mouse_drag/
mod.rs

1//! Drag and drop operations for mouse.
2
3use tracing::{debug, instrument};
4use viewpoint_js::js;
5
6use super::Page;
7use crate::error::LocatorError;
8
9/// Builder for drag and drop operations.
10///
11/// Created via [`Page::drag_and_drop`].
12///
13/// # Example
14///
15/// ```
16/// # #[cfg(feature = "integration")]
17/// # tokio_test::block_on(async {
18/// # use viewpoint_core::Browser;
19/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
20/// # let context = browser.new_context().await.unwrap();
21/// # let page = context.new_page().await.unwrap();
22/// # page.goto("about:blank").goto().await.unwrap();
23///
24/// // Simple drag and drop
25/// page.drag_and_drop("#source", "#target").send().await.ok();
26///
27/// // With position options
28/// page.drag_and_drop("#source", "#target")
29///     .source_position(10.0, 10.0)
30///     .target_position(5.0, 5.0)
31///     .send()
32///     .await.ok();
33/// # });
34/// ```
35#[derive(Debug)]
36pub struct DragAndDropBuilder<'a> {
37    page: &'a Page,
38    source: String,
39    target: String,
40    source_position: Option<(f64, f64)>,
41    target_position: Option<(f64, f64)>,
42    steps: u32,
43}
44
45impl<'a> DragAndDropBuilder<'a> {
46    /// Create a new drag and drop builder.
47    pub(crate) fn new(page: &'a Page, source: String, target: String) -> Self {
48        Self {
49            page,
50            source,
51            target,
52            source_position: None,
53            target_position: None,
54            steps: 1,
55        }
56    }
57
58    /// Set the position within the source element to start dragging from.
59    ///
60    /// Coordinates are relative to the element's top-left corner.
61    #[must_use]
62    pub fn source_position(mut self, x: f64, y: f64) -> Self {
63        self.source_position = Some((x, y));
64        self
65    }
66
67    /// Set the position within the target element to drop at.
68    ///
69    /// Coordinates are relative to the element's top-left corner.
70    #[must_use]
71    pub fn target_position(mut self, x: f64, y: f64) -> Self {
72        self.target_position = Some((x, y));
73        self
74    }
75
76    /// Set the number of intermediate steps for smooth dragging.
77    #[must_use]
78    pub fn steps(mut self, steps: u32) -> Self {
79        self.steps = steps.max(1);
80        self
81    }
82
83    /// Execute the drag and drop operation.
84    #[instrument(level = "debug", skip(self), fields(source = %self.source, target = %self.target))]
85    pub async fn send(self) -> Result<(), LocatorError> {
86        // Get source element bounding box
87        let source_box = self.get_element_box(&self.source).await?;
88
89        // Get target element bounding box
90        let target_box = self.get_element_box(&self.target).await?;
91
92        // Calculate source coordinates
93        let (source_x, source_y) = if let Some((ox, oy)) = self.source_position {
94            (source_box.0 + ox, source_box.1 + oy)
95        } else {
96            // Use center
97            (
98                source_box.0 + source_box.2 / 2.0,
99                source_box.1 + source_box.3 / 2.0,
100            )
101        };
102
103        // Calculate target coordinates
104        let (target_x, target_y) = if let Some((ox, oy)) = self.target_position {
105            (target_box.0 + ox, target_box.1 + oy)
106        } else {
107            // Use center
108            (
109                target_box.0 + target_box.2 / 2.0,
110                target_box.1 + target_box.3 / 2.0,
111            )
112        };
113
114        debug!(
115            "Dragging from ({}, {}) to ({}, {})",
116            source_x, source_y, target_x, target_y
117        );
118
119        // Perform drag operation
120        self.page.mouse().move_(source_x, source_y).send().await?;
121        self.page.mouse().down().send().await?;
122        self.page
123            .mouse()
124            .move_(target_x, target_y)
125            .steps(self.steps)
126            .send()
127            .await?;
128        self.page.mouse().up().send().await?;
129
130        Ok(())
131    }
132
133    /// Get the bounding box of an element (x, y, width, height).
134    async fn get_element_box(&self, selector: &str) -> Result<(f64, f64, f64, f64), LocatorError> {
135        let js_code = js! {
136            (function() {
137                const el = document.querySelector(#{selector});
138                if (!el) return null;
139                const rect = el.getBoundingClientRect();
140                return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
141            })()
142        };
143
144        let result = self.evaluate_js(&js_code).await?;
145
146        if result.is_null() {
147            return Err(LocatorError::NotFound(selector.to_string()));
148        }
149
150        let x = result
151            .get("x")
152            .and_then(serde_json::Value::as_f64)
153            .unwrap_or(0.0);
154        let y = result
155            .get("y")
156            .and_then(serde_json::Value::as_f64)
157            .unwrap_or(0.0);
158        let width = result
159            .get("width")
160            .and_then(serde_json::Value::as_f64)
161            .unwrap_or(0.0);
162        let height = result
163            .get("height")
164            .and_then(serde_json::Value::as_f64)
165            .unwrap_or(0.0);
166
167        Ok((x, y, width, height))
168    }
169
170    /// Evaluate JavaScript and return the result.
171    ///
172    /// Delegates to `Page::evaluate_js_raw` for the actual evaluation.
173    async fn evaluate_js(&self, expression: &str) -> Result<serde_json::Value, LocatorError> {
174        if self.page.is_closed() {
175            return Err(LocatorError::PageClosed);
176        }
177
178        self.page
179            .evaluate_js_raw(expression)
180            .await
181            .map_err(|e| LocatorError::EvaluationError(e.to_string()))
182    }
183}