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}