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