modal_test.go RawHistoryFolder

1package input
2
3import (
4	"context"
5	"sync"
6	"testing"
7	"time"
8)
9
10func TestModalState_InitialState(t *testing.T) {
11	ctx := context.Background()
12	ms := NewModalState(ctx)
13
14	if ms.Mode() != ModeNormal {
15		t.Errorf("initial mode = %v, want ModeNormal", ms.Mode())
16	}
17}
18
19func TestModalState_EnterTabMode(t *testing.T) {
20	ctx := context.Background()
21	ms := NewModalState(ctx)
22
23	ms.EnterTabMode(ctx, 0)
24
25	if ms.Mode() != ModeTab {
26		t.Errorf("mode after EnterTabMode = %v, want ModeTab", ms.Mode())
27	}
28}
29
30func TestModalState_EnterPaneMode(t *testing.T) {
31	ctx := context.Background()
32	ms := NewModalState(ctx)
33
34	ms.EnterPaneMode(ctx, 0)
35
36	if ms.Mode() != ModePane {
37		t.Errorf("mode after EnterPaneMode = %v, want ModePane", ms.Mode())
38	}
39}
40
41func TestModalState_ExitMode(t *testing.T) {
42	ctx := context.Background()
43	ms := NewModalState(ctx)
44
45	// Enter tab mode
46	ms.EnterTabMode(ctx, 0)
47	if ms.Mode() != ModeTab {
48		t.Fatalf("mode should be ModeTab")
49	}
50
51	// Exit mode
52	ms.ExitMode(ctx)
53	if ms.Mode() != ModeNormal {
54		t.Errorf("mode after ExitMode = %v, want ModeNormal", ms.Mode())
55	}
56}
57
58func TestModalState_ExitMode_FromNormal(t *testing.T) {
59	ctx := context.Background()
60	ms := NewModalState(ctx)
61
62	// ExitMode from normal should be no-op
63	ms.ExitMode(ctx)
64
65	if ms.Mode() != ModeNormal {
66		t.Errorf("mode = %v, want ModeNormal", ms.Mode())
67	}
68}
69
70func TestModalState_SwitchModes(t *testing.T) {
71	ctx := context.Background()
72	ms := NewModalState(ctx)
73
74	// Enter tab mode
75	ms.EnterTabMode(ctx, 0)
76	if ms.Mode() != ModeTab {
77		t.Fatalf("expected ModeTab, got %v", ms.Mode())
78	}
79
80	// Switch to pane mode directly
81	ms.EnterPaneMode(ctx, 0)
82	if ms.Mode() != ModePane {
83		t.Errorf("mode after switch to pane = %v, want ModePane", ms.Mode())
84	}
85
86	// Switch back to tab mode
87	ms.EnterTabMode(ctx, 0)
88	if ms.Mode() != ModeTab {
89		t.Errorf("mode after switch to tab = %v, want ModeTab", ms.Mode())
90	}
91}
92
93func TestModalState_ModeChangeCallback(t *testing.T) {
94	ctx := context.Background()
95	ms := NewModalState(ctx)
96
97	var callCount int
98	var lastFrom, lastTo Mode
99	var mu sync.Mutex
100
101	ms.SetOnModeChange(func(from, to Mode) {
102		mu.Lock()
103		defer mu.Unlock()
104		callCount++
105		lastFrom = from
106		lastTo = to
107	})
108
109	// Enter tab mode
110	ms.EnterTabMode(ctx, 0)
111
112	mu.Lock()
113	if callCount != 1 {
114		t.Errorf("callback count = %d, want 1", callCount)
115	}
116	if lastFrom != ModeNormal || lastTo != ModeTab {
117		t.Errorf("callback args = (%v, %v), want (ModeNormal, ModeTab)", lastFrom, lastTo)
118	}
119	mu.Unlock()
120
121	// Exit mode
122	ms.ExitMode(ctx)
123
124	mu.Lock()
125	if callCount != 2 {
126		t.Errorf("callback count = %d, want 2", callCount)
127	}
128	if lastFrom != ModeTab || lastTo != ModeNormal {
129		t.Errorf("callback args = (%v, %v), want (ModeTab, ModeNormal)", lastFrom, lastTo)
130	}
131	mu.Unlock()
132}
133
134func TestModalState_NoCallbackWhenAlreadyInMode(t *testing.T) {
135	ctx := context.Background()
136	ms := NewModalState(ctx)
137
138	var callCount int
139	ms.SetOnModeChange(func(from, to Mode) {
140		callCount++
141	})
142
143	// Enter tab mode
144	ms.EnterTabMode(ctx, 0)
145	if callCount != 1 {
146		t.Fatalf("expected 1 callback, got %d", callCount)
147	}
148
149	// Enter tab mode again (should not trigger callback)
150	ms.EnterTabMode(ctx, 0)
151	if callCount != 1 {
152		t.Errorf("callback count = %d, want 1 (no new call)", callCount)
153	}
154}
155
156func TestModalState_Timeout(t *testing.T) {
157	ctx := context.Background()
158	ms := NewModalState(ctx)
159
160	// Enter with short timeout
161	ms.EnterTabMode(ctx, 50*time.Millisecond)
162
163	if ms.Mode() != ModeTab {
164		t.Fatalf("mode should be ModeTab")
165	}
166
167	// Wait for timeout
168	time.Sleep(100 * time.Millisecond)
169
170	if ms.Mode() != ModeNormal {
171		t.Errorf("mode after timeout = %v, want ModeNormal", ms.Mode())
172	}
173}
174
175func TestModalState_TimeoutReset(t *testing.T) {
176	ctx := context.Background()
177	ms := NewModalState(ctx)
178
179	// Enter with timeout
180	ms.EnterTabMode(ctx, 100*time.Millisecond)
181
182	// Wait a bit, then reset
183	time.Sleep(50 * time.Millisecond)
184	ms.ResetTimeout(ctx)
185
186	// Wait a bit more (original timeout would have expired)
187	time.Sleep(80 * time.Millisecond)
188
189	// Should still be in tab mode because timeout was reset
190	if ms.Mode() != ModeTab {
191		t.Errorf("mode = %v, want ModeTab (timeout should have been reset)", ms.Mode())
192	}
193
194	// Wait for the reset timeout to expire
195	time.Sleep(50 * time.Millisecond)
196
197	if ms.Mode() != ModeNormal {
198		t.Errorf("mode after reset timeout = %v, want ModeNormal", ms.Mode())
199	}
200}
201
202func TestModalState_ExitCancelsTimeout(t *testing.T) {
203	ctx := context.Background()
204	ms := NewModalState(ctx)
205
206	var callCount int
207	ms.SetOnModeChange(func(from, to Mode) {
208		callCount++
209	})
210
211	// Enter with timeout
212	ms.EnterTabMode(ctx, 100*time.Millisecond)
213	if callCount != 1 {
214		t.Fatalf("expected 1 callback")
215	}
216
217	// Exit immediately
218	ms.ExitMode(ctx)
219	if callCount != 2 {
220		t.Fatalf("expected 2 callbacks")
221	}
222
223	// Wait past the timeout
224	time.Sleep(150 * time.Millisecond)
225
226	// Callback should not have been called again
227	if callCount != 2 {
228		t.Errorf("callback count = %d, want 2 (timeout should have been canceled)", callCount)
229	}
230}
231
232func TestModalState_ConcurrentAccess(t *testing.T) {
233	ctx := context.Background()
234	ms := NewModalState(ctx)
235
236	var wg sync.WaitGroup
237	iterations := 100
238
239	// Multiple goroutines entering/exiting modes
240	for range 10 {
241		wg.Go(func() {
242			for range iterations {
243				ms.EnterTabMode(ctx, 0)
244				_ = ms.Mode()
245				ms.ExitMode(ctx)
246				ms.EnterPaneMode(ctx, 0)
247				_ = ms.Mode()
248				ms.ExitMode(ctx)
249			}
250		})
251	}
252
253	wg.Wait()
254
255	// Should end up in normal mode
256	if ms.Mode() != ModeNormal {
257		t.Errorf("final mode = %v, want ModeNormal", ms.Mode())
258	}
259}
260
261func TestMode_String(t *testing.T) {
262	tests := []struct {
263		mode Mode
264		want string
265	}{
266		{ModeNormal, "normal"},
267		{ModeTab, "tab"},
268		{ModePane, "pane"},
269		{Mode(99), "unknown"},
270	}
271
272	for _, tt := range tests {
273		t.Run(tt.want, func(t *testing.T) {
274			if got := tt.mode.String(); got != tt.want {
275				t.Errorf("Mode.String() = %v, want %v", got, tt.want)
276			}
277		})
278	}
279}

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