Artifact 8ae5420c4d84c4d56cd786a1307091c2f3429c15e3b37b34e7b129b98c8db8fb:
- File
internal/web/web_test.go
— part of check-in
[a74b77467d]
at
2026-08-15 17:21:59
on branch trunk
— timeline UX: fossil-modern cards, continuous rails, controls
Card layout with title/body split and bottom-right metadata; date pills; stretchable rail SVG + fixed node dot (rails continuous through tall rows, leaf tips as rings); Max control with fossil's n= name. (user: dmestas, size: 8061) [annotate] [blame] [check-ins using]
package web import ( "fmt" "net/http/httptest" "path/filepath" "strings" "testing" "craftdesign.group/quarry/internal/api" "craftdesign.group/quarry/internal/artifact" "craftdesign.group/quarry/internal/index" "craftdesign.group/quarry/internal/ops" ) func testRegistry(t *testing.T) *ops.Registry { t.Helper() // An empty (but real) index so entity lookups answer not-found // rather than "no store configured". ix, err := index.Open(filepath.Join(t.TempDir(), "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) r := ops.NewRegistry() if err := api.RegisterAll(r, api.Deps{Version: "9.9.9-test", Repos: map[string]*api.Repo{"test": {Index: ix}}}); err != nil { t.Fatal(err) } return r } func TestHomePageRendersThroughVersionOp(t *testing.T) { rec := httptest.NewRecorder() Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) if rec.Code != 200 { t.Fatalf("status %d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "quarry 9.9.9-test") { t.Fatalf("footer missing op-served version:\n%s", body) } if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { t.Fatalf("content-type %q", ct) } if strings.Contains(body, "<script") { t.Fatal("no-JS baseline violated: script tag on the base page") } if !strings.Contains(body, "prefers-color-scheme") { t.Fatal("dark theme missing") } } func TestUnknownPathIs404(t *testing.T) { rec := httptest.NewRecorder() Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", "/nope", nil)) if rec.Code != 404 { t.Fatalf("status %d", rec.Code) } } func TestThemed404Page(t *testing.T) { for _, path := range []string{"/nope", "/r/test/wiki/NoSuchPage", "/r/test/checkin/deadbeefdead", "/r/test/ticket/deadbeef"} { rec := httptest.NewRecorder() Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", path, nil)) if rec.Code != 404 { t.Errorf("%s: status %d", path, rec.Code) continue } body := rec.Body.String() if !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/html") { t.Errorf("%s: content-type %q — bare default 404?", path, rec.Header().Get("Content-Type")) } if !strings.Contains(body, "quarry 9.9.9-test") { t.Errorf("%s: 404 page missing version footer", path) } if !strings.Contains(body, `href="/"`) { t.Errorf("%s: 404 page has no way back into the site", path) } } } func TestPersistentNavOnEveryPage(t *testing.T) { // Ticket 010: every repo page carries that repo's nav and search // form; every page (fleet and 404 included) carries the site link. for _, path := range []string{"/r/test/timeline", "/r/test/wiki", "/r/test/ticket", "/r/test/search"} { rec := httptest.NewRecorder() Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", path, nil)) body := rec.Body.String() for _, want := range []string{`<nav>`, `href="/r/test/timeline"`, `href="/r/test/wiki"`, `href="/r/test/ticket"`, `action="/r/test/search"`} { if !strings.Contains(body, want) { t.Errorf("%s: missing %s in header", path, want) } } } for _, path := range []string{"/", "/nope"} { rec := httptest.NewRecorder() Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", path, nil)) if !strings.Contains(rec.Body.String(), `class="site" href="/"`) { t.Errorf("%s: missing site link", path) } } } func TestFleetPageListsRepos(t *testing.T) { rec := httptest.NewRecorder() Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) if rec.Code != 200 { t.Fatalf("status %d", rec.Code) } if !strings.Contains(rec.Body.String(), `href="/r/test/timeline"`) { t.Fatalf("fleet page missing repo link:\n%s", rec.Body.String()) } } func TestTablesScrollInWrapper(t *testing.T) { // Ticket 120: wide tables scroll inside .tw, not the page. rec := httptest.NewRecorder() Handler(testRegistry(t)).ServeHTTP(rec, httptest.NewRequest("GET", "/r/test/timeline", nil)) body := rec.Body.String() if strings.Contains(body, "<table>") && !strings.Contains(body, `<div class="tw"><table>`) { t.Fatalf("timeline table not wrapped:\n%s", body) } } func TestTimelineRendersAllRowsWithBranches(t *testing.T) { // Regression: a template field mismatch once truncated the page at // the first row — Execute's error was discarded. Render a POPULATED // timeline and assert every row, branch labels, and a complete page. ix, err := index.Open(filepath.Join(t.TempDir(), "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) var batch []index.Parsed for i := 0; i < 5; i++ { h := strings.Repeat(fmt.Sprintf("%02d", i+1), 32) art := &artifact.Artifact{Type: artifact.Manifest, User: "dan", Date: fmt.Sprintf("2026-08-15T0%d:00:00", i), Comment: fmt.Sprintf("commit %d", i)} if i == 0 { art.Tags = []artifact.Tag{{Name: "*branch", Target: "*", Value: "trunk"}} } if i > 0 { art.Parents = []string{strings.Repeat(fmt.Sprintf("%02d", i), 32)} } batch = append(batch, index.Parsed{Hash: h, Art: art}) } if err := ix.Fold(batch); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := api.RegisterAll(r, api.Deps{Version: "9.9.9-test", Repos: map[string]*api.Repo{"test": {Index: ix}}}); err != nil { t.Fatal(err) } rec := httptest.NewRecorder() Handler(r).ServeHTTP(rec, httptest.NewRequest("GET", "/r/test/timeline", nil)) body := rec.Body.String() if got := strings.Count(body, `class="tl-card"`); got != 5 { t.Fatalf("rendered %d rows, want 5:\n%s", got, body) } if got := strings.Count(body, "<svg "); got != 5 { t.Fatalf("rendered %d graph cells, want 5", got) } if got := strings.Count(body, `class="tl-node`); got != 5 { t.Fatalf("rendered %d node dots, want 5", got) } if !strings.Contains(body, `<span class="tl-pill">2026-08-15</span>`) { t.Fatal("date pill missing") } if !strings.Contains(body, `href="/r/test/timeline?branch=trunk"><b>trunk</b></a>`) { t.Fatalf("branch label missing:\n%s", body) } if !strings.HasSuffix(strings.TrimSpace(body), "</html>") { t.Fatal("page truncated — template error swallowed") } } func TestTimelineGraphRails(t *testing.T) { // DAG: m (merge of c2+b1) ← newest // b1 (branch dev, parent c1) // c2 (trunk, parent c1) // c1 (trunk root) c1, c2, b1, m := strings.Repeat("aa", 32), strings.Repeat("bb", 32), strings.Repeat("cc", 32), strings.Repeat("dd", 32) entries := []timelineEntry{ {Hash: m, Date: "2026-08-15T04:00:00", Branch: "trunk", Comment: "merge", IsMerge: true, Parents: []string{c2, b1}}, {Hash: b1, Date: "2026-08-15T03:00:00", Branch: "dev", Comment: "dev work", Parents: []string{c1}}, {Hash: c2, Date: "2026-08-15T02:00:00", Branch: "trunk", Comment: "trunk work", Parents: []string{c1}}, {Hash: c1, Date: "2026-08-15T01:00:00", Branch: "trunk", Comment: "root"}, } days := buildTimeline(entries) if len(days) != 1 || len(days[0].Rows) != 4 { t.Fatalf("days = %+v", days) } svg := func(i int) string { return string(days[0].Rows[i].Rails) } // The merge row curves out to a second rail. if !strings.Contains(svg(0), "<path") { t.Fatalf("merge row has no curve:\n%s", svg(0)) } // The dev row sits on a different x than the trunk rail and carries // the dev branch color. if !strings.Contains(svg(1), branchStroke("dev")) { t.Fatalf("dev row not dev-colored:\n%s", svg(1)) } if days[0].Rows[1].NodeX != 21 { t.Fatalf("dev node not on second rail: x=%d", days[0].Rows[1].NodeX) } // The trunk row between branch point and merge shows the dev rail // passing through. if !strings.Contains(svg(2), `x1="21"`) { t.Fatalf("dev rail not passing through trunk row:\n%s", svg(2)) } // The root row converges both rails (a join curve). if !strings.Contains(svg(3), "<path") { t.Fatalf("root row has no join curve:\n%s", svg(3)) } // Root has nothing below: no line from cy to unitH on the node rail. if strings.Contains(svg(3), `y1="13" x2="7" y2="26"`) { t.Fatalf("root row continues below:\n%s", svg(3)) } }