internal/control/view_test.go

v1.43.0
gitbay/internal/control/view_test.go history · blame · raw

154 lines · 5144 bytes

  1package control
  2
  3import (
  4	"bytes"
  5	"strings"
  6	"testing"
  7
  8	"gitbay.org/gitbay/internal/protocol"
  9	"gitbay.org/gitbay/internal/store"
 10)
 11
 12func showIssue(t *testing.T, term Term) string {
 13	t.Helper()
 14	st, repo, uid := newQueueTestRepo(t)
 15	n, err := st.CreateIssue(repo.ID, uid, "A title", "Body with a [link](/x/y).", "md")
 16	if err != nil {
 17		t.Fatal(err)
 18	}
 19	issue, err := st.IssueByNumber(repo.ID, n)
 20	if err != nil {
 21		t.Fatal(err)
 22	}
 23	if err := st.AddIssueSystemComment(issue.ID, uid, "referenced in commit [abc1234567](/o/r/commit/abc) by [alice](/alice)"); err != nil {
 24		t.Fatal(err)
 25	}
 26	c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid, Username: "alice"})
 27	c.Cfg.Server.SiteURL = "https://forge.test"
 28	c.Term = term
 29	if code := Dispatch(c, []string{"issue", "show", repo.Path(), "1"}); code != protocol.ExitOK {
 30		t.Fatalf("exit %d: %s", code, errOut)
 31	}
 32	return c.Stdout.(*bytes.Buffer).String()
 33}
 34
 35func TestIssueShowPlain(t *testing.T) {
 36	out := showIssue(t, Term{})
 37	for _, want := range []string{
 38		"#1  A title  open\n",
 39		"  author  alice, ",
 40		"  url     https://forge.test/",
 41		"Body with a link (https://forge.test/x/y).",
 42		"  · referenced in commit abc1234567 by alice  ",
 43	} {
 44		if !strings.Contains(out, want) {
 45			t.Errorf("missing %q in:\n%s", want, out)
 46		}
 47	}
 48	if strings.Contains(out, "\x1b") || strings.Contains(out, "](") {
 49		t.Errorf("markup or SGR in plain view:\n%s", out)
 50	}
 51}
 52
 53func TestIssueShowTerminal(t *testing.T) {
 54	out := showIssue(t, Term{Cols: 60, Color: true})
 55	if !strings.Contains(out, sgrGreen+"open"+sgrReset) {
 56		t.Errorf("state not coloured:\n%s", out)
 57	}
 58	if !strings.Contains(stripSGR(out), "referenced in commit abc1234567 by alice") {
 59		t.Errorf("event missing:\n%s", out)
 60	}
 61	for _, line := range strings.Split(stripSGR(out), "\n") {
 62		// A suggested command is never cut: it has to paste whole.
 63		if strings.HasPrefix(line, "gitbay ") {
 64			continue
 65		}
 66		if cells(line) > 60 {
 67			t.Errorf("line over 60 cells: %q", line)
 68		}
 69	}
 70}
 71
 72// TestIssueShowNarrowWraps: a long title and long labels must not push
 73// any line past the terminal width, and no content is lost to it.
 74func TestIssueShowNarrowWraps(t *testing.T) {
 75	st, repo, uid := newQueueTestRepo(t)
 76	title := strings.TrimSpace(strings.Repeat("a very long issue title that keeps going on and on ", 3))[:90]
 77	n, err := st.CreateIssue(repo.ID, uid, title, "body", "md")
 78	if err != nil {
 79		t.Fatal(err)
 80	}
 81	for _, name := range []string{"needs-review-before-merge", "blocked-on-release", "requires-design-signoff"} {
 82		if err := st.SetLabel(repo, name, "888888"); err != nil {
 83			t.Fatal(err)
 84		}
 85		if err := st.SetIssueLabel(repo, issueIDFor(t, st, repo.ID, n), name, true); err != nil {
 86			t.Fatal(err)
 87		}
 88	}
 89	// No SiteURL set: the url field stays a short path so it does not
 90	// itself force an unbreakable line over width, which is not what
 91	// this test is about (title/labels/author wrapping is).
 92	c, errOut := pruneCtx(st, t.TempDir(), store.User{ID: uid, Username: "alice"})
 93	c.Term = Term{Cols: 40, Color: true}
 94	if code := Dispatch(c, []string{"issue", "show", repo.Path(), "1"}); code != protocol.ExitOK {
 95		t.Fatalf("exit %d: %s", code, errOut)
 96	}
 97	out := c.Stdout.(*bytes.Buffer).String()
 98
 99	var titleLines []string
100	inTitle := false
101	for _, line := range strings.Split(stripSGR(out), "\n") {
102		// A suggested command is never cut: it has to paste whole.
103		if cells(line) > 40 && !strings.HasPrefix(line, "gitbay ") {
104			t.Errorf("line over 40 cells: %q", line)
105		}
106		_, rest, isTitle := strings.Cut(line, "#1  ")
107		switch {
108		case strings.HasPrefix(line, "Issue:") && isTitle:
109			inTitle = true
110			titleLines = append(titleLines, rest)
111		case inTitle && strings.HasPrefix(line, "     "):
112			titleLines = append(titleLines, strings.TrimSpace(line))
113		default:
114			inTitle = false
115		}
116	}
117	got := strings.Join(strings.Fields(strings.Join(titleLines, " ")), " ")
118	want := strings.Join(strings.Fields(title), " ")
119	if got != want {
120		t.Errorf("title text lost across wrap: got %q, want %q", got, want)
121	}
122	if n := strings.Count(stripSGR(out), "open"); n != 1 {
123		t.Errorf("state word appears %d times, want 1:\n%s", n, out)
124	}
125}
126
127func issueIDFor(t *testing.T, st *store.Store, repoID, n int64) int64 {
128	t.Helper()
129	issue, err := st.IssueByNumber(repoID, n)
130	if err != nil {
131		t.Fatal(err)
132	}
133	return issue.ID
134}
135
136// view is the piped writer: at a terminal it writes what it writes
137// piped, so a caller that never migrated falls back to the plain layout.
138func TestViewIgnoresTerminal(t *testing.T) {
139	render := func(term Term) string {
140		var b bytes.Buffer
141		c := &Ctx{Term: term}
142		v := c.view(&b)
143		v.title("#1", "A title", "open")
144		v.fields("author", "alice", "when", "2026-09-01T10:00:00Z")
145		v.body("Some *body* text that is long enough to wrap at forty columns, if it wraps.", "md")
146		v.section("files")
147		v.event("labelled [bug](/x)", "md", "2026-09-01T10:00:00Z")
148		v.comment(1, "bob", "2026-09-01T10:00:00Z", "A comment.", "md")
149		return b.String()
150	}
151	if got, want := render(Term{Cols: 40, Color: true}), render(Term{}); got != want {
152		t.Errorf("terminal view differs from piped:\n%s\n---\n%s", got, want)
153	}
154}