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