vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Add repository search, list pagination, and theme/a11y polish
5cab7b18548e5263c01549cb61415fd7777d1758 · Keenan Nemetz · 15d ago · parent 0b26547e
Add repository search, list pagination, and theme/a11y polish The chrome and the index page search repository names and descriptions after the same read filter, so a private repository cannot appear. Issues and changes paginate at 25 rows with filters and HTMX navigation preserved. Polish from the UI plan: a light token block follows prefers-color-scheme, a skip link and focus-visible outlines help keyboard users, panels scroll horizontally on narrow screens, and the empty index/search states explain what is visible. Also fixes a flaky key assertion in the admin users test.
server/polish_test.go +89
| @@ -0,0 +1,89 @@ | |||
| 1 | package server_test | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "strings" | ||
| 6 | "testing" | ||
| 7 | "time" | ||
| 8 | |||
| 9 | voleid "github.com/rodent-software/vole/core/id" | ||
| 10 | ) | ||
| 11 | |||
| 12 | func TestRepositorySearchOnlyFindsVisibleRepositories(t *testing.T) { | ||
| 13 | t.Parallel() | ||
| 14 | f := newFixture(t) | ||
| 15 | f.publish(f.repo) | ||
| 16 | |||
| 17 | notes, err := f.srv.Repos.Create("notes", f.ownerID, "release notes and drafts") | ||
| 18 | if err != nil { | ||
| 19 | t.Fatal(err) | ||
| 20 | } | ||
| 21 | if err := f.srv.Index.Upsert(notes); err != nil { | ||
| 22 | t.Fatal(err) | ||
| 23 | } | ||
| 24 | f.publish("notes") | ||
| 25 | |||
| 26 | secret, err := f.srv.Repos.Create("secret", f.ownerID, "hidden roadmap") | ||
| 27 | if err != nil { | ||
| 28 | t.Fatal(err) | ||
| 29 | } | ||
| 30 | if err := f.srv.Index.Upsert(secret); err != nil { | ||
| 31 | t.Fatal(err) | ||
| 32 | } | ||
| 33 | |||
| 34 | if _, body := f.get("/?q=notes"); !strings.Contains(body, `href="/r/notes"`) || strings.Contains(body, "secret") { | ||
| 35 | t.Fatalf("name search = %q", body) | ||
| 36 | } | ||
| 37 | if _, body := f.get("/?q=RELEASE"); !strings.Contains(body, `href="/r/notes"`) { | ||
| 38 | t.Fatalf("case-insensitive description search missed notes:\n%s", body) | ||
| 39 | } | ||
| 40 | if _, body := f.get("/?q=hidden"); strings.Contains(body, "secret") { | ||
| 41 | t.Fatalf("search leaked a private repository:\n%s", body) | ||
| 42 | } | ||
| 43 | if _, body := f.get("/?q=nothingmatches"); !strings.Contains(body, "No repositories match") { | ||
| 44 | t.Fatalf("empty search state missing:\n%s", body) | ||
| 45 | } | ||
| 46 | if _, body := f.get("/"); !strings.Contains(body, `name="q"`) { | ||
| 47 | t.Fatalf("index page has no search form:\n%s", body) | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | func TestIssueListPaginates(t *testing.T) { | ||
| 52 | t.Parallel() | ||
| 53 | f := newFixture(t) | ||
| 54 | f.publish(f.repo) | ||
| 55 | |||
| 56 | files := map[string]string{} | ||
| 57 | now := time.Now().UTC() | ||
| 58 | for i := 1; i <= 27; i++ { | ||
| 59 | id, err := voleid.New(now.Add(time.Duration(i) * time.Millisecond)) | ||
| 60 | if err != nil { | ||
| 61 | t.Fatal(err) | ||
| 62 | } | ||
| 63 | files["issues/"+id+"/issue.md"] = issueFile( | ||
| 64 | fmt.Sprintf("Issue %02d", i), "keenan", "open", "2026-10-22T21:00:00Z", nil, "body\n", | ||
| 65 | ) | ||
| 66 | } | ||
| 67 | work := f.issuesRepo(t, files) | ||
| 68 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "HEAD:refs/heads/vole"); err != nil { | ||
| 69 | t.Fatalf("issues push: %s", errs) | ||
| 70 | } | ||
| 71 | |||
| 72 | _, page1 := f.get("/r/" + f.repo + "/issues") | ||
| 73 | if !strings.Contains(page1, "Issue 27") || strings.Contains(page1, "Issue 01") { | ||
| 74 | t.Fatalf("page 1 has the wrong issues:\n%s", page1) | ||
| 75 | } | ||
| 76 | if !strings.Contains(page1, "page 1 of 2") || !strings.Contains(page1, ">Next<") { | ||
| 77 | t.Fatalf("page 1 has no next link:\n%s", page1) | ||
| 78 | } | ||
| 79 | _, page2 := f.get("/r/" + f.repo + "/issues?page=2") | ||
| 80 | if !strings.Contains(page2, "Issue 01") || !strings.Contains(page2, "Issue 02") { | ||
| 81 | t.Fatalf("page 2 is missing the oldest issues:\n%s", page2) | ||
| 82 | } | ||
| 83 | if strings.Contains(page2, "Issue 27") { | ||
| 84 | t.Fatalf("page 2 repeated the newest issue:\n%s", page2) | ||
| 85 | } | ||
| 86 | if !strings.Contains(page2, ">Previous<") { | ||
| 87 | t.Fatalf("page 2 has no previous link:\n%s", page2) | ||
| 88 | } | ||
| 89 | } | ||