v0.30.0 eb3068f

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 TestResetSearchSessionState(t *testing.T) {
37	o := &Omnibox{}
38	o.lastQuery = "github"
39
40	o.resetSearchSessionState()
41
42	if o.lastQuery != "" {
43		t.Fatalf("lastQuery should be reset, got %q", o.lastQuery)
44	}
45}
46
47func TestIsDeletionKey(t *testing.T) {
48	tests := []struct {
49		name string
50		key  uint
51		want bool
52	}{
53		{name: "backspace is deletion", key: uint(gdk.KEY_BackSpace), want: true},
54		{name: "delete is deletion", key: uint(gdk.KEY_Delete), want: true},
55		{name: "typing key is not deletion", key: uint(gdk.KEY_g), want: false},
56		{name: "arrow key is not deletion", key: uint(gdk.KEY_Left), want: false},
57	}
58
59	for _, tt := range tests {
60		t.Run(tt.name, func(t *testing.T) {
61			if got := isDeletionKey(tt.key); got != tt.want {
62				t.Fatalf("isDeletionKey(%d) = %v, want %v", tt.key, got, tt.want)
63			}
64		})
65	}
66}
67
68func TestEffectiveSearchQuery(t *testing.T) {
69	tests := []struct {
70		name      string
71		entryText string
72		realInput string
73		hasGhost  bool
74		want      string
75	}{
76		{name: "uses typed input when ghost visible", entryText: "dumber.bnema.dev", realInput: "dumb", hasGhost: true, want: "dumb"},
77		{name: "falls back to entry when no ghost", entryText: "dumb", realInput: "dumb", hasGhost: false, want: "dumb"},
78		{name: "uses entry when real input unavailable", entryText: "dumb", realInput: "", hasGhost: true, want: "dumb"},
79	}
80
81	for _, tt := range tests {
82		t.Run(tt.name, func(t *testing.T) {
83			if got := effectiveSearchQuery(tt.entryText, tt.realInput, tt.hasGhost); got != tt.want {
84				t.Fatalf("effectiveSearchQuery(%q, %q, %v) = %q, want %q", tt.entryText, tt.realInput, tt.hasGhost, got, tt.want)
85			}
86		})
87	}
88}
89
90func TestResultsContainerState(t *testing.T) {
91	tests := []struct {
92		name            string
93		rowCount        int
94		wantVisible     bool
95		wantExpand      bool
96		wantListVisible bool
97	}{
98		{name: "negative rows stays hidden", rowCount: -1, wantVisible: false, wantExpand: false, wantListVisible: false},
99		{name: "no rows stays hidden", rowCount: 0, wantVisible: false, wantExpand: false, wantListVisible: false},
100		{name: "rows show and expand", rowCount: 3, wantVisible: true, wantExpand: true, wantListVisible: true},
101	}
102
103	for _, tt := range tests {
104		t.Run(tt.name, func(t *testing.T) {
105			gotVisible, gotExpand, gotListVisible := resultsContainerState(tt.rowCount)
106			if gotVisible != tt.wantVisible || gotExpand != tt.wantExpand || gotListVisible != tt.wantListVisible {
107				t.Fatalf(
108					"resultsContainerState(%d) = (%v, %v, %v), want (%v, %v, %v)",
109					tt.rowCount,
110					gotVisible,
111					gotExpand,
112					gotListVisible,
113					tt.wantVisible,
114					tt.wantExpand,
115					tt.wantListVisible,
116				)
117			}
118		})
119	}
120}
121
122func TestUpdateGhostFromSelectionUsesRealInputWhenGhostVisible(t *testing.T) {
123	entryText := "github.com/bnema/dumber"
124	realInput := "upl"
125
126	got := effectiveSearchQuery(entryText, realInput, true)
127	if got != realInput {
128		t.Fatalf("effectiveSearchQuery should prefer real input when ghost is visible, got %q want %q", got, realInput)
129	}
130}
131
132func TestResolveTargetURLForSelection(t *testing.T) {
133	suggestions := []Suggestion{
134		{URL: "https://github.com/bnema/dumber"},
135		{URL: "https://github.com/bnema/dumber/pulls"},
136		{URL: "https://github.com/bnema/dumber/issues"},
137	}
138	favorites := []Favorite{
139		{URL: "https://dumber.bnema.dev"},
140		{URL: "https://docs.dumber.bnema.dev"},
141	}
142
143	tests := []struct {
144		name    string
145		mode    ViewMode
146		index   int
147		limit   int
148		wantURL string
149	}{
150		{name: "history index", mode: ViewModeHistory, index: 1, limit: 10, wantURL: "https://github.com/bnema/dumber/pulls"},
151		{name: "favorites index", mode: ViewModeFavorites, index: 0, limit: 10, wantURL: "https://dumber.bnema.dev"},
152		{name: "invalid index", mode: ViewModeHistory, index: 99, limit: 10, wantURL: ""},
153		{name: "history index beyond visible limit is hidden", mode: ViewModeHistory, index: 2, limit: 2, wantURL: ""},
154		{name: "favorites index beyond visible limit is hidden", mode: ViewModeFavorites, index: 1, limit: 1, wantURL: ""},
155	}
156
157	for _, tt := range tests {
158		t.Run(tt.name, func(t *testing.T) {
159			got := resolveTargetURLForSelection(tt.mode, tt.index, tt.limit, suggestions, favorites)
160			if got != tt.wantURL {
161				t.Fatalf("resolveTargetURLForSelection(%s, %d) = %q, want %q", tt.mode, tt.index, got, tt.wantURL)
162			}
163		})
164	}
165}
166
167func TestSelectedTargetURL(t *testing.T) {
168	suggestions := []Suggestion{
169		{URL: "https://github.com/bnema/dumber"},
170		{URL: "https://github.com/bnema/dumber/pulls"},
171	}
172	favorites := []Favorite{
173		{URL: "https://dumber.bnema.dev"},
174		{URL: "https://docs.dumber.bnema.dev"},
175	}
176
177	tests := []struct {
178		name     string
179		mode     ViewMode
180		index    int
181		limit    int
182		wantURL  string
183		wantBool bool
184	}{
185		{name: "negative index is not explicit selection", mode: ViewModeHistory, index: -1, limit: 10, wantURL: "", wantBool: false},
186		{name: "history selection is explicit", mode: ViewModeHistory, index: 0, limit: 10, wantURL: "https://github.com/bnema/dumber", wantBool: true},
187		{name: "favorites selection is explicit", mode: ViewModeFavorites, index: 0, limit: 10, wantURL: "https://dumber.bnema.dev", wantBool: true},
188		{name: "hidden history selection is ignored", mode: ViewModeHistory, index: 1, limit: 1, wantURL: "", wantBool: true},
189		{name: "hidden favorites selection is ignored", mode: ViewModeFavorites, index: 1, limit: 1, wantURL: "", wantBool: true},
190	}
191
192	for _, tt := range tests {
193		t.Run(tt.name, func(t *testing.T) {
194			gotURL, gotBool := selectedTargetURL(tt.mode, tt.index, tt.limit, suggestions, favorites)
195			if gotURL != tt.wantURL || gotBool != tt.wantBool {
196				t.Fatalf("selectedTargetURL(%s, %d) = (%q, %v), want (%q, %v)", tt.mode, tt.index, gotURL, gotBool, tt.wantURL, tt.wantBool)
197			}
198		})
199	}
200}
201
202func TestBangSuggestionTextAt(t *testing.T) {
203	bangSuggestions := []BangSuggestion{{Key: "g"}, {Key: "gh"}}
204
205	if got, ok := bangSuggestionTextAt(1, bangSuggestions); !ok || got != "!gh " {
206		t.Fatalf("bangSuggestionTextAt(1) = (%q, %v), want (%q, %v)", got, ok, "!gh ", true)
207	}
208	if got, ok := bangSuggestionTextAt(99, bangSuggestions); ok || got != "" {
209		t.Fatalf("bangSuggestionTextAt(out of range) = (%q, %v), want (%q, %v)", got, ok, "", false)
210	}
211}
212
213func TestVisibleGhostSuggestion(t *testing.T) {
214	suggestions := []Suggestion{
215		{URL: "https://github.com/bnema/dumber"},
216		{URL: "https://gitlab.com/team/project"},
217	}
218
219	tests := []struct {
220		name                 string
221		input                string
222		selectedURL          string
223		hasExplicitSelection bool
224		wantFull             string
225		wantSuffix           string
226		wantOK               bool
227	}{
228		{
229			name:       "top visible candidate drives ghost without selection",
230			input:      "git",
231			wantFull:   "github.com",
232			wantSuffix: "hub.com",
233			wantOK:     true,
234		},
235		{
236			name:                 "explicit selection wins over top candidate",
237			input:                "git",
238			selectedURL:          "https://gitlab.com/team/project",
239			hasExplicitSelection: true,
240			wantFull:             "gitlab.com/team/project",
241			wantSuffix:           "lab.com/team/project",
242			wantOK:               true,
243		},
244		{
245			name:       "no visible prefix match means no ghost",
246			input:      "xyz",
247			wantFull:   "",
248			wantSuffix: "",
249			wantOK:     false,
250		},
251	}
252
253	for _, tt := range tests {
254		t.Run(tt.name, func(t *testing.T) {
255			gotFull, gotSuffix, gotOK := visibleGhostSuggestion(
256				tt.input,
257				tt.selectedURL,
258				tt.hasExplicitSelection,
259				ViewModeHistory,
260				10,
261				suggestions,
262				nil,
263			)
264			if gotFull != tt.wantFull || gotSuffix != tt.wantSuffix || gotOK != tt.wantOK {
265				t.Fatalf(
266					"visibleGhostSuggestion(%q) = (%q, %q, %v), want (%q, %q, %v)",
267					tt.input,
268					gotFull,
269					gotSuffix,
270					gotOK,
271					tt.wantFull,
272					tt.wantSuffix,
273					tt.wantOK,
274				)
275			}
276		})
277	}
278}
279
280func TestVisibleGhostSuggestionRespectsVisibleLimit(t *testing.T) {
281	suggestions := []Suggestion{
282		{URL: "https://github.com/bnema/dumber"},
283		{URL: "https://gitlab.com/team/project"},
284	}
285
286	gotFull, gotSuffix, gotOK := visibleGhostSuggestion("gitl", "", false, ViewModeHistory, 1, suggestions, nil)
287	if gotFull != "" || gotSuffix != "" || gotOK {
288		t.Fatalf("visibleGhostSuggestion should ignore hidden suggestions, got (%q, %q, %v)", gotFull, gotSuffix, gotOK)
289	}
290}
291
292func TestShouldPromoteHoverSelection(t *testing.T) {
293	tests := []struct {
294		name         string
295		realInput    string
296		hasGhostText bool
297		hasNavigated bool
298		want         bool
299	}{
300		{
301			name: "initial open allows hover selection",
302			want: true,
303		},
304		{
305			name:      "typed input keeps hover from stealing authority",
306			realInput: "git",
307			want:      false,
308		},
309		{
310			name:         "visible ghost keeps hover from stealing authority",
311			hasGhostText: true,
312			want:         false,
313		},
314		{
315			name:         "keyboard navigation keeps hover from stealing authority",
316			hasNavigated: true,
317			want:         false,
318		},
319	}
320
321	for _, tt := range tests {
322		t.Run(tt.name, func(t *testing.T) {
323			got := shouldPromoteHoverSelection(tt.realInput, tt.hasGhostText, tt.hasNavigated)
324			if got != tt.want {
325				t.Fatalf(
326					"shouldPromoteHoverSelection(%q, %v, %v) = %v, want %v",
327					tt.realInput,
328					tt.hasGhostText,
329					tt.hasNavigated,
330					got,
331					tt.want,
332				)
333			}
334		})
335	}
336}

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