v0.23.2 bfd5eb4

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/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}

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