package e2e import ( "net/url" "slices" "strings" "testing" "github.com/PuerkitoBio/goquery" ) // goquerySel shortens the selection type used in the Each callbacks below. type goquerySel = goquery.Selection // navSwitchBranch submits the branch selector of the given page the way a // browser without JavaScript would: a plain GET of the form action with the // form's hidden fields plus the chosen revision. func navSwitchBranch(t *testing.T, s *session, page, rev string) *response { t.Helper() r := s.get(page) form := r.Find("form.branch-selector").First() action, ok := form.Attr("action") if !ok { t.Fatalf("no branch selector form on %s", page) } q := url.Values{"rev": {rev}} form.Find(`input[type="hidden"]`).Each(func(_ int, in *goquerySel) { name, _ := in.Attr("name") value, _ := in.Attr("value") if name != "" { q.Set(name, value) } }) return s.get(action + "?" + q.Encode()) } func TestNavigation(t *testing.T) { e := newEnv(t) admin := e.admin() e.createRepo(admin, "my-repo") e.seedRepo("my-repo", nil) // ─── Branch selector ─────────────────────────────────────────────────── // A second branch makes the selector meaningful. e.seedBranch("my-repo", "dev", nil) t.Run("branch selector appears on repo home", func(t *testing.T) { r := admin.get("/my-repo") if !r.Has(".branch-selector") { t.Fatal("branch-selector missing") } if got := r.Value(".branch-select"); got != "main" { t.Errorf("selected branch = %q", got) } }) t.Run("branch selector shows all branches on repo home", func(t *testing.T) { opts := admin.get("/my-repo").Texts(".branch-select option") if !slices.Contains(opts, "main") || !slices.Contains(opts, "dev") { t.Errorf("options = %v", opts) } }) t.Run("branch selector appears on file tree with current ref selected", func(t *testing.T) { r := admin.get("/my-repo/tree/main") if !r.Has(".branch-selector") { t.Fatal("branch-selector missing") } if got := r.Value(".branch-select"); got != "main" { t.Errorf("selected branch = %q", got) } }) t.Run("branch selector appears on commit log with current ref selected", func(t *testing.T) { r := admin.get("/my-repo/commits/main") if !r.Has(".branch-selector") { t.Fatal("branch-selector missing") } if got := r.Value(".branch-select"); got != "main" { t.Errorf("selected branch = %q", got) } }) t.Run("branch selector appears on file blob", func(t *testing.T) { r := admin.get("/my-repo/blob/main/README.md") if !r.Has(".branch-selector") { t.Fatal("branch-selector missing") } if got := r.Value(".branch-select"); got != "main" { t.Errorf("selected branch = %q", got) } }) t.Run("switching branch on commit log navigates to the selected branch", func(t *testing.T) { loc := navSwitchBranch(t, admin, "/my-repo/commits/main", "dev").mustRedirect("/my-repo") if !strings.Contains(loc, "/commits/dev") { t.Errorf("location = %q", loc) } }) t.Run("switching branch on file tree navigates to the selected branch", func(t *testing.T) { loc := navSwitchBranch(t, admin, "/my-repo/tree/main", "dev").mustRedirect("/my-repo") if !strings.Contains(loc, "/tree/dev") { t.Errorf("location = %q", loc) } }) t.Run("branch-switch route preserves subpath when switching tree", func(t *testing.T) { loc := admin.get("/my-repo/branch-switch?view=tree&rev=dev&path=src/foo").Location() if !strings.Contains(loc, "/tree/dev/src/foo") { t.Errorf("location = %q", loc) } }) t.Run("branch-switch route redirects commits view correctly", func(t *testing.T) { loc := admin.get("/my-repo/branch-switch?view=commits&rev=dev").Location() if !strings.Contains(loc, "/commits/dev") { t.Errorf("location = %q", loc) } }) t.Run("branch-switch route redirects blob view correctly", func(t *testing.T) { loc := admin.get("/my-repo/branch-switch?view=blob&rev=dev&path=README.md").Location() if !strings.Contains(loc, "/blob/dev/README.md") { t.Errorf("location = %q", loc) } }) // ─── Default branch settings ─────────────────────────────────────────── t.Run("settings page shows default branch select", func(t *testing.T) { r := admin.get("/my-repo/settings") if !r.Has("#default_branch") { t.Fatal("#default_branch missing") } opts := r.Texts("#default_branch option") if !slices.Contains(opts, "main") || !slices.Contains(opts, "dev") { t.Errorf("options = %v", opts) } }) t.Run("current default branch is pre-selected", func(t *testing.T) { if got := admin.get("/my-repo/settings").Value("#default_branch"); got != "main" { t.Errorf("default branch = %q", got) } }) t.Run("changing default branch saves and is reflected in the repo home", func(t *testing.T) { r := admin.follow(admin.post("/my-repo/settings", url.Values{"default_branch": {"dev"}})) if !r.Has(".form-success") { t.Fatal("form-success missing") } if got := r.Value("#default_branch"); got != "dev" { t.Errorf("default branch = %q", got) } if got := admin.get("/my-repo").Value(".branch-select"); got != "dev" { t.Errorf("repo home branch = %q", got) } }) t.Run("commit log link in repo nav uses the new default branch", func(t *testing.T) { href := admin.get("/my-repo").Attr(`.repo-tab[href*="/commits/"]`, "href") if !strings.Contains(href, "/commits/dev") { t.Errorf("commits href = %q", href) } }) t.Run("changing default branch back to main restores original state", func(t *testing.T) { r := admin.follow(admin.post("/my-repo/settings", url.Values{"default_branch": {"main"}})) if !r.Has(".form-success") { t.Fatal("form-success missing") } if got := r.Value("#default_branch"); got != "main" { t.Errorf("default branch = %q", got) } }) t.Run("settings page shows hint instead of select when repo has no branches", func(t *testing.T) { e.createRepo(admin, "empty-for-branch-test") r := admin.get("/empty-for-branch-test/settings") if n := r.Count("#default_branch"); n != 0 { t.Errorf("#default_branch count = %d", n) } if !r.Has(".form-hint") { t.Error("form-hint missing") } }) // ─── File browser ────────────────────────────────────────────────────── // A subdirectory makes directory navigation testable. e.seedRepo("my-repo", map[string]string{ "src/app.ts": "export {};\n", "src/README.md": "# src readme\n", }, "Add src") t.Run("repo home shows file tree instead of recent commits", func(t *testing.T) { r := admin.get("/my-repo") if !r.Has(".file-tree") { t.Error("file-tree missing") } if n := r.Count(".repo-commits-section"); n != 0 { t.Errorf("repo-commits-section count = %d", n) } }) t.Run("repo home file tree lists files and directories", func(t *testing.T) { names := admin.get("/my-repo").Texts(".file-name a") for _, want := range []string{"README.md", "index.js", "src"} { if !slices.Contains(names, want) { t.Errorf("names %v missing %q", names, want) } } }) t.Run("directories appear before files in file tree", func(t *testing.T) { names := admin.get("/my-repo").Texts(".file-name a") srcIdx := slices.Index(names, "src") readmeIdx := slices.Index(names, "README.md") if srcIdx < 0 || readmeIdx < 0 || srcIdx >= readmeIdx { t.Errorf("src at %d, README.md at %d in %v", srcIdx, readmeIdx, names) } }) t.Run("no .. entry at repository root", func(t *testing.T) { if names := admin.get("/my-repo").Texts(".file-name a"); slices.Contains(names, "..") { t.Errorf("names = %v", names) } }) t.Run("clicking directory navigates into it", func(t *testing.T) { href := "" admin.get("/my-repo").Find(".file-name a").Each(func(_ int, s *goquerySel) { if strings.TrimSpace(s.Text()) == "src" { href, _ = s.Attr("href") } }) if href != "/my-repo/tree/main/src" { t.Fatalf("src href = %q", href) } admin.get(href).mustStatus(200) }) t.Run(".. entry appears in subdirectory", func(t *testing.T) { names := admin.get("/my-repo/tree/main/src").Texts(".file-name a") if !slices.Contains(names, "..") { t.Errorf("names = %v", names) } }) t.Run(".. at one level deep links to tree root", func(t *testing.T) { up := "" admin.get("/my-repo/tree/main/src").Find(".file-name a").Each(func(_ int, s *goquerySel) { if strings.TrimSpace(s.Text()) == ".." { up, _ = s.Attr("href") } }) if up != "/my-repo/tree/main" { t.Errorf("up href = %q", up) } }) t.Run("files in subdirectory show plain names, not full paths", func(t *testing.T) { names := admin.get("/my-repo/tree/main/src").Texts(".file-name a") if !slices.Contains(names, "app.ts") { t.Errorf("names = %v", names) } for _, bad := range []string{"src/app.ts", "src/README.md"} { if slices.Contains(names, bad) { t.Errorf("names %v contains full path %q", names, bad) } } }) t.Run("readme is shown below file tree on repo home", func(t *testing.T) { // Geometry needs a browser; document order carries the same intent. body := admin.get("/my-repo").BodyString() tree := strings.Index(body, `class="file-tree"`) readme := strings.Index(body, `class="readme-section"`) if tree < 0 || readme < 0 || readme < tree { t.Errorf("file-tree at %d, readme-section at %d", tree, readme) } }) t.Run("readme in subdirectory is shown when present", func(t *testing.T) { r := admin.get("/my-repo/tree/main/src") if !r.Has(".readme-section") { t.Fatal("readme-section missing") } html, _ := r.Find(".readme-section .markdown-body").Html() if !strings.Contains(html, "src readme") { t.Errorf("readme html = %q", html) } }) t.Run("file tree on /tree/:ref also shows readme", func(t *testing.T) { r := admin.get("/my-repo/tree/main") if !r.Has(".file-tree") { t.Error("file-tree missing") } if !r.Has(".readme-section") { t.Error("readme-section missing") } }) }