internal/control/table_test.go
126 lines · 4162 bytes
1package control
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7 "time"
8)
9
10func fixtureTable(c *Ctx, w *bytes.Buffer) {
11 tb := c.table(w, "#", "STATE", "TITLE", "AUTHOR")
12 tb.row(cRef("#252"), cState("open"), cFlex("Dependency updates available for every module"), cText("gitbay-bot"))
13 tb.row(cRef("#12"), cState("closed"), cFlex("Android app"), cText("cmc"))
14 tb.flush()
15}
16
17func TestTablePlainIsTabs(t *testing.T) {
18 var b bytes.Buffer
19 fixtureTable(&Ctx{}, &b)
20 want := "#252\topen\tDependency updates available for every module\tgitbay-bot\n" +
21 "#12\tclosed\tAndroid app\tcmc\n"
22 if b.String() != want {
23 t.Errorf("plain:\n%q\nwant\n%q", b.String(), want)
24 }
25}
26
27func TestTableTerminalFits(t *testing.T) {
28 var b bytes.Buffer
29 fixtureTable(&Ctx{Term: Term{Cols: 40}}, &b)
30 want := "# STATE TITLE AUTHOR\n" +
31 "#252 open Dependency up… gitbay-bot\n" +
32 "#12 closed Android app cmc\n"
33 if b.String() != want {
34 t.Errorf("terminal:\n%s\nwant\n%s", b.String(), want)
35 }
36}
37
38// A column shrunk below its header's width clips the header too.
39func TestTableClipsHeaderToColumn(t *testing.T) {
40 var b bytes.Buffer
41 tb := (&Ctx{Term: Term{Cols: 16}}).table(&b, "FINGERPRINT", "SCOPE")
42 tb.row(cFlex("SHA256:abcdefghijklmnopqrstuvwxyz"), cState("full"))
43 tb.flush()
44 for _, line := range strings.Split(strings.TrimSuffix(b.String(), "\n"), "\n") {
45 if cells(line) > 16 {
46 t.Errorf("line of %d cells at 16 columns: %q", cells(line), line)
47 }
48 }
49 if !strings.HasPrefix(b.String(), "FINGERPR… SCOPE\n") {
50 t.Errorf("header:\n%s", b.String())
51 }
52}
53
54func TestTableColourOnlyAddsSGR(t *testing.T) {
55 var mono, colour bytes.Buffer
56 fixtureTable(&Ctx{Term: Term{Cols: 40}}, &mono)
57 fixtureTable(&Ctx{Term: Term{Cols: 40, Color: true}}, &colour)
58 if !strings.Contains(colour.String(), sgrGreen+"open"+sgrReset) {
59 t.Errorf("open not green: %q", colour.String())
60 }
61 if !strings.HasPrefix(colour.String(), sgrDim) {
62 t.Errorf("header not dim: %q", colour.String())
63 }
64 if stripSGR(colour.String()) != mono.String() {
65 t.Errorf("colour changed the layout:\n%s\nvs\n%s", stripSGR(colour.String()), mono.String())
66 }
67}
68
69func TestTableAgesAndPlainStamps(t *testing.T) {
70 termNow = func() time.Time { return time.Date(2026, 9, 23, 12, 0, 1, 0, time.UTC) }
71 t.Cleanup(func() { termNow = time.Now })
72 var plain, term bytes.Buffer
73 for _, c := range []struct {
74 ctx *Ctx
75 w *bytes.Buffer
76 }{{&Ctx{}, &plain}, {&Ctx{Term: Term{Cols: 80}}, &term}} {
77 tb := c.ctx.table(c.w, "#", "UPDATED")
78 tb.row(cRef("#1"), cAge("2026-09-23T10:00:00.123Z"))
79 tb.flush()
80 }
81 if plain.String() != "#1\t2026-09-23T10:00:00Z\n" {
82 t.Errorf("plain = %q", plain.String())
83 }
84 if term.String() != "# UPDATED\n#1 2h ago\n" {
85 t.Errorf("term = %q", term.String())
86 }
87}
88
89// A row may carry a cell beyond what the header names — repo list's
90// trailing [archived] marker, only present on some rows. flush must
91// still render it, not silently drop it because it falls past
92// len(header).
93func TestTableKeepsCellsBeyondTheHeader(t *testing.T) {
94 var b bytes.Buffer
95 tb := (&Ctx{Term: Term{Cols: 80}}).table(&b, "PATH", "VISIBILITY", "DESCRIPTION")
96 tb.row(cRef("a/x"), cState("public"), cFlex("one"))
97 tb.row(cRef("a/y"), cState("public"), cFlex("two"), cText("[archived]"))
98 tb.flush()
99
100 out := b.String()
101 if !strings.Contains(out, "[archived]") {
102 t.Fatalf("archived marker dropped:\n%s", out)
103 }
104 lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
105 if len(lines) != 3 {
106 t.Fatalf("want 3 lines, got %d:\n%s", len(lines), out)
107 }
108 header, row1, row2 := lines[0], lines[1], lines[2]
109 descAt := strings.Index(header, "DESCRIPTION")
110 oneAt := strings.Index(row1, "one")
111 twoAt := strings.Index(row2, "two")
112 if descAt < 0 || oneAt < 0 || twoAt < 0 {
113 t.Fatalf("columns not found:\n%s", out)
114 }
115 if descAt != oneAt || descAt != twoAt {
116 t.Errorf("DESCRIPTION column not aligned: header at %d, row1 at %d, row2 at %d\n%s", descAt, oneAt, twoAt, out)
117 }
118}
119
120func TestTableEmptyPrintsNothing(t *testing.T) {
121 var b bytes.Buffer
122 (&Ctx{Term: Term{Cols: 80}}).table(&b, "#").flush()
123 if b.Len() != 0 {
124 t.Errorf("empty table printed %q", b.String())
125 }
126}