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