internal/control/view_test.go
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}