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