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/// ```
15/// # #[cfg(feature = "integration")]
16/// # tokio_test::block_on(async {
17/// # use viewpoint_core::Browser;
18/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
19/// # let context = browser.new_context().await.unwrap();
20/// # let page = context.new_page().await.unwrap();
21/// # page.goto("about:blank").goto().await.unwrap();
22///
23/// // Simple drag and drop
24/// page.drag_and_drop("#source", "#target").send().await.ok();
25///
26/// // With position options
27/// page.drag_and_drop("#source", "#target")
28///     .source_position(10.0, 10.0)
29///     .target_position(5.0, 5.0)
30///     .send()
31///     .await.ok();
32/// # });
33/// ```
34#[derive(Debug)]
35pub struct DragAndDropBuilder<'a> {
36    page: &'a Page,
37    source: String,
38    target: String,
39    source_position: Option<(f64, f64)>,
40    target_position: Option<(f64, f64)>,
41    steps: u32,
42}
43
44impl<'a> DragAndDropBuilder<'a> {
45    /// Create a new drag and drop builder.
46    pub(crate) fn new(page: &'a Page, source: String, target: String) -> Self {
47        Self {
48            page,
49            source,
50            target,
51            source_position: None,
52            target_position: None,
53            steps: 1,
54        }
55    }
56
57    /// Set the position within the source element to start dragging from.
58    ///
59    /// Coordinates are relative to the element's top-left corner.
60    #[must_use]
61    pub fn source_position(mut self, x: f64, y: f64) -> Self {
62        self.source_position = Some((x, y));
63        self
64    }
65
66    /// Set the position within the target element to drop at.
67    ///
68    /// Coordinates are relative to the element's top-left corner.
69    #[must_use]
70    pub fn target_position(mut self, x: f64, y: f64) -> Self {
71        self.target_position = Some((x, y));
72        self
73    }
74
75    /// Set the number of intermediate steps for smooth dragging.
76    #[must_use]
77    pub fn steps(mut self, steps: u32) -> Self {
78        self.steps = steps.max(1);
79        self
80    }
81
82    /// Execute the drag and drop operation.
83    #[instrument(level = "debug", skip(self), fields(source = %self.source, target = %self.target))]
84    pub async fn send(self) -> Result<(), LocatorError> {
85        // Get source element bounding box
86        let source_box = self.get_element_box(&self.source).await?;
87
88        // Get target element bounding box
89        let target_box = self.get_element_box(&self.target).await?;
90
91        // Calculate source coordinates
92        let (source_x, source_y) = if let Some((ox, oy)) = self.source_position {
93            (source_box.0 + ox, source_box.1 + oy)
94        } else {
95            // Use center
96            (
97                source_box.0 + source_box.2 / 2.0,
98                source_box.1 + source_box.3 / 2.0,
99            )
100        };
101
102        // Calculate target coordinates
103        let (target_x, target_y) = if let Some((ox, oy)) = self.target_position {
104            (target_box.0 + ox, target_box.1 + oy)
105        } else {
106            // Use center
107            (
108                target_box.0 + target_box.2 / 2.0,
109                target_box.1 + target_box.3 / 2.0,
110            )
111        };
112
113        debug!(
114            "Dragging from ({}, {}) to ({}, {})",
115            source_x, source_y, target_x, target_y
116        );
117
118        // Perform drag operation
119        self.page.mouse().move_(source_x, source_y).send().await?;
120        self.page.mouse().down().send().await?;
121        self.page
122            .mouse()
123            .move_(target_x, target_y)
124            .steps(self.steps)
125            .send()
126            .await?;
127        self.page.mouse().up().send().await?;
128
129        Ok(())
130    }
131
132    /// Get the bounding box of an element (x, y, width, height).
133    async fn get_element_box(&self, selector: &str) -> Result<(f64, f64, f64, f64), LocatorError> {
134        let js = format!(
135            r"(function() {{
136                const el = document.querySelector({selector});
137                if (!el) return null;
138                const rect = el.getBoundingClientRect();
139                return {{ x: rect.x, y: rect.y, width: rect.width, height: rect.height }};
140            }})()",
141            selector = crate::page::locator::selector::js_string_literal(selector)
142        );
143
144        let result = self.evaluate_js(&js).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    async fn evaluate_js(&self, expression: &str) -> Result<serde_json::Value, LocatorError> {
172        if self.page.is_closed() {
173            return Err(LocatorError::PageClosed);
174        }
175
176        let params = viewpoint_cdp::protocol::runtime::EvaluateParams {
177            expression: expression.to_string(),
178            object_group: None,
179            include_command_line_api: None,
180            silent: Some(true),
181            context_id: None,
182            return_by_value: Some(true),
183            await_promise: Some(false),
184        };
185
186        let result: viewpoint_cdp::protocol::runtime::EvaluateResult = self
187            .page
188            .connection()
189            .send_command(
190                "Runtime.evaluate",
191                Some(params),
192                Some(self.page.session_id()),
193            )
194            .await?;
195
196        if let Some(exception) = result.exception_details {
197            return Err(LocatorError::EvaluationError(exception.text));
198        }
199
200        result
201            .result
202            .value
203            .ok_or_else(|| LocatorError::EvaluationError("No result value".to_string()))
204    }
205}