main 5041353

navigation_url_normalizer_test.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"errors"
6	stdurl "net/url"
7	"path/filepath"
8	"testing"
9)
10
11type spyLocalPathResolver struct {
12	calls int
13}
14
15func (r *spyLocalPathResolver) ResolveExistingPath(_ context.Context, _ string) (string, bool, error) {
16	r.calls++
17	return filepath.Join(string(filepath.Separator), "tmp", "should-not-be-used.html"), true, nil
18}
19
20func TestNavigationURLNormalizerResolvesExistingLocalPaths(t *testing.T) {
21	ctx := context.Background()
22	absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")
23	normalizer := NewNavigationURLNormalizer(fakeLocalPathResolver{paths: map[string]string{
24		absFile:          absFile,
25		"page.html":      absFile,
26		"~/page.html":    absFile,
27		"notes:2026.txt": absFile,
28		"http:notes.txt": absFile,
29		"http:notes":     absFile,
30	}})
31
32	for _, input := range []string{absFile, "page.html", "~/page.html", "notes:2026.txt", "http:notes.txt", "http:notes"} {
33		t.Run(input, func(t *testing.T) {
34			got := normalizer.Normalize(ctx, input)
35			want := (&stdurl.URL{Scheme: "file", Path: absFile}).String()
36			if got != want {
37				t.Fatalf("Normalize(%q) = %q, want %q", input, got, want)
38			}
39		})
40	}
41}
42
43func TestNavigationURLNormalizerFallsBackToDomainNormalize(t *testing.T) {
44	ctx := context.Background()
45	normalizer := NewNavigationURLNormalizer(fakeLocalPathResolver{err: errors.New("boom")})
46
47	if got := normalizer.Normalize(ctx, "example.com"); got != "https://example.com" {
48		t.Fatalf("Normalize(example.com) = %q", got)
49	}
50	if got := normalizer.Normalize(ctx, "./missing.html"); got != "./missing.html" {
51		t.Fatalf("Normalize(./missing.html) = %q", got)
52	}
53}
54func TestNavigationURLNormalizerBuildNavigationURLCentralizesSearchFallback(t *testing.T) {
55	ctx := context.Background()
56	absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html")
57	normalizer := NewNavigationURLNormalizer(fakeLocalPathResolver{paths: map[string]string{
58		"page.html": absFile,
59	}})
60	shortcuts := map[string]string{"g": "https://google.com/search?q=%s"}
61
62	if got := normalizer.BuildNavigationURL(ctx, "page.html", shortcuts, "https://search.example/?q=%s"); got != "file://"+absFile {
63		t.Fatalf("BuildNavigationURL(local file) = %q, want file URL", got)
64	}
65	if got := normalizer.BuildNavigationURL(ctx, "!g cats", shortcuts, "https://search.example/?q=%s"); got != "https://google.com/search?q=cats" {
66		t.Fatalf("BuildNavigationURL(shortcut) = %q, want shortcut search", got)
67	}
68}
69
70func TestNavigationURLNormalizerDoesNotProbeSchemeBearingInputs(t *testing.T) {
71	ctx := context.Background()
72
73	tests := []struct {
74		name  string
75		input string
76		want  string
77	}{
78		{
79			name:  "https scheme",
80			input: "https://example.com/page",
81			want:  "https://example.com/page",
82		},
83		{
84			name:  "file scheme",
85			input: "file:///tmp/page.html",
86			want:  "file:///tmp/page.html",
87		},
88		{
89			name:  "about scheme",
90			input: "about:blank",
91			want:  "about:blank",
92		},
93		{
94			name:  "external scheme without slashes",
95			input: "mailto:user@example.com",
96			want:  "mailto:user@example.com",
97		},
98		{
99			name:  "data scheme",
100			input: "data:text/plain,hello",
101			want:  "data:text/plain,hello",
102		},
103	}
104
105	for _, tt := range tests {
106		t.Run(tt.name, func(t *testing.T) {
107			resolver := &spyLocalPathResolver{}
108			normalizer := NewNavigationURLNormalizer(resolver)
109
110			got := normalizer.Normalize(ctx, tt.input)
111			if got != tt.want {
112				t.Fatalf("Normalize(%q) = %q, want %q", tt.input, got, tt.want)
113			}
114			if resolver.calls != 0 {
115				t.Fatalf("ResolveExistingPath called %d times for %q, want 0", resolver.calls, tt.input)
116			}
117		})
118	}
119}

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