main 5041353

insert_accent_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close