internal/control/term_test.go
239 lines · 7785 bytes
1package control
2
3import (
4 "strings"
5 "testing"
6 "time"
7)
8
9func TestParseTerm(t *testing.T) {
10 cases := map[string]Term{
11 "120": {Cols: 120},
12 "120,color": {Cols: 120, Color: true},
13 "40": {Cols: 40},
14 "39": {},
15 "": {},
16 "abc": {},
17 "80,blink": {Cols: 80},
18 "80,": {Cols: 80},
19 "80,truecolor,color": {Cols: 80, Color: true, TrueColor: true},
20 "80,color,links": {Cols: 80, Color: true, Links: true},
21 "80,truecolor": {Cols: 80},
22 "abc,color": {},
23 "5000": {},
24 "120,color,here=krz/gitbay": {Cols: 120, Color: true, Here: "krz/gitbay"},
25 "120,here=": {Cols: 120},
26 }
27 for in, want := range cases {
28 if got := ParseTerm(in); got != want {
29 t.Errorf("ParseTerm(%q) = %+v, want %+v", in, got, want)
30 }
31 }
32}
33
34func TestCells(t *testing.T) {
35 cases := map[string]int{
36 "abc": 3,
37 "日本": 4,
38 "é": 1,
39 "é": 1,
40 "\x1b[32mopen\x1b[0m": 4,
41 "": 0,
42 }
43 for in, want := range cases {
44 if got := cells(in); got != want {
45 t.Errorf("cells(%q) = %d, want %d", in, got, want)
46 }
47 }
48}
49
50func TestClip(t *testing.T) {
51 if got := clip("Dependency updates available", 14); got != "Dependency up…" {
52 t.Errorf("clip = %q", got)
53 }
54 if got := clip("short", 14); got != "short" {
55 t.Errorf("clip = %q", got)
56 }
57 if got := clip("日本語のタイトル", 7); got != "日本語…" {
58 t.Errorf("clip wide = %q", got)
59 }
60}
61
62func TestStampAndRelAge(t *testing.T) {
63 if got := stamp("2026-09-23T23:26:00.570Z"); got != "2026-09-23T23:26:00Z" {
64 t.Errorf("stamp = %q", got)
65 }
66 if got := stamp("2026-09-23 23:26:00"); got != "2026-09-23T23:26:00Z" {
67 t.Errorf("stamp sqlite = %q", got)
68 }
69 if got := stamp("garbage"); got != "garbage" {
70 t.Errorf("stamp garbage = %q", got)
71 }
72 now := time.Date(2026, 9, 23, 12, 0, 0, 0, time.UTC)
73 cases := map[string]string{
74 "2026-09-23T11:59:30Z": "just now",
75 "2026-09-23T11:55:00Z": "5m ago",
76 "2026-09-23T10:00:00Z": "2h ago",
77 "2026-09-20T12:00:00Z": "3d ago",
78 "2026-09-02T12:00:00Z": "3w ago",
79 "2026-06-01T12:00:00Z": "2026-06-01",
80 "2026-09-23T12:00:20Z": "just now",
81 "2026-09-23T17:00:00Z": "in 5h",
82 "2026-09-24T12:00:00Z": "in 1d",
83 "2027-09-24T12:00:00Z": "2027-09-24",
84 "not a time": "not a time",
85 }
86 for in, want := range cases {
87 if got := relAge(in, now); got != want {
88 t.Errorf("relAge(%q) = %q, want %q", in, got, want)
89 }
90 }
91}
92
93func TestSizeAndDur(t *testing.T) {
94 plain, term := Term{}, Term{Cols: 80}
95 if got := plain.size(3177346); got != "3177346" {
96 t.Errorf("plain size = %q", got)
97 }
98 if got := term.size(3177346); got != "3.0 MiB" {
99 t.Errorf("term size = %q", got)
100 }
101 if got := plain.dur(3223); got != "3223s" {
102 t.Errorf("plain dur = %q", got)
103 }
104 if got := term.dur(3223); got != "53m43s" {
105 t.Errorf("term dur = %q", got)
106 }
107}
108
109func TestPaintStateEachWord(t *testing.T) {
110 got := Term{Cols: 80, Color: true}.paintState("private, archived")
111 want := sgrRed + "private" + sgrReset + ", " + sgrDim + "archived" + sgrReset
112 if got != want {
113 t.Errorf("paintState = %q, want %q", got, want)
114 }
115 if got := (Term{Cols: 80}).paintState("private, archived"); got != "private, archived" {
116 t.Errorf("no colour = %q", got)
117 }
118}
119
120func TestFailureAtTerminal(t *testing.T) {
121 msg := "unknown flag \"--stat\"; did you mean --state?\nusage: gitbay issue list [<owner/name>] [--state open|closed|all] [--label <l>] [--assignee <user>]"
122 got := Term{Cols: 50}.failure(msg)
123 want := "error: unknown flag \"--stat\"; did you mean --state?\n" +
124 "usage: gitbay issue list [<owner/name>]\n" +
125 " [--state open|closed|all] [--label <l>]\n" +
126 " [--assignee <user>]"
127 if got != want {
128 t.Errorf("failure:\n%s\nwant\n%s", got, want)
129 }
130 alt := Term{Cols: 80}.failure("usage: gitbay issue list [--limit <n>] | issue list --query <name> | --q <query>")
131 if alt != "usage: gitbay issue list [--limit <n>]\n | issue list --query <name>\n | --q <query>" {
132 t.Errorf("alternatives:\n%s", alt)
133 }
134 if got := (Term{Cols: 80}).failure("usage: gitbay mr merge <n>"); got != "usage: gitbay mr merge <n>" {
135 t.Errorf("bare usage = %q", got)
136 }
137}
138
139func TestDiffPaint(t *testing.T) {
140 patch := " go.mod | 2 +-\n 1 file changed\n\ndiff --git a/go.mod b/go.mod\nindex a..b 100644\n--- a/go.mod\n+++ b/go.mod\n@@ -1,2 +1,2 @@ require (\n same\n-old\n+new\n"
141 if got := (Term{}).diff(patch); got != patch {
142 t.Errorf("plain changed:\n%s", got)
143 }
144 if got := (Term{Cols: 80}).diff(patch); got != patch {
145 t.Errorf("no colour changed:\n%s", got)
146 }
147 got := Term{Cols: 80, Color: true}.diff(patch)
148 for _, want := range []string{
149 " go.mod | 2 " + sgrGreen + "+" + sgrReset + sgrRed + "-" + sgrReset,
150 sgrBold + "diff --git a/go.mod b/go.mod" + sgrReset,
151 sgrBold + "--- a/go.mod" + sgrReset,
152 sgrBold + "+++ b/go.mod" + sgrReset,
153 sgrCyan + "@@ -1,2 +1,2 @@" + sgrReset + " require (",
154 "\n same\n",
155 sgrRed + "-old" + sgrReset,
156 sgrGreen + "+new" + sgrReset,
157 } {
158 if !strings.Contains(got, want) {
159 t.Errorf("missing %q in\n%q", want, got)
160 }
161 }
162 if stripSGR(got) != patch {
163 t.Errorf("colour changed the text:\n%s", stripSGR(got))
164 }
165}
166
167func TestDiffPaintRangeDiff(t *testing.T) {
168 rd := "1: abc1234 ! 1: def5678 subject\n @@ f.go\n -old\n +new\n"
169 got := Term{Cols: 80, Color: true}.diff(rd)
170 for _, want := range []string{
171 sgrBold + "1: abc1234 ! 1: def5678 subject" + sgrReset,
172 " " + sgrRed + "-old" + sgrReset,
173 " " + sgrGreen + "+new" + sgrReset,
174 } {
175 if !strings.Contains(got, want) {
176 t.Errorf("missing %q in\n%q", want, got)
177 }
178 }
179}
180
181func TestDiffIsSafe(t *testing.T) {
182 got := Term{Cols: 80}.diff("+\x1b]52;c;aGk=\x07\n")
183 if strings.ContainsRune(got, 0x1b) {
184 t.Errorf("escape reached the terminal: %q", got)
185 }
186}
187
188func TestBuildLog(t *testing.T) {
189 log := "$ git clone x\n\x1b[32mok\x1b[0m\n$ make test\nFAIL\n"
190 got := Term{Cols: 80, Color: true}.buildLog(log, "make test")
191 want := sgrBold + "$ git clone x" + sgrReset + "\nok\n" + sgrBold + sgrRed + "$ make test" + sgrReset + "\nFAIL\n"
192 if got != want {
193 t.Errorf("buildLog:\n%q\nwant\n%q", got, want)
194 }
195 if got := (Term{Cols: 80}).buildLog("a\x1b]52;c;aGk=\x07b\n", ""); strings.ContainsRune(got, 0x1b) {
196 t.Errorf("escape reached the terminal: %q", got)
197 }
198}
199
200func TestLinkAndSwatch(t *testing.T) {
201 on := Term{Cols: 80, Color: true, TrueColor: true, Links: true}
202 if got := on.link("https://x.test/a", "a"); got != "\x1b]8;;https://x.test/a\x1b\\a\x1b]8;;\x1b\\" {
203 t.Errorf("link = %q", got)
204 }
205 if got := on.link("https://x.test/\x1b", "a"); got != "a" {
206 t.Errorf("control byte in url = %q", got)
207 }
208 if got := (Term{Cols: 80, Color: true}).link("https://x.test/a", "a"); got != "a" {
209 t.Errorf("without links = %q", got)
210 }
211 if got := on.swatch("● #cf222e"); got != "\x1b[38;2;207;34;46m●"+sgrReset+" #cf222e" {
212 t.Errorf("swatch = %q", got)
213 }
214 if got := on.swatch("● #nothex"); got != "● #nothex" {
215 t.Errorf("bad hex = %q", got)
216 }
217}
218
219func TestGlyph(t *testing.T) {
220 for _, c := range []struct{ state, g, sgr string }{
221 {"success", "✓", sgrGreen},
222 {"approved", "✓", sgrGreen},
223 {"merged", "✓", ""},
224 {"failure", "✗", sgrRed},
225 {"changes requested", "✗", sgrRed},
226 {"signed_key_revoked", "✗", sgrRed},
227 {"running", "◐", ""},
228 {"pending", "◐", ""},
229 {"closed", "○", ""},
230 {"draft", "○", ""},
231 {"open", "", ""},
232 {"", "", ""},
233 } {
234 g, sgr := glyph(c.state)
235 if g != c.g || sgr != c.sgr {
236 t.Errorf("glyph(%q) = %q %q, want %q %q", c.state, g, sgr, c.g, c.sgr)
237 }
238 }
239}