package web
import (
"context"
"net/http"
"net/url"
"strings"
"testing"
"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)
// milestoneFixture: four issues across two milestones (m1 has one closed), one
// issue with no milestone label.
func milestoneFixture() *fakeSession {
issues := &browse.RowPage{
Columns: []string{"id", "title", "status", "priority", "issue_type", "assignee"},
Rows: [][]string{
{"i-a", "Alpha", "closed", "1", "task", "alice"},
{"i-b", "Bravo", "open", "0", "bug", "bob"},
{"i-c", "Charlie", "open", "2", "feature", ""},
{"i-d", "Delta", "open", "1", "task", ""}, // no milestone
},
Total: 4,
}
labels := &browse.RowPage{
Columns: []string{"issue_id", "label"},
Rows: [][]string{
{"i-a", "milestone:m1"},
{"i-b", "milestone:m1"},
{"i-c", "milestone:m2"},
{"i-b", "backend"}, // non-milestone label ignored by grouping
},
Total: 4,
}
statuses := &browse.RowPage{
Columns: []string{"name", "category"},
Rows: [][]string{{"open", "open"}, {"closed", "closed"}},
Total: 2,
}
return &fakeSession{
branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}},
tables: beadsTables(),
rowsByTable: map[string]*browse.RowPage{
"issues": issues,
"labels": labels,
"custom_statuses": statuses,
},
}
}
func TestMilestonesApplies(t *testing.T) {
// The milestone tab appears exactly where the beads tab does.
if (&milestonesView{}).Applies(beadsTables()) != (&beadsView{}).Applies(beadsTables()) {
t.Fatalf("milestones Applies should mirror beads Applies")
}
if (&milestonesView{}).Applies([]browse.TableInfo{{Name: "widgets"}}) {
t.Fatalf("milestones should not apply to an unrelated schema")
}
}
func TestMilestonesBuild(t *testing.T) {
got, err := (&milestonesView{}).Build(context.Background(), milestoneFixture(), nil, "main", url.Values{})
if err != nil {
t.Fatalf("Build: %v", err)
}
d := got.(*MilestoneView)
if len(d.Milestones) != 2 {
t.Fatalf("milestones = %d, want 2: %+v", len(d.Milestones), d.Milestones)
}
if d.Unlabeled != 1 || d.Total != 4 {
t.Errorf("unlabeled=%d total=%d, want 1 and 4", d.Unlabeled, d.Total)
}
m1 := d.Milestones[0]
if m1.Name != "m1" || m1.Total != 2 || m1.Done != 1 || m1.Open != 1 || m1.Pct() != 50 {
t.Errorf("m1 = %+v, want name m1 total 2 done 1 open 1 pct 50", m1)
}
// Issues under m1, open-first: i-b (open) before i-a (closed).
if len(m1.Issues) != 2 || m1.Issues[0].ID != "i-b" || m1.Issues[1].ID != "i-a" {
t.Errorf("m1 issue order = %+v, want [i-b i-a]", m1.Issues)
}
if m1.Issues[1].Category != "closed" {
t.Errorf("i-a should be closed: %+v", m1.Issues[1])
}
m2 := d.Milestones[1]
if m2.Name != "m2" || m2.Total != 1 || m2.Done != 0 {
t.Errorf("m2 = %+v", m2)
}
}
func TestMilestonesRender(t *testing.T) {
h := newHarness(t)
h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
h.browse.sess = milestoneFixture()
setViews(t, h, &beadsView{}, &milestonesView{})
rec := h.do("GET", "/~alice/db/view/milestones", nil, nil)
if rec.Code != http.StatusOK {
t.Fatalf("milestones view: got %d; body=%s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
for _, want := range []string{
"· milestones", // page heading
"ms-title\">m1<", // milestone name
"1</b>/2 done", // rollup counts
"Charlie", // an issue under m2
"carry no milestone", // unlabeled footnote
`view/milestones">Milestones`, // the tab link
"nav-link active", // the active tab marker
} {
if !strings.Contains(body, want) {
t.Errorf("milestones render missing %q", want)
}
}
}
// TestMilestonesEmpty: a beads DB with no milestone labels still gets the tab,
// showing an empty state rather than erroring.
func TestMilestonesEmpty(t *testing.T) {
got, err := (&milestonesView{}).Build(context.Background(), beadsFixture(), nil, "main", url.Values{})
if err != nil {
t.Fatalf("Build: %v", err)
}
d := got.(*MilestoneView)
if len(d.Milestones) != 0 {
t.Errorf("expected no milestones, got %+v", d.Milestones)
}
if d.Unlabeled != d.Total {
t.Errorf("with no milestone labels all issues are unlabeled; got %d/%d", d.Unlabeled, d.Total)
}
}