internal/control/term_test.go

v1.41.0
gitbay/internal/control/term_test.go history · blame · raw

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}