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/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) } }