main 5041353

omnibox_navigation_state_test.go RawHistoryFolder

1package component
2
3import (
4	"testing"
5
6	"github.com/bnema/puregotk/v4/gdk"
7)
8
9func TestShouldPreferTypedURLNavigation(t *testing.T) {
10	tests := []struct {
11		name  string
12		input string
13		want  bool
14	}{
15		{name: "domain", input: "x.com", want: true},
16		{name: "domain with path", input: "github.com/bnema", want: true},
17		{name: "http url", input: "http://example.com", want: true},
18		{name: "localhost", input: "localhost", want: true},
19		{name: "ipv4", input: "127.0.0.1", want: true},
20		{name: "ipv4 with port", input: "192.168.1.1:8080", want: true},
21		{name: "ipv6 bracketed", input: "[::1]", want: true},
22		{name: "http ipv6 bracketed", input: "http://[::1]", want: true},
23		{name: "search query", input: "hello world", want: false},
24		{name: "empty", input: "", want: false},
25	}
26
27	for _, tt := range tests {
28		t.Run(tt.name, func(t *testing.T) {
29			if got := shouldPreferTypedURLNavigation(tt.input); got != tt.want {
30				t.Fatalf("shouldPreferTypedURLNavigation(%q) = %v, want %v", tt.input, got, tt.want)
31			}
32		})
33	}
34}
35
36func TestPasteSubmissionStateDefersEnterUntilTextArrives(t *testing.T) {
37	state := pasteSubmissionState{}
38	state.beginPaste("")
39
40	if submit, _ := state.requestSubmit(""); submit {
41		t.Fatal("Enter must not submit while the paste buffer is still empty")
42	}
43	if !state.textChanged() {
44		t.Fatal("the completed paste must consume the deferred submission")
45	}
46	if state.textChanged() {
47		t.Fatal("the deferred submission must be consumed exactly once")
48	}
49}
50
51func TestPasteSubmissionStateCompletesWhenPasteKeepsTheSameText(t *testing.T) {
52	state := pasteSubmissionState{}
53	state.beginPaste("https://example.com")
54
55	if submit, _ := state.requestSubmit("https://example.com"); submit {
56		t.Fatal("Enter must be deferred while the clipboard read is still in flight")
57	}
58	if !state.textChanged() {
59		t.Fatal("an identical replacement must consume the deferred submission")
60	}
61	if state.textChanged() {
62		t.Fatal("the deferred submission must be consumed exactly once")
63	}
64}
65
66func TestPasteSubmissionStateRecognizesPasteThatArrivesBeforeEnter(t *testing.T) {
67	state := pasteSubmissionState{}
68	state.beginPaste("old")
69
70	submit, pastedTextReady := state.requestSubmit("new")
71	if !submit || !pastedTextReady {
72		t.Fatalf("completed paste should submit immediately, got submit=%v ready=%v", submit, pastedTextReady)
73	}
74}
75
76func TestPasteSubmissionStateResetCancelsDeferredEnter(t *testing.T) {
77	state := pasteSubmissionState{}
78	state.beginPaste("")
79	state.requestSubmit("")
80	state.reset()
81
82	if state.textChanged() {
83		t.Fatal("a later edit must not consume a canceled paste submission")
84	}
85}
86
87func TestPasteSubmissionStateReplaysEnterWhenPasteKeepsGhostSuffixText(t *testing.T) {
88	state := pasteSubmissionState{}
89	fullText := "https://example.com/guide"
90	state.beginPaste(fullText)
91
92	if submit, _ := state.requestSubmit(fullText); submit {
93		t.Fatal("Enter must be deferred while the clipboard replacement is pending")
94	}
95	if !state.textChanged() {
96		t.Fatal("an unchanged replacement matching ghost text must replay deferred Enter")
97	}
98	if state.textChanged() {
99		t.Fatal("the replay must be consumed exactly once")
100	}
101}
102
103func TestGhostEchoDetectionKeepsSelfGeneratedCompletion(t *testing.T) {
104	realInput := "https://example.com/gu"
105	ghostSuffix := "ide"
106	entryText := realInput + ghostSuffix
107
108	if !isGhostEcho(entryText, realInput, ghostSuffix) {
109		t.Fatal("the unchanged completion text must be recognized as a ghost echo")
110	}
111	if isGhostEcho(entryText+"/x", realInput, ghostSuffix) {
112		t.Fatal("text beyond the completion must not be treated as a ghost echo")
113	}
114	if isGhostEcho(entryText, realInput, "") {
115		t.Fatal("absent ghost text must not be treated as a ghost echo")
116	}
117}
118
119func TestResetSearchSessionState(t *testing.T) {
120	o := &Omnibox{}
121	o.lastQuery = "github"
122
123	o.resetSearchSessionState()
124
125	if o.lastQuery != "" {
126		t.Fatalf("lastQuery should be reset, got %q", o.lastQuery)
127	}
128}
129
130func TestIsDeletionKey(t *testing.T) {
131	tests := []struct {
132		name string
133		key  uint
134		want bool
135	}{
136		{name: "backspace is deletion", key: uint(gdk.KEY_BackSpace), want: true},
137		{name: "delete is deletion", key: uint(gdk.KEY_Delete), want: true},
138		{name: "typing key is not deletion", key: uint(gdk.KEY_g), want: false},
139		{name: "arrow key is not deletion", key: uint(gdk.KEY_Left), want: false},
140	}
141
142	for _, tt := range tests {
143		t.Run(tt.name, func(t *testing.T) {
144			if got := isDeletionKey(tt.key); got != tt.want {
145				t.Fatalf("isDeletionKey(%d) = %v, want %v", tt.key, got, tt.want)
146			}
147		})
148	}
149}
150
151func TestEffectiveSearchQuery(t *testing.T) {
152	tests := []struct {
153		name      string
154		entryText string
155		realInput string
156		hasGhost  bool
157		want      string
158	}{
159		{name: "uses typed input when ghost visible", entryText: "bnema.dev/dumber", realInput: "dumb", hasGhost: true, want: "dumb"},
160		{name: "falls back to entry when no ghost", entryText: "dumb", realInput: "dumb", hasGhost: false, want: "dumb"},
161		{name: "uses entry when real input unavailable", entryText: "dumb", realInput: "", hasGhost: true, want: "dumb"},
162	}
163
164	for _, tt := range tests {
165		t.Run(tt.name, func(t *testing.T) {
166			if got := effectiveSearchQuery(tt.entryText, tt.realInput, tt.hasGhost); got != tt.want {
167				t.Fatalf("effectiveSearchQuery(%q, %q, %v) = %q, want %q", tt.entryText, tt.realInput, tt.hasGhost, got, tt.want)
168			}
169		})
170	}
171}
172
173func TestResultsContainerState(t *testing.T) {
174	tests := []struct {
175		name            string
176		rowCount        int
177		wantVisible     bool
178		wantExpand      bool
179		wantListVisible bool
180	}{
181		{name: "negative rows stays hidden", rowCount: -1, wantVisible: false, wantExpand: false, wantListVisible: false},
182		{name: "no rows stays hidden", rowCount: 0, wantVisible: false, wantExpand: false, wantListVisible: false},
183		{name: "rows show and expand", rowCount: 3, wantVisible: true, wantExpand: true, wantListVisible: true},
184	}
185
186	for _, tt := range tests {
187		t.Run(tt.name, func(t *testing.T) {
188			gotVisible, gotExpand, gotListVisible := resultsContainerState(tt.rowCount)
189			if gotVisible != tt.wantVisible || gotExpand != tt.wantExpand || gotListVisible != tt.wantListVisible {
190				t.Fatalf(
191					"resultsContainerState(%d) = (%v, %v, %v), want (%v, %v, %v)",
192					tt.rowCount,
193					gotVisible,
194					gotExpand,
195					gotListVisible,
196					tt.wantVisible,
197					tt.wantExpand,
198					tt.wantListVisible,
199				)
200			}
201		})
202	}
203}
204
205func TestUpdateGhostFromSelectionUsesRealInputWhenGhostVisible(t *testing.T) {
206	entryText := "github.com/bnema/dumber"
207	realInput := "upl"
208
209	got := effectiveSearchQuery(entryText, realInput, true)
210	if got != realInput {
211		t.Fatalf("effectiveSearchQuery should prefer real input when ghost is visible, got %q want %q", got, realInput)
212	}
213}
214
215func TestResolveTargetURLForSelection(t *testing.T) {
216	suggestions := []Suggestion{
217		{URL: "https://github.com/bnema/dumber"},
218		{URL: "https://github.com/bnema/dumber/pulls"},
219		{URL: "https://github.com/bnema/dumber/issues"},
220	}
221	favorites := []Favorite{
222		{URL: "https://bnema.dev/dumber"},
223		{URL: "https://bnema.dev/dumber/docs"},
224	}
225
226	tests := []struct {
227		name    string
228		mode    ViewMode
229		index   int
230		limit   int
231		wantURL string
232	}{
233		{name: "history index", mode: ViewModeHistory, index: 1, limit: 10, wantURL: "https://github.com/bnema/dumber/pulls"},
234		{name: "favorites index", mode: ViewModeFavorites, index: 0, limit: 10, wantURL: "https://bnema.dev/dumber"},
235		{name: "invalid index", mode: ViewModeHistory, index: 99, limit: 10, wantURL: ""},
236		{name: "history index beyond visible limit is hidden", mode: ViewModeHistory, index: 2, limit: 2, wantURL: ""},
237		{name: "favorites index beyond visible limit is hidden", mode: ViewModeFavorites, index: 1, limit: 1, wantURL: ""},
238	}
239
240	for _, tt := range tests {
241		t.Run(tt.name, func(t *testing.T) {
242			got := resolveTargetURLForSelection(tt.mode, tt.index, tt.limit, suggestions, favorites)
243			if got != tt.wantURL {
244				t.Fatalf("resolveTargetURLForSelection(%s, %d) = %q, want %q", tt.mode, tt.index, got, tt.wantURL)
245			}
246		})
247	}
248}
249
250func TestSelectedTargetURL(t *testing.T) {
251	suggestions := []Suggestion{
252		{URL: "https://github.com/bnema/dumber"},
253		{URL: "https://github.com/bnema/dumber/pulls"},
254	}
255	favorites := []Favorite{
256		{URL: "https://bnema.dev/dumber"},
257		{URL: "https://bnema.dev/dumber/docs"},
258	}
259
260	tests := []struct {
261		name     string
262		mode     ViewMode
263		index    int
264		limit    int
265		wantURL  string
266		wantBool bool
267	}{
268		{name: "negative index is not explicit selection", mode: ViewModeHistory, index: -1, limit: 10, wantURL: "", wantBool: false},
269		{name: "history selection is explicit", mode: ViewModeHistory, index: 0, limit: 10, wantURL: "https://github.com/bnema/dumber", wantBool: true},
270		{name: "favorites selection is explicit", mode: ViewModeFavorites, index: 0, limit: 10, wantURL: "https://bnema.dev/dumber", wantBool: true},
271		{name: "hidden history selection is ignored", mode: ViewModeHistory, index: 1, limit: 1, wantURL: "", wantBool: true},
272		{name: "hidden favorites selection is ignored", mode: ViewModeFavorites, index: 1, limit: 1, wantURL: "", wantBool: true},
273	}
274
275	for _, tt := range tests {
276		t.Run(tt.name, func(t *testing.T) {
277			gotURL, gotBool := selectedTargetURL(tt.mode, tt.index, tt.limit, suggestions, favorites)
278			if gotURL != tt.wantURL || gotBool != tt.wantBool {
279				t.Fatalf("selectedTargetURL(%s, %d) = (%q, %v), want (%q, %v)", tt.mode, tt.index, gotURL, gotBool, tt.wantURL, tt.wantBool)
280			}
281		})
282	}
283}
284
285func TestBangSuggestionTextAt(t *testing.T) {
286	bangSuggestions := []BangSuggestion{{Key: "g"}, {Key: "gh"}}
287
288	if got, ok := bangSuggestionTextAt(1, bangSuggestions); !ok || got != "!gh " {
289		t.Fatalf("bangSuggestionTextAt(1) = (%q, %v), want (%q, %v)", got, ok, "!gh ", true)
290	}
291	if got, ok := bangSuggestionTextAt(99, bangSuggestions); ok || got != "" {
292		t.Fatalf("bangSuggestionTextAt(out of range) = (%q, %v), want (%q, %v)", got, ok, "", false)
293	}
294}
295
296func TestVisibleGhostSuggestion(t *testing.T) {
297	suggestions := []Suggestion{
298		{URL: "https://github.com/bnema/dumber"},
299		{URL: "https://gitlab.com/team/project"},
300	}
301
302	tests := []struct {
303		name                 string
304		input                string
305		selectedURL          string
306		hasExplicitSelection bool
307		wantFull             string
308		wantSuffix           string
309		wantOK               bool
310	}{
311		{
312			name:       "top visible candidate drives ghost without selection",
313			input:      "git",
314			wantFull:   "github.com",
315			wantSuffix: "hub.com",
316			wantOK:     true,
317		},
318		{
319			name:                 "explicit selection wins over top candidate",
320			input:                "git",
321			selectedURL:          "https://gitlab.com/team/project",
322			hasExplicitSelection: true,
323			wantFull:             "gitlab.com/team/project",
324			wantSuffix:           "lab.com/team/project",
325			wantOK:               true,
326		},
327		{
328			name:       "no visible prefix match means no ghost",
329			input:      "xyz",
330			wantFull:   "",
331			wantSuffix: "",
332			wantOK:     false,
333		},
334	}
335
336	for _, tt := range tests {
337		t.Run(tt.name, func(t *testing.T) {
338			gotFull, gotSuffix, gotOK := visibleGhostSuggestion(
339				tt.input,
340				tt.selectedURL,
341				tt.hasExplicitSelection,
342				ViewModeHistory,
343				10,
344				suggestions,
345				nil,
346			)
347			if gotFull != tt.wantFull || gotSuffix != tt.wantSuffix || gotOK != tt.wantOK {
348				t.Fatalf(
349					"visibleGhostSuggestion(%q) = (%q, %q, %v), want (%q, %q, %v)",
350					tt.input,
351					gotFull,
352					gotSuffix,
353					gotOK,
354					tt.wantFull,
355					tt.wantSuffix,
356					tt.wantOK,
357				)
358			}
359		})
360	}
361}
362
363func TestVisibleGhostSuggestionRespectsResultLimit(t *testing.T) {
364	suggestions := []Suggestion{
365		{URL: "https://github.com/bnema/dumber"},
366		{URL: "https://gitlab.com/team/project"},
367	}
368
369	gotFull, gotSuffix, gotOK := visibleGhostSuggestion("gitl", "", false, ViewModeHistory, 1, suggestions, nil)
370	if gotFull != "" || gotSuffix != "" || gotOK {
371		t.Fatalf("visibleGhostSuggestion should ignore hidden suggestions, got (%q, %q, %v)", gotFull, gotSuffix, gotOK)
372	}
373}
374
375func TestShouldPromoteHoverSelection(t *testing.T) {
376	tests := []struct {
377		name         string
378		realInput    string
379		hasGhostText bool
380		hasNavigated bool
381		want         bool
382	}{
383		{
384			name: "initial open allows hover selection",
385			want: true,
386		},
387		{
388			name:      "typed input keeps hover from stealing authority",
389			realInput: "git",
390			want:      false,
391		},
392		{
393			name:         "visible ghost keeps hover from stealing authority",
394			hasGhostText: true,
395			want:         false,
396		},
397		{
398			name:         "keyboard navigation keeps hover from stealing authority",
399			hasNavigated: true,
400			want:         false,
401		},
402	}
403
404	for _, tt := range tests {
405		t.Run(tt.name, func(t *testing.T) {
406			got := shouldPromoteHoverSelection(tt.realInput, tt.hasGhostText, tt.hasNavigated)
407			if got != tt.want {
408				t.Fatalf(
409					"shouldPromoteHoverSelection(%q, %v, %v) = %v, want %v",
410					tt.realInput,
411					tt.hasGhostText,
412					tt.hasNavigated,
413					got,
414					tt.want,
415				)
416			}
417		})
418	}
419}

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