Skip to main content

browser_commander/interactions/
keyboard.rs

1//! Page-level keyboard interactions.
2//!
3//! This module provides functions for sending keyboard events at the page level,
4//! independent of any specific element. This is useful for:
5//! - Dismissing dialogs (`Escape`)
6//! - Submitting forms (`Enter`)
7//! - Tab navigation
8//! - Keyboard shortcuts (e.g. `Control+A`)
9
10#[cfg(test)]
11use crate::core::engine::PdfOptions;
12use crate::core::engine::{EngineAdapter, EngineError};
13
14/// Press a key at the page level.
15///
16/// Key names follow the Playwright/Puppeteer convention, e.g.:
17/// `"Escape"`, `"Enter"`, `"Tab"`, `"ArrowDown"`, `"Control"`, `"Shift"`.
18///
19/// # Arguments
20///
21/// * `engine` - The browser engine adapter
22/// * `key` - The key name to press
23///
24/// # Errors
25///
26/// Returns `EngineError` if the key press fails.
27///
28/// # Example
29///
30/// ```rust,no_run
31/// use browser_commander::interactions::keyboard::press_key;
32///
33/// // press_key(&engine, "Escape").await?;
34/// ```
35pub async fn press_key(engine: &dyn EngineAdapter, key: &str) -> Result<(), EngineError> {
36    engine.keyboard_press(key).await
37}
38
39/// Type text at the page level (dispatches key events for each character).
40///
41/// Unlike element-level fill/type, this sends keyboard events to whatever
42/// element is currently focused on the page.
43///
44/// # Arguments
45///
46/// * `engine` - The browser engine adapter
47/// * `text` - The text to type
48///
49/// # Errors
50///
51/// Returns `EngineError` if typing fails.
52pub async fn type_text(engine: &dyn EngineAdapter, text: &str) -> Result<(), EngineError> {
53    engine.keyboard_type(text).await
54}
55
56/// Hold a key down at the page level.
57///
58/// Must be paired with [`key_up`] to release the key.
59///
60/// # Arguments
61///
62/// * `engine` - The browser engine adapter
63/// * `key` - The key name to hold down
64///
65/// # Errors
66///
67/// Returns `EngineError` if the key down operation fails.
68pub async fn key_down(engine: &dyn EngineAdapter, key: &str) -> Result<(), EngineError> {
69    engine.keyboard_down(key).await
70}
71
72/// Release a held key at the page level.
73///
74/// # Arguments
75///
76/// * `engine` - The browser engine adapter
77/// * `key` - The key name to release
78///
79/// # Errors
80///
81/// Returns `EngineError` if the key up operation fails.
82pub async fn key_up(engine: &dyn EngineAdapter, key: &str) -> Result<(), EngineError> {
83    engine.keyboard_up(key).await
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::core::engine::{ElementInfo, EngineError, EngineType};
90    use async_trait::async_trait;
91    use std::sync::{Arc, Mutex};
92
93    /// Mock engine for testing keyboard operations.
94    struct MockEngine {
95        pressed_keys: Arc<Mutex<Vec<String>>>,
96        typed_texts: Arc<Mutex<Vec<String>>>,
97        down_keys: Arc<Mutex<Vec<String>>>,
98        up_keys: Arc<Mutex<Vec<String>>>,
99    }
100
101    impl MockEngine {
102        fn new() -> Self {
103            Self {
104                pressed_keys: Arc::new(Mutex::new(vec![])),
105                typed_texts: Arc::new(Mutex::new(vec![])),
106                down_keys: Arc::new(Mutex::new(vec![])),
107                up_keys: Arc::new(Mutex::new(vec![])),
108            }
109        }
110    }
111
112    #[async_trait]
113    impl EngineAdapter for MockEngine {
114        fn engine_type(&self) -> EngineType {
115            EngineType::Fantoccini
116        }
117
118        async fn url(&self) -> Result<String, EngineError> {
119            Ok("https://example.com".to_string())
120        }
121
122        async fn goto(&self, _url: &str) -> Result<(), EngineError> {
123            Ok(())
124        }
125
126        async fn query_selector(
127            &self,
128            _selector: &str,
129        ) -> Result<Option<ElementInfo>, EngineError> {
130            Ok(None)
131        }
132
133        async fn query_selector_all(
134            &self,
135            _selector: &str,
136        ) -> Result<Vec<ElementInfo>, EngineError> {
137            Ok(vec![])
138        }
139
140        async fn count(&self, _selector: &str) -> Result<usize, EngineError> {
141            Ok(0)
142        }
143
144        async fn click(&self, _selector: &str) -> Result<(), EngineError> {
145            Ok(())
146        }
147
148        async fn fill(&self, _selector: &str, _text: &str) -> Result<(), EngineError> {
149            Ok(())
150        }
151
152        async fn type_text(&self, _selector: &str, _text: &str) -> Result<(), EngineError> {
153            Ok(())
154        }
155
156        async fn text_content(&self, _selector: &str) -> Result<Option<String>, EngineError> {
157            Ok(None)
158        }
159
160        async fn input_value(&self, _selector: &str) -> Result<Option<String>, EngineError> {
161            Ok(None)
162        }
163
164        async fn get_attribute(
165            &self,
166            _selector: &str,
167            _attribute: &str,
168        ) -> Result<Option<String>, EngineError> {
169            Ok(None)
170        }
171
172        async fn is_visible(&self, _selector: &str) -> Result<bool, EngineError> {
173            Ok(true)
174        }
175
176        async fn is_enabled(&self, _selector: &str) -> Result<bool, EngineError> {
177            Ok(true)
178        }
179
180        async fn wait_for_selector(
181            &self,
182            _selector: &str,
183            _timeout_ms: u64,
184        ) -> Result<(), EngineError> {
185            Ok(())
186        }
187
188        async fn scroll_into_view(&self, _selector: &str) -> Result<(), EngineError> {
189            Ok(())
190        }
191
192        async fn evaluate(&self, _script: &str) -> Result<serde_json::Value, EngineError> {
193            Ok(serde_json::Value::Null)
194        }
195
196        async fn screenshot(&self) -> Result<Vec<u8>, EngineError> {
197            Ok(vec![])
198        }
199
200        async fn bring_to_front(&self) -> Result<(), EngineError> {
201            Ok(())
202        }
203
204        async fn wait_for_navigation(&self, _timeout_ms: u64) -> Result<(), EngineError> {
205            Ok(())
206        }
207
208        async fn keyboard_press(&self, key: &str) -> Result<(), EngineError> {
209            self.pressed_keys.lock().unwrap().push(key.to_string());
210            Ok(())
211        }
212
213        async fn keyboard_type(&self, text: &str) -> Result<(), EngineError> {
214            self.typed_texts.lock().unwrap().push(text.to_string());
215            Ok(())
216        }
217
218        async fn keyboard_down(&self, key: &str) -> Result<(), EngineError> {
219            self.down_keys.lock().unwrap().push(key.to_string());
220            Ok(())
221        }
222
223        async fn keyboard_up(&self, key: &str) -> Result<(), EngineError> {
224            self.up_keys.lock().unwrap().push(key.to_string());
225            Ok(())
226        }
227
228        async fn pdf(&self, _options: PdfOptions) -> Result<Vec<u8>, EngineError> {
229            Err(EngineError::Browser(
230                "PDF not supported in mock engine".to_string(),
231            ))
232        }
233    }
234
235    #[tokio::test]
236    async fn test_press_key() {
237        let engine = MockEngine::new();
238        press_key(&engine, "Escape").await.unwrap();
239        assert_eq!(
240            *engine.pressed_keys.lock().unwrap(),
241            vec!["Escape".to_string()]
242        );
243    }
244
245    #[tokio::test]
246    async fn test_type_text() {
247        let engine = MockEngine::new();
248        type_text(&engine, "Hello World").await.unwrap();
249        assert_eq!(
250            *engine.typed_texts.lock().unwrap(),
251            vec!["Hello World".to_string()]
252        );
253    }
254
255    #[tokio::test]
256    async fn test_key_down() {
257        let engine = MockEngine::new();
258        key_down(&engine, "Control").await.unwrap();
259        assert_eq!(
260            *engine.down_keys.lock().unwrap(),
261            vec!["Control".to_string()]
262        );
263    }
264
265    #[tokio::test]
266    async fn test_key_up() {
267        let engine = MockEngine::new();
268        key_up(&engine, "Control").await.unwrap();
269        assert_eq!(*engine.up_keys.lock().unwrap(), vec!["Control".to_string()]);
270    }
271
272    #[tokio::test]
273    async fn test_press_enter_key() {
274        let engine = MockEngine::new();
275        press_key(&engine, "Enter").await.unwrap();
276        assert_eq!(
277            *engine.pressed_keys.lock().unwrap(),
278            vec!["Enter".to_string()]
279        );
280    }
281}