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    /// ```ignore
73    /// // Enable touch with default settings
74    /// page.touchscreen().enable().await?;
75    ///
76    /// // Enable touch with multiple touch points
77    /// page.touchscreen().enable_with_max_points(5).await?;
78    /// ```
79    #[instrument(level = "debug", skip(self))]
80    pub async fn enable(&self) -> Result<(), LocatorError> {
81        self.enable_with_max_points(1).await
82    }
83
84    /// Enable touch emulation with a specific maximum number of touch points.
85    #[instrument(level = "debug", skip(self), fields(max_touch_points = max_touch_points))]
86    pub async fn enable_with_max_points(&self, max_touch_points: i32) -> Result<(), LocatorError> {
87        debug!("Enabling touch emulation with max_touch_points={}", max_touch_points);
88
89        self.connection
90            .send_command::<_, serde_json::Value>(
91                "Emulation.setTouchEmulationEnabled",
92                Some(SetTouchEmulationEnabledParams {
93                    enabled: true,
94                    max_touch_points: Some(max_touch_points),
95                }),
96                Some(&self.session_id),
97            )
98            .await?;
99
100        self.enabled.store(true, Ordering::SeqCst);
101        Ok(())
102    }
103
104    /// Disable touch emulation.
105    #[instrument(level = "debug", skip(self))]
106    pub async fn disable(&self) -> Result<(), LocatorError> {
107        debug!("Disabling touch emulation");
108
109        self.connection
110            .send_command::<_, serde_json::Value>(
111                "Emulation.setTouchEmulationEnabled",
112                Some(SetTouchEmulationEnabledParams {
113                    enabled: false,
114                    max_touch_points: None,
115                }),
116                Some(&self.session_id),
117            )
118            .await?;
119
120        self.enabled.store(false, Ordering::SeqCst);
121        Ok(())
122    }
123
124    /// Check if touch emulation is enabled.
125    pub fn is_enabled(&self) -> bool {
126        self.enabled.load(Ordering::SeqCst)
127    }
128
129    /// Mark touch as enabled (for internal use when context is created with hasTouch).
130    pub(crate) fn set_enabled(&self, enabled: bool) {
131        self.enabled.store(enabled, Ordering::SeqCst);
132    }
133
134    /// Check that touch is enabled, returning an error if not.
135    fn check_enabled(&self) -> Result<(), LocatorError> {
136        if !self.is_enabled() {
137            return Err(LocatorError::TouchNotEnabled);
138        }
139        Ok(())
140    }
141
142    /// Tap at the specified coordinates.
143    ///
144    /// Dispatches touchStart and touchEnd events.
145    ///
146    /// # Arguments
147    ///
148    /// * `x` - X coordinate in CSS pixels
149    /// * `y` - Y coordinate in CSS pixels
150    ///
151    /// # Errors
152    ///
153    /// Returns [`LocatorError::TouchNotEnabled`] if touch emulation is not enabled.
154    /// Call [`enable`](Touchscreen::enable) first or set `hasTouch: true` in context options.
155    ///
156    /// # Example
157    ///
158    /// ```ignore
159    /// page.touchscreen().enable().await?;
160    /// page.touchscreen().tap(100.0, 200.0).await?;
161    /// ```
162    #[instrument(level = "debug", skip(self), fields(x = x, y = y))]
163    pub async fn tap(&self, x: f64, y: f64) -> Result<(), LocatorError> {
164        self.check_enabled()?;
165        debug!("Tapping at ({}, {})", x, y);
166
167        // Generate unique touch ID
168        let touch_id = TOUCH_ID_COUNTER.fetch_add(1, Ordering::SeqCst);
169
170        // Touch start
171        let mut touch_point = TouchPoint::new(x, y);
172        touch_point.id = Some(touch_id);
173
174        let start_params = DispatchTouchEventParams {
175            event_type: viewpoint_cdp::protocol::input::TouchEventType::TouchStart,
176            touch_points: vec![touch_point.clone()],
177            modifiers: None,
178            timestamp: None,
179        };
180
181        self.connection
182            .send_command::<_, serde_json::Value>(
183                "Input.dispatchTouchEvent",
184                Some(start_params),
185                Some(&self.session_id),
186            )
187            .await?;
188
189        // Touch end
190        let end_params = DispatchTouchEventParams {
191            event_type: viewpoint_cdp::protocol::input::TouchEventType::TouchEnd,
192            touch_points: vec![],
193            modifiers: None,
194            timestamp: None,
195        };
196
197        self.connection
198            .send_command::<_, serde_json::Value>(
199                "Input.dispatchTouchEvent",
200                Some(end_params),
201                Some(&self.session_id),
202            )
203            .await?;
204
205        Ok(())
206    }
207
208    /// Tap with modifiers (Shift, Control, etc).
209    ///
210    /// # Errors
211    ///
212    /// Returns [`LocatorError::TouchNotEnabled`] if touch emulation is not enabled.
213    #[instrument(level = "debug", skip(self), fields(x = x, y = y, modifiers = modifiers))]
214    pub async fn tap_with_modifiers(
215        &self,
216        x: f64,
217        y: f64,
218        modifiers: i32,
219    ) -> Result<(), LocatorError> {
220        self.check_enabled()?;
221        debug!("Tapping at ({}, {}) with modifiers {}", x, y, modifiers);
222
223        let touch_id = TOUCH_ID_COUNTER.fetch_add(1, Ordering::SeqCst);
224
225        let mut touch_point = TouchPoint::new(x, y);
226        touch_point.id = Some(touch_id);
227
228        // Touch start with modifiers
229        let start_params = DispatchTouchEventParams {
230            event_type: viewpoint_cdp::protocol::input::TouchEventType::TouchStart,
231            touch_points: vec![touch_point],
232            modifiers: Some(modifiers),
233            timestamp: None,
234        };
235
236        self.connection
237            .send_command::<_, serde_json::Value>(
238                "Input.dispatchTouchEvent",
239                Some(start_params),
240                Some(&self.session_id),
241            )
242            .await?;
243
244        // Touch end with modifiers
245        let end_params = DispatchTouchEventParams {
246            event_type: viewpoint_cdp::protocol::input::TouchEventType::TouchEnd,
247            touch_points: vec![],
248            modifiers: Some(modifiers),
249            timestamp: None,
250        };
251
252        self.connection
253            .send_command::<_, serde_json::Value>(
254                "Input.dispatchTouchEvent",
255                Some(end_params),
256                Some(&self.session_id),
257            )
258            .await?;
259
260        Ok(())
261    }
262}