1package usecase
2
3import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 "github.com/bnema/dumber/internal/application/port/mocks"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/mock"
12)
13
14func TestInsertAccentUseCase_OnKeyPressed_NoAccents(t *testing.T) {
15 ctx := context.Background()
16 focusProvider := mocks.NewMockFocusedInputProvider(t)
17 accentPicker := mocks.NewMockAccentPickerUI(t)
18
19 uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() })
20
21 // Key 'x' has no accents - should return false and not start timer
22 handled := uc.OnKeyPressed(ctx, 'x', false)
23 assert.False(t, handled, "OnKeyPressed should return false for key without accents")
24 assert.False(t, uc.IsPickerVisible(), "picker should not be visible")
25}
26
27func TestInsertAccentUseCase_OnKeyPressed_HasAccents(t *testing.T) {
28 ctx := context.Background()
29 focusProvider := mocks.NewMockFocusedInputProvider(t)
30 accentPicker := mocks.NewMockAccentPickerUI(t)
31 textInput := mocks.NewMockTextInputTarget(t)
32
33 // GetFocusedInput is called when starting long-press to capture target
34 focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe()
35
36 uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() })
37
38 // Key 'e' has accents - should return false initially (timer started)
39 handled := uc.OnKeyPressed(ctx, 'e', false)
40 assert.False(t, handled, "OnKeyPressed should return false while timer is pending")
41
42 // Cancel to clean up timer
43 uc.Cancel(ctx)
44}
45
46func TestInsertAccentUseCase_OnKeyReleased_CancelsTimer(t *testing.T) {
47 ctx := context.Background()
48 focusProvider := mocks.NewMockFocusedInputProvider(t)
49 accentPicker := mocks.NewMockAccentPickerUI(t)
50 textInput := mocks.NewMockTextInputTarget(t)
51
52 // GetFocusedInput is called when starting long-press to capture target
53 focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe()
54
55 timerFired := false
56 uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) {
57 timerFired = true
58 fn()
59 })
60
61 // Start long-press detection
62 uc.OnKeyPressed(ctx, 'e', false)
63
64 // Release before timer fires
65 uc.OnKeyReleased(ctx, 'e')
66
67 // Wait a bit longer than LongPressDelay
68 time.Sleep(LongPressDelay + 50*time.Millisecond)
69
70 assert.False(t, timerFired, "timer should have been canceled by key release")
71}
72
73func TestInsertAccentUseCase_LongPress_ShowsPicker(t *testing.T) {
74 ctx := context.Background()
75 focusProvider := mocks.NewMockFocusedInputProvider(t)
76 accentPicker := mocks.NewMockAccentPickerUI(t)
77 textInput := mocks.NewMockTextInputTarget(t)
78
79 var wg sync.WaitGroup
80 wg.Add(1)
81
82 // GetFocusedInput is called when showing picker to delete repeated characters
83 focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe()
84 // DeleteBeforeCursor will be called to delete the initial character + any repeats
85 textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe()
86
87 // Expect Show to be called with accents for 'e'
88 accentPicker.EXPECT().
89 Show(mock.MatchedBy(func(accents []rune) bool {
90 return len(accents) > 0 && accents[0] == 'è'
91 }), mock.AnythingOfType("func(int32)"), mock.AnythingOfType("func()")).
92 Run(func(_ []rune, _ func(rune), _ func()) {
93 wg.Done()
94 }).
95 Once()
96
97 // Hide will be called by Cancel
98 accentPicker.EXPECT().Hide().Maybe()
99
100 uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() })
101
102 // Start long-press detection
103 uc.OnKeyPressed(ctx, 'e', false)
104
105 // Wait for timer to fire
106 wg.Wait()
107 time.Sleep(50 * time.Millisecond) // Give time for state update
108
109 assert.True(t, uc.IsPickerVisible(), "picker should be visible after long-press")
110
111 // Clean up
112 uc.Cancel(ctx)
113}
114
115func TestInsertAccentUseCase_Cancel(t *testing.T) {
116 ctx := context.Background()
117 focusProvider := mocks.NewMockFocusedInputProvider(t)
118 accentPicker := mocks.NewMockAccentPickerUI(t)
119 textInput := mocks.NewMockTextInputTarget(t)
120
121 // GetFocusedInput is called when starting long-press to capture target
122 focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe()
123
124 uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() })
125
126 // Start timer
127 uc.OnKeyPressed(ctx, 'e', false)
128
129 // Cancel should stop timer
130 uc.Cancel(ctx)
131
132 // Wait past the timer duration
133 time.Sleep(LongPressDelay + 50*time.Millisecond)
134
135 // Picker should never have been shown
136 assert.False(t, uc.IsPickerVisible(), "picker should not be visible after cancel")
137}
138
139func TestInsertAccentUseCase_UppercaseAccents(t *testing.T) {
140 ctx := context.Background()
141 focusProvider := mocks.NewMockFocusedInputProvider(t)
142 accentPicker := mocks.NewMockAccentPickerUI(t)
143 textInput := mocks.NewMockTextInputTarget(t)
144
145 var wg sync.WaitGroup
146 wg.Add(1)
147
148 // GetFocusedInput is called when showing picker to delete repeated characters
149 focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe()
150 // DeleteBeforeCursor will be called to delete the initial character + any repeats
151 textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe()
152
153 // Expect Show to be called with uppercase accents when shift is held
154 accentPicker.EXPECT().
155 Show(mock.MatchedBy(func(accents []rune) bool {
156 return len(accents) > 0 && accents[0] == 'È' // Uppercase È
157 }), mock.AnythingOfType("func(int32)"), mock.AnythingOfType("func()")).
158 Run(func(_ []rune, _ func(rune), _ func()) {
159 wg.Done()
160 }).
161 Once()
162
163 // Hide will be called by Cancel
164 accentPicker.EXPECT().Hide().Maybe()
165
166 uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() })
167
168 // Start long-press with shift held
169 uc.OnKeyPressed(ctx, 'e', true)
170
171 // Wait for timer
172 wg.Wait()
173
174 // Clean up
175 uc.Cancel(ctx)
176}
177
178func TestInsertAccentUseCase_PickerVisibleBlocksNewLongPress(t *testing.T) {
179 ctx := context.Background()
180 focusProvider := mocks.NewMockFocusedInputProvider(t)
181 accentPicker := mocks.NewMockAccentPickerUI(t)
182 textInput := mocks.NewMockTextInputTarget(t)
183
184 var showCalled int
185 var mu sync.Mutex
186
187 // GetFocusedInput is called when showing picker to delete repeated characters
188 focusProvider.EXPECT().GetFocusedInput().Return(textInput).Maybe()
189 // DeleteBeforeCursor will be called to delete the initial character + any repeats
190 textInput.EXPECT().DeleteBeforeCursor(mock.Anything, mock.AnythingOfType("int")).Return(nil).Maybe()
191
192 accentPicker.EXPECT().
193 Show(mock.Anything, mock.Anything, mock.Anything).
194 Run(func(_ []rune, _ func(rune), _ func()) {
195 mu.Lock()
196 showCalled++
197 mu.Unlock()
198 }).
199 Maybe()
200
201 accentPicker.EXPECT().Hide().Maybe()
202
203 uc := NewInsertAccentUseCase(focusProvider, accentPicker, func(fn func()) { fn() })
204
205 // Trigger first long-press
206 uc.OnKeyPressed(ctx, 'e', false)
207 time.Sleep(LongPressDelay + 50*time.Millisecond)
208
209 // Try to start another long-press while picker is visible
210 handled := uc.OnKeyPressed(ctx, 'a', false)
211 assert.True(t, handled, "should return true when picker is visible")
212
213 mu.Lock()
214 count := showCalled
215 mu.Unlock()
216 assert.LessOrEqual(t, count, 1, "Show should only be called once")
217
218 uc.Cancel(ctx)
219}