@@ -30,37 +30,39 @@ impl EnigoInjector {
3030 Enigo :: new ( & Settings :: default ( ) ) . is_ok ( )
3131 }
3232
33+ /// Inner logic for typing text, generic over the Keyboard trait for testing.
34+ fn type_text_logic < K : Keyboard > ( enigo : & mut K , text : & str ) -> Result < ( ) , InjectionError > {
35+ // Type each character with a small delay
36+ for c in text. chars ( ) {
37+ match c {
38+ ' ' => enigo
39+ . key ( Key :: Space , Direction :: Click )
40+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to type space: {}" , e) ) ) ?,
41+ '\n' => enigo
42+ . key ( Key :: Return , Direction :: Click )
43+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to type enter: {}" , e) ) ) ?,
44+ '\t' => enigo
45+ . key ( Key :: Tab , Direction :: Click )
46+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to type tab: {}" , e) ) ) ?,
47+ _ => {
48+ // Use text method for all other characters
49+ enigo
50+ . text ( & c. to_string ( ) )
51+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to type text: {}" , e) ) ) ?;
52+ }
53+ }
54+ }
55+ Ok ( ( ) )
56+ }
57+
3358 /// Type text using enigo
3459 async fn type_text ( & self , text : & str ) -> Result < ( ) , InjectionError > {
3560 let text_clone = text. to_string ( ) ;
3661
3762 let result = tokio:: task:: spawn_blocking ( move || {
38- let mut enigo = Enigo :: new ( & Settings :: default ( ) ) . map_err ( |e| {
39- InjectionError :: MethodFailed ( format ! ( "Failed to create Enigo: {}" , e) )
40- } ) ?;
41-
42- // Type each character with a small delay
43- for c in text_clone. chars ( ) {
44- match c {
45- ' ' => enigo. key ( Key :: Space , Direction :: Click ) . map_err ( |e| {
46- InjectionError :: MethodFailed ( format ! ( "Failed to type space: {}" , e) )
47- } ) ?,
48- '\n' => enigo. key ( Key :: Return , Direction :: Click ) . map_err ( |e| {
49- InjectionError :: MethodFailed ( format ! ( "Failed to type enter: {}" , e) )
50- } ) ?,
51- '\t' => enigo. key ( Key :: Tab , Direction :: Click ) . map_err ( |e| {
52- InjectionError :: MethodFailed ( format ! ( "Failed to type tab: {}" , e) )
53- } ) ?,
54- _ => {
55- // Use text method for all other characters
56- enigo. text ( & c. to_string ( ) ) . map_err ( |e| {
57- InjectionError :: MethodFailed ( format ! ( "Failed to type text: {}" , e) )
58- } ) ?;
59- }
60- }
61- }
62-
63- Ok ( ( ) )
63+ let mut enigo = Enigo :: new ( & Settings :: default ( ) )
64+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to create Enigo: {}" , e) ) ) ?;
65+ Self :: type_text_logic ( & mut enigo, & text_clone)
6466 } )
6567 . await ;
6668
@@ -74,44 +76,42 @@ impl EnigoInjector {
7476 }
7577 }
7678
79+ /// Inner logic for triggering paste, generic over the Keyboard trait for testing.
80+ fn trigger_paste_logic < K : Keyboard > ( enigo : & mut K ) -> Result < ( ) , InjectionError > {
81+ // Press platform-appropriate paste shortcut
82+ #[ cfg( target_os = "macos" ) ]
83+ {
84+ enigo
85+ . key ( Key :: Meta , Direction :: Press )
86+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to press Cmd: {}" , e) ) ) ?;
87+ enigo
88+ . key ( Key :: Unicode ( 'v' ) , Direction :: Click )
89+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to type 'v': {}" , e) ) ) ?;
90+ enigo
91+ . key ( Key :: Meta , Direction :: Release )
92+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to release Cmd: {}" , e) ) ) ?;
93+ }
94+ #[ cfg( not( target_os = "macos" ) ) ]
95+ {
96+ enigo
97+ . key ( Key :: Control , Direction :: Press )
98+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to press Ctrl: {}" , e) ) ) ?;
99+ enigo
100+ . key ( Key :: Unicode ( 'v' ) , Direction :: Click )
101+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to type 'v': {}" , e) ) ) ?;
102+ enigo
103+ . key ( Key :: Control , Direction :: Release )
104+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to release Ctrl: {}" , e) ) ) ?;
105+ }
106+ Ok ( ( ) )
107+ }
108+
77109 /// Trigger paste action using enigo (Ctrl+V)
78110 async fn trigger_paste ( & self ) -> Result < ( ) , InjectionError > {
79111 let result = tokio:: task:: spawn_blocking ( || {
80- let mut enigo = Enigo :: new ( & Settings :: default ( ) ) . map_err ( |e| {
81- InjectionError :: MethodFailed ( format ! ( "Failed to create Enigo: {}" , e) )
82- } ) ?;
83-
84- // Press platform-appropriate paste shortcut
85- #[ cfg( target_os = "macos" ) ]
86- {
87- enigo. key ( Key :: Meta , Direction :: Press ) . map_err ( |e| {
88- InjectionError :: MethodFailed ( format ! ( "Failed to press Cmd: {}" , e) )
89- } ) ?;
90- enigo
91- . key ( Key :: Unicode ( 'v' ) , Direction :: Click )
92- . map_err ( |e| {
93- InjectionError :: MethodFailed ( format ! ( "Failed to type 'v': {}" , e) )
94- } ) ?;
95- enigo. key ( Key :: Meta , Direction :: Release ) . map_err ( |e| {
96- InjectionError :: MethodFailed ( format ! ( "Failed to release Cmd: {}" , e) )
97- } ) ?;
98- }
99- #[ cfg( not( target_os = "macos" ) ) ]
100- {
101- enigo. key ( Key :: Control , Direction :: Press ) . map_err ( |e| {
102- InjectionError :: MethodFailed ( format ! ( "Failed to press Ctrl: {}" , e) )
103- } ) ?;
104- enigo
105- . key ( Key :: Unicode ( 'v' ) , Direction :: Click )
106- . map_err ( |e| {
107- InjectionError :: MethodFailed ( format ! ( "Failed to type 'v': {}" , e) )
108- } ) ?;
109- enigo. key ( Key :: Control , Direction :: Release ) . map_err ( |e| {
110- InjectionError :: MethodFailed ( format ! ( "Failed to release Ctrl: {}" , e) )
111- } ) ?;
112- }
113-
114- Ok ( ( ) )
112+ let mut enigo = Enigo :: new ( & Settings :: default ( ) )
113+ . map_err ( |e| InjectionError :: MethodFailed ( format ! ( "Failed to create Enigo: {}" , e) ) ) ?;
114+ Self :: trigger_paste_logic ( & mut enigo)
115115 } )
116116 . await ;
117117
@@ -179,3 +179,145 @@ impl TextInjector for EnigoInjector {
179179 ]
180180 }
181181}
182+
183+ #[ cfg( test) ]
184+ mod tests {
185+ use super :: * ;
186+ use crate :: types:: InjectionConfig ;
187+ use std:: cell:: RefCell ;
188+ use std:: collections:: VecDeque ;
189+
190+ // A more robust MockEnigo that can simulate failures and captures actions.
191+ struct MockEnigo {
192+ actions : RefCell < Vec < String > > ,
193+ failures : RefCell < VecDeque < bool > > , // A queue of whether the next action should fail.
194+ }
195+
196+ impl MockEnigo {
197+ fn new ( ) -> Self {
198+ Self {
199+ actions : RefCell :: new ( Vec :: new ( ) ) ,
200+ failures : RefCell :: new ( VecDeque :: new ( ) ) ,
201+ }
202+ }
203+
204+ fn should_fail ( & self ) -> bool {
205+ self . failures . borrow_mut ( ) . pop_front ( ) . unwrap_or ( false )
206+ }
207+
208+ #[ allow( dead_code) ]
209+ fn push_failure ( & self , fail : bool ) {
210+ self . failures . borrow_mut ( ) . push_back ( fail) ;
211+ }
212+ }
213+
214+ impl Keyboard for MockEnigo {
215+ fn key ( & mut self , key : Key , direction : Direction ) -> Result < ( ) , enigo:: Error > {
216+ if self . should_fail ( ) {
217+ return Err ( enigo:: Error :: InvalidKey ) ;
218+ }
219+ self . actions
220+ . borrow_mut ( )
221+ . push ( format ! ( "key({:?},{:?})" , key, direction) ) ;
222+ Ok ( ( ) )
223+ }
224+
225+ fn text ( & mut self , text : & str ) -> Result < ( ) , enigo:: Error > {
226+ if self . should_fail ( ) {
227+ return Err ( enigo:: Error :: InvalidText ) ;
228+ }
229+ self . actions . borrow_mut ( ) . push ( format ! ( "text(\" {}\" )" , text) ) ;
230+ Ok ( ( ) )
231+ }
232+ }
233+
234+ #[ test]
235+ fn test_type_text_logic_simple ( ) {
236+ let mut mock_enigo = MockEnigo :: new ( ) ;
237+ let result = EnigoInjector :: type_text_logic ( & mut mock_enigo, "abc" ) ;
238+ assert ! ( result. is_ok( ) ) ;
239+ assert_eq ! (
240+ * mock_enigo. actions. borrow( ) ,
241+ vec![ "text(\" a\" )" , "text(\" b\" )" , "text(\" c\" )" ]
242+ ) ;
243+ }
244+
245+ #[ test]
246+ fn test_type_text_logic_special_chars ( ) {
247+ let mut mock_enigo = MockEnigo :: new ( ) ;
248+ let result = EnigoInjector :: type_text_logic ( & mut mock_enigo, " \n \t " ) ;
249+ assert ! ( result. is_ok( ) ) ;
250+ assert_eq ! (
251+ * mock_enigo. actions. borrow( ) ,
252+ vec![
253+ "key(Space,Click)" ,
254+ "key(Return,Click)" ,
255+ "key(Tab,Click)"
256+ ]
257+ ) ;
258+ }
259+
260+ #[ test]
261+ fn test_type_text_logic_failure ( ) {
262+ let mut mock_enigo = MockEnigo :: new ( ) ;
263+ mock_enigo. push_failure ( true ) ; // First action will fail
264+ let result = EnigoInjector :: type_text_logic ( & mut mock_enigo, "a" ) ;
265+ assert ! ( result. is_err( ) ) ;
266+ }
267+
268+ #[ test]
269+ #[ cfg( not( target_os = "macos" ) ) ]
270+ fn test_trigger_paste_logic_non_macos ( ) {
271+ let mut mock_enigo = MockEnigo :: new ( ) ;
272+ let result = EnigoInjector :: trigger_paste_logic ( & mut mock_enigo) ;
273+ assert ! ( result. is_ok( ) ) ;
274+ assert_eq ! (
275+ * mock_enigo. actions. borrow( ) ,
276+ vec![
277+ "key(Control,Press)" ,
278+ "key(Unicode('v'),Click)" ,
279+ "key(Control,Release)"
280+ ]
281+ ) ;
282+ }
283+
284+ #[ test]
285+ #[ cfg( target_os = "macos" ) ]
286+ fn test_trigger_paste_logic_macos ( ) {
287+ let mut mock_enigo = MockEnigo :: new ( ) ;
288+ let result = EnigoInjector :: trigger_paste_logic ( & mut mock_enigo) ;
289+ assert ! ( result. is_ok( ) ) ;
290+ assert_eq ! (
291+ * mock_enigo. actions. borrow( ) ,
292+ vec![
293+ "key(Meta,Press)" ,
294+ "key(Unicode('v'),Click)" ,
295+ "key(Meta,Release)"
296+ ]
297+ ) ;
298+ }
299+
300+ #[ test]
301+ fn test_trigger_paste_logic_failure ( ) {
302+ let mut mock_enigo = MockEnigo :: new ( ) ;
303+ mock_enigo. push_failure ( true ) ;
304+ let result = EnigoInjector :: trigger_paste_logic ( & mut mock_enigo) ;
305+ assert ! ( result. is_err( ) ) ;
306+ }
307+
308+ // The async tests can remain as integration tests, but we'll keep them simple.
309+ #[ tokio:: test]
310+ async fn test_enigo_injector_new ( ) {
311+ let config = InjectionConfig :: default ( ) ;
312+ let injector = EnigoInjector :: new ( config) ;
313+ assert_eq ! ( injector. config, config) ;
314+ }
315+
316+ #[ tokio:: test]
317+ async fn test_inject_text_empty ( ) {
318+ let config = InjectionConfig :: default ( ) ;
319+ let injector = EnigoInjector :: new ( config) ;
320+ let result = injector. inject_text ( "" , None ) . await ;
321+ assert ! ( result. is_ok( ) ) ;
322+ }
323+ }
0 commit comments