navigation_test.go
⎇
Raw
1package e2e
2
3import (
4 "net/url"
5 "slices"
6 "strings"
7 "testing"
8
9 "github.com/PuerkitoBio/goquery"
10)
11
12// goquerySel shortens the selection type used in the Each callbacks below.
13type goquerySel = goquery.Selection
14
15// navSwitchBranch submits the branch selector of the given page the way a
16// browser without JavaScript would: a plain GET of the form action with the
17// form's hidden fields plus the chosen revision.
18func navSwitchBranch(t *testing.T, s *session, page, rev string) *response {
19 t.Helper()
20 r := s.get(page)
21 form := r.Find("form.branch-selector").First()
22 action, ok := form.Attr("action")
23 if !ok {
24 t.Fatalf("no branch selector form on %s", page)
25 }
26 q := url.Values{"rev": {rev}}
27 form.Find(`input[type="hidden"]`).Each(func(_ int, in *goquerySel) {
28 name, _ := in.Attr("name")
29 value, _ := in.Attr("value")
30 if name != "" {
31 q.Set(name, value)
32 }
33 })
34 return s.get(action + "?" + q.Encode())
35}
36
37func TestNavigation(t *testing.T) {
38 e := newEnv(t)
39 admin := e.admin()
40 e.createRepo(admin, "my-repo")
41 e.seedRepo("my-repo", nil)
42
43 // ─── Branch selector ───────────────────────────────────────────────────
44 // A second branch makes the selector meaningful.
45 e.seedBranch("my-repo", "dev", nil)
46
47 t.Run("branch selector appears on repo home", func(t *testing.T) {
48 r := admin.get("/my-repo")
49 if !r.Has(".branch-selector") {
50 t.Fatal("branch-selector missing")
51 }
52 if got := r.Value(".branch-select"); got != "main" {
53 t.Errorf("selected branch = %q", got)
54 }
55 })
56
57 t.Run("branch selector shows all branches on repo home", func(t *testing.T) {
58 opts := admin.get("/my-repo").Texts(".branch-select option")
59 if !slices.Contains(opts, "main") || !slices.Contains(opts, "dev") {
60 t.Errorf("options = %v", opts)
61 }
62 })
63
64 t.Run("branch selector appears on file tree with current ref selected", func(t *testing.T) {
65 r := admin.get("/my-repo/tree/main")
66 if !r.Has(".branch-selector") {
67 t.Fatal("branch-selector missing")
68 }
69 if got := r.Value(".branch-select"); got != "main" {
70 t.Errorf("selected branch = %q", got)
71 }
72 })
73
74 t.Run("branch selector appears on commit log with current ref selected", func(t *testing.T) {
75 r := admin.get("/my-repo/commits/main")
76 if !r.Has(".branch-selector") {
77 t.Fatal("branch-selector missing")
78 }
79 if got := r.Value(".branch-select"); got != "main" {
80 t.Errorf("selected branch = %q", got)
81 }
82 })
83
84 t.Run("branch selector appears on file blob", func(t *testing.T) {
85 r := admin.get("/my-repo/blob/main/README.md")
86 if !r.Has(".branch-selector") {
87 t.Fatal("branch-selector missing")
88 }
89 if got := r.Value(".branch-select"); got != "main" {
90 t.Errorf("selected branch = %q", got)
91 }
92 })
93
94 t.Run("switching branch on commit log navigates to the selected branch", func(t *testing.T) {
95 loc := navSwitchBranch(t, admin, "/my-repo/commits/main", "dev").mustRedirect("/my-repo")
96 if !strings.Contains(loc, "/commits/dev") {
97 t.Errorf("location = %q", loc)
98 }
99 })
100
101 t.Run("switching branch on file tree navigates to the selected branch", func(t *testing.T) {
102 loc := navSwitchBranch(t, admin, "/my-repo/tree/main", "dev").mustRedirect("/my-repo")
103 if !strings.Contains(loc, "/tree/dev") {
104 t.Errorf("location = %q", loc)
105 }
106 })
107
108 t.Run("branch-switch route preserves subpath when switching tree", func(t *testing.T) {
109 loc := admin.get("/my-repo/branch-switch?view=tree&rev=dev&path=src/foo").Location()
110 if !strings.Contains(loc, "/tree/dev/src/foo") {
111 t.Errorf("location = %q", loc)
112 }
113 })
114
115 t.Run("branch-switch route redirects commits view correctly", func(t *testing.T) {
116 loc := admin.get("/my-repo/branch-switch?view=commits&rev=dev").Location()
117 if !strings.Contains(loc, "/commits/dev") {
118 t.Errorf("location = %q", loc)
119 }
120 })
121
122 t.Run("branch-switch route redirects blob view correctly", func(t *testing.T) {
123 loc := admin.get("/my-repo/branch-switch?view=blob&rev=dev&path=README.md").Location()
124 if !strings.Contains(loc, "/blob/dev/README.md") {
125 t.Errorf("location = %q", loc)
126 }
127 })
128
129 // ─── Default branch settings ───────────────────────────────────────────
130
131 t.Run("settings page shows default branch select", func(t *testing.T) {
132 r := admin.get("/my-repo/settings")
133 if !r.Has("#default_branch") {
134 t.Fatal("#default_branch missing")
135 }
136 opts := r.Texts("#default_branch option")
137 if !slices.Contains(opts, "main") || !slices.Contains(opts, "dev") {
138 t.Errorf("options = %v", opts)
139 }
140 })
141
142 t.Run("current default branch is pre-selected", func(t *testing.T) {
143 if got := admin.get("/my-repo/settings").Value("#default_branch"); got != "main" {
144 t.Errorf("default branch = %q", got)
145 }
146 })
147
148 t.Run("changing default branch saves and is reflected in the repo home", func(t *testing.T) {
149 r := admin.follow(admin.post("/my-repo/settings", url.Values{"default_branch": {"dev"}}))
150 if !r.Has(".form-success") {
151 t.Fatal("form-success missing")
152 }
153 if got := r.Value("#default_branch"); got != "dev" {
154 t.Errorf("default branch = %q", got)
155 }
156 if got := admin.get("/my-repo").Value(".branch-select"); got != "dev" {
157 t.Errorf("repo home branch = %q", got)
158 }
159 })
160
161 t.Run("commit log link in repo nav uses the new default branch", func(t *testing.T) {
162 href := admin.get("/my-repo").Attr(`.repo-tab[href*="/commits/"]`, "href")
163 if !strings.Contains(href, "/commits/dev") {
164 t.Errorf("commits href = %q", href)
165 }
166 })
167
168 t.Run("changing default branch back to main restores original state", func(t *testing.T) {
169 r := admin.follow(admin.post("/my-repo/settings", url.Values{"default_branch": {"main"}}))
170 if !r.Has(".form-success") {
171 t.Fatal("form-success missing")
172 }
173 if got := r.Value("#default_branch"); got != "main" {
174 t.Errorf("default branch = %q", got)
175 }
176 })
177
178 t.Run("settings page shows hint instead of select when repo has no branches", func(t *testing.T) {
179 e.createRepo(admin, "empty-for-branch-test")
180 r := admin.get("/empty-for-branch-test/settings")
181 if n := r.Count("#default_branch"); n != 0 {
182 t.Errorf("#default_branch count = %d", n)
183 }
184 if !r.Has(".form-hint") {
185 t.Error("form-hint missing")
186 }
187 })
188
189 // ─── File browser ──────────────────────────────────────────────────────
190 // A subdirectory makes directory navigation testable.
191 e.seedRepo("my-repo", map[string]string{
192 "src/app.ts": "export {};\n",
193 "src/README.md": "# src readme\n",
194 }, "Add src")
195
196 t.Run("repo home shows file tree instead of recent commits", func(t *testing.T) {
197 r := admin.get("/my-repo")
198 if !r.Has(".file-tree") {
199 t.Error("file-tree missing")
200 }
201 if n := r.Count(".repo-commits-section"); n != 0 {
202 t.Errorf("repo-commits-section count = %d", n)
203 }
204 })
205
206 t.Run("repo home file tree lists files and directories", func(t *testing.T) {
207 names := admin.get("/my-repo").Texts(".file-name a")
208 for _, want := range []string{"README.md", "index.js", "src"} {
209 if !slices.Contains(names, want) {
210 t.Errorf("names %v missing %q", names, want)
211 }
212 }
213 })
214
215 t.Run("directories appear before files in file tree", func(t *testing.T) {
216 names := admin.get("/my-repo").Texts(".file-name a")
217 srcIdx := slices.Index(names, "src")
218 readmeIdx := slices.Index(names, "README.md")
219 if srcIdx < 0 || readmeIdx < 0 || srcIdx >= readmeIdx {
220 t.Errorf("src at %d, README.md at %d in %v", srcIdx, readmeIdx, names)
221 }
222 })
223
224 t.Run("no .. entry at repository root", func(t *testing.T) {
225 if names := admin.get("/my-repo").Texts(".file-name a"); slices.Contains(names, "..") {
226 t.Errorf("names = %v", names)
227 }
228 })
229
230 t.Run("clicking directory navigates into it", func(t *testing.T) {
231 href := ""
232 admin.get("/my-repo").Find(".file-name a").Each(func(_ int, s *goquerySel) {
233 if strings.TrimSpace(s.Text()) == "src" {
234 href, _ = s.Attr("href")
235 }
236 })
237 if href != "/my-repo/tree/main/src" {
238 t.Fatalf("src href = %q", href)
239 }
240 admin.get(href).mustStatus(200)
241 })
242
243 t.Run(".. entry appears in subdirectory", func(t *testing.T) {
244 names := admin.get("/my-repo/tree/main/src").Texts(".file-name a")
245 if !slices.Contains(names, "..") {
246 t.Errorf("names = %v", names)
247 }
248 })
249
250 t.Run(".. at one level deep links to tree root", func(t *testing.T) {
251 up := ""
252 admin.get("/my-repo/tree/main/src").Find(".file-name a").Each(func(_ int, s *goquerySel) {
253 if strings.TrimSpace(s.Text()) == ".." {
254 up, _ = s.Attr("href")
255 }
256 })
257 if up != "/my-repo/tree/main" {
258 t.Errorf("up href = %q", up)
259 }
260 })
261
262 t.Run("files in subdirectory show plain names, not full paths", func(t *testing.T) {
263 names := admin.get("/my-repo/tree/main/src").Texts(".file-name a")
264 if !slices.Contains(names, "app.ts") {
265 t.Errorf("names = %v", names)
266 }
267 for _, bad := range []string{"src/app.ts", "src/README.md"} {
268 if slices.Contains(names, bad) {
269 t.Errorf("names %v contains full path %q", names, bad)
270 }
271 }
272 })
273
274 t.Run("readme is shown below file tree on repo home", func(t *testing.T) {
275 // Geometry needs a browser; document order carries the same intent.
276 body := admin.get("/my-repo").BodyString()
277 tree := strings.Index(body, `class="file-tree"`)
278 readme := strings.Index(body, `class="readme-section"`)
279 if tree < 0 || readme < 0 || readme < tree {
280 t.Errorf("file-tree at %d, readme-section at %d", tree, readme)
281 }
282 })
283
284 t.Run("readme in subdirectory is shown when present", func(t *testing.T) {
285 r := admin.get("/my-repo/tree/main/src")
286 if !r.Has(".readme-section") {
287 t.Fatal("readme-section missing")
288 }
289 html, _ := r.Find(".readme-section .markdown-body").Html()
290 if !strings.Contains(html, "src readme") {
291 t.Errorf("readme html = %q", html)
292 }
293 })
294
295 t.Run("file tree on /tree/:ref also shows readme", func(t *testing.T) {
296 r := admin.get("/my-repo/tree/main")
297 if !r.Has(".file-tree") {
298 t.Error("file-tree missing")
299 }
300 if !r.Has(".readme-section") {
301 t.Error("readme-section missing")
302 }
303 })
304}
305