internal/control/term_test.go

239 lines · 7785 bytes

13 symbols in this file
  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}