viewpoint_core/page/touchscreen/
mod.rs

1//! Touchscreen input handling.
2//!
3//! Provides touch input simulation for mobile testing scenarios.
4
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
7
8use tracing::{debug, instrument};
9use viewpoint_cdp::protocol::emulation::SetTouchEmulationEnabledParams;
10use viewpoint_cdp::protocol::input::{DispatchTouchEventParams, TouchPoint};
11use viewpoint_cdp::CdpConnection;
12
13use crate::error::LocatorError;
14
15/// Global touch identifier counter for unique touch point IDs.
16static TOUCH_ID_COUNTER: AtomicI32 = AtomicI32::new(0);
17
18/// Touchscreen controller for touch input simulation.
19///
20/// Provides methods for tapping and touch gestures.
21/// Requires touch to be enabled via [`enable`](Touchscreen::enable) or `hasTouch: true`
22/// in browser context options.
23///
24/// # Example
25///
26/// ```ignore
27/// // Enable touch on the page
28/// page.touchscreen().enable().await?;
29///
30/// // Tap at coordinates
31/// page.touchscreen().tap(100.0, 200.0).await?;
32/// ```
33#[derive(Debug)]
34pub struct Touchscreen {
35    /// CDP connection.
36    connection: Arc<CdpConnection>,
37    /// Session ID for the page.
38    session_id: String,
39    /// Whether touch emulation is enabled.
40    enabled: AtomicBool,
41}
42
43impl Touchscreen {
44    /// Create a new touchscreen controller.
45    pub(crate) fn new(connection: Arc<CdpConnection>, session_id: String) -> Self {
46        Self {
47            connection,
48            session_id,
49            enabled: AtomicBool::new(false),
50        }
51    }
52
53    /// Enable touch emulation.
54    ///
55    /// This must be called before using touch methods, or touch must be enabled
56    /// in browser context options.
57    ///
58    /// # Arguments
59    ///
60    /// * `max_touch_points` - Maximum number of touch points. Defaults to 1.
61    ///
62    /// # Example
63    ///
64    /// ```ignore
65    /// // Enable touch with default settings
66    /// page.touchscreen().enable().await?;
67    ///
68    /// // Enable touch with multiple touch points
69    /// page.touchscreen().enable_with_max_points(5).await?;
70    /// ```
71    #[instrument(level = "debug", skip(self))]
72    pub async fn enable(&self) -> Result<(), LocatorError> {
73        self.enable_with_max_points(1).await
74    }
75
76    /// Enable touch emulation with a specific maximum number of touch points.
77    #[instrument(level = "debug", skip(self), fields(max_touch_points = max_touch_points))]
78    pub async fn enable_with_max_points(&self, max_touch_points: i32) -> Result<(), LocatorError> {
79        debug!("Enabling touch emulation with max_touch_points={}", max_touch_points);
80
81        self.connection
82            .send_command::<_, serde_json::Value>(
83                "Emulation.setTouchEmulationEnabled",
84                Some(SetTouchEmulationEnabledParams {
85                    enabled: true,
86                    max_touch_points: Some(max_touch_points),
87                }),
88                Some(&self.session_id),
89            )
90            .await?;
91
92        self.enabled.store(true, Ordering::SeqCst);
93        Ok(())
94    }
95
96    /// Disable touch emulation.
97    #[instrument(level = "debug", skip(self))]
98    pub async fn disable(&self) -> Result<(), LocatorError> {
99        debug!("Disabling touch emulation");
100
101        self.connection
102            .send_command::<_, serde_json::Value>(
103                "Emulation.setTouchEmulationEnabled",
104                Some(SetTouchEmulationEnabledParams {
105                    enabled: false,
106                    max_touch_points: None,
107                }),
108                Some(&self.session_id),
109            )
110            .await?;
111
112        self.enabled.store(false, Ordering::SeqCst);
113        Ok(())
114    }
115
116    /// Check if touch emulation is enabled.
117    pub fn is_enabled(&self) -> bool {
118        self.enabled.load(Ordering::SeqCst)
119    }
120
121    /// Mark touch as enabled (for internal use when context is created with hasTouch).
122    pub(crate) fn set_enabled(&self, enabled: bool) {
123        self.enabled.store(enabled, Ordering::SeqCst);
124    }
125
126    /// Check that touch is enabled, returning an error if not.
127    fn check_enabled(&self) -> Result<(), LocatorError> {
128        if !self.is_enabled() {
129            return Err(LocatorError::TouchNotEnabled);
130        }
131        Ok(())
132    }
133
134    /// Tap at the specified coordinates.
135    ///
136    /// Dispatches touchStart and touchEnd events.
137    ///
138    /// # Arguments
139    ///
140    /// * `x` - X coordinate in CSS pixels
141    /// * `y` - Y coordinate in CSS pixels
142    ///
143    /// # Errors
144    ///
145    /// Returns [`LocatorError::TouchNotEnabled`] if touch emulation is not enabled.
146    /// Call [`enable`](Touchscreen::enable) first or set `hasTouch: true` in context options.
147    ///
148    /// # Example
149    ///
150    /// ```ignore
151    /// page.touchscreen().enable().await?;
152    /// page.touchscreen().tap(100.0, 200.0).await?;
153    /// ```
154    #[instrument(level = "debug", skip(self), fields(x = x, y = y))]
155    pub async fn tap(&self, x: f64, y: f64) -> Result<(), LocatorError> {
156        self.check_enabled()?;
157        debug!("Tapping at ({}, {})", x, y);
158
159        // Generate unique touch ID
160        let touch_id = TOUCH_ID_COUNTER.fetch_add(1, Ordering::SeqCst);
161
162        // Touch start
163        let mut touch_point = TouchPoint::new(x, y);
164        touch_point.id = Some(touch_id);
165
166        let start_params = DispatchTouchEventParams {
167            event_type: viewpoint_cdp::protocol::input::TouchEventType::TouchStart,
168            touch_points: vec![touch_point.clone()],
169            modifiers: None,
170            timestamp: None,
171        };
172
173        self.connection
174            .send_command::<_, serde_json::Value>(
175                "Input.dispatchTouchEvent",
176                Some(start_params),
177                Some(&self.session_id),
178            )
179            .await?;
180
181        // Touch end
182        let end_params = DispatchTouchEventParams {
183            event_type: viewpoint_cdp::protocol::input::TouchEventType::TouchEnd,
184            touch_points: vec![],
185            modifiers: None,
186            timestamp: None,
187        };
188
189        self.connection
190            .send_command::<_, serde_json::Value>(
191                "Input.dispatchTouchEvent",
192                Some(end_params),
193                Some(&self.session_id),
194            )
195            .await?;
196
197        Ok(())
198    }
199
200    /// Tap with modifiers (Shift, Control, etc).
201    ///
202    /// # Errors
203    ///
204    /// Returns [`LocatorError::TouchNotEnabled`] if touch emulation is not enabled.
205    #[instrument(level = "debug", skip(self), fields(x = x, y = y, modifiers = modifiers))]
206    pub async fn tap_with_modifiers(
207        &self,
208        x: f64,
209        y: f64,
210        modifiers: i32,
211    ) -> Result<(), LocatorError> {
212        self.check_enabled()?;
213        debug!("Tapping at ({}, {}) with modifiers {}", x, y, modifiers);
214
215        let touch_id = TOUCH_ID_COUNTER.fetch_add(1, Ordering::SeqCst);
216
217        let mut touch_point = TouchPoint::new(x, y);
218        touch_point.id = Some(touch_id);
219
220        // Touch start with modifiers
221        let start_params = DispatchTouchEventParams {
222            event_type: viewpoint_cdp::protocol::input::TouchEventType::TouchStart,
223            touch_points: vec![touch_point],
224            modifiers: Some(modifiers),
225            timestamp: None,
226        };
227
228        self.connection
229            .send_command::<_, serde_json::Value>(
230                "Input.dispatchTouchEvent",
231                Some(start_params),
232                Some(&self.session_id),
233            )
234            .await?;
235
236        // Touch end with modifiers
237        let end_params = DispatchTouchEventParams {
238            event_type: viewpoint_cdp::protocol::input::TouchEventType::TouchEnd,
239            touch_points: vec![],
240            modifiers: Some(modifiers),
241            timestamp: None,
242        };
243
244        self.connection
245            .send_command::<_, serde_json::Value>(
246                "Input.dispatchTouchEvent",
247                Some(end_params),
248                Some(&self.session_id),
249            )
250            .await?;
251
252        Ok(())
253    }
254}