1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
package render
import (
"strings"
"testing"
)
func TestDisplayWidth(t *testing.T) {
cases := []struct {
name string
in string
want int
}{
{"ascii", "temp", 4},
{"empty", "", 0},
{"degree sign is one cell", "28°", 3},
{"polish diacritics are one cell each", "słońce", 6},
{"emoji with variation selector counts once", "☀️", 1},
{"bare BMP symbol", "☀", 1},
{"sun behind cloud is wide", "⛅", 2},
{"rain cloud is wide", "\U0001F327", 2},
{"nerd font glyph is one cell", "", 1},
{"block drawing is one cell", "█", 1},
{"combining acute adds nothing", "é", 1},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := DisplayWidth(c.in); got != c.want {
t.Errorf("DisplayWidth(%q) = %d, want %d", c.in, got, c.want)
}
})
}
}
// The invariant that actually matters: whatever goes in a column, the padded
// result occupies exactly the requested number of cells. If this fails, every
// column to the right shears -- but only in a real terminal.
func TestPadReachesExactWidthForEveryIconSet(t *testing.T) {
samples := []string{
"clear", "", "28°", "słońce",
"☀️", "⛅", "\U0001F327", "⛈", // emoji set
"", "", "", // nerd set
}
for _, s := range samples {
for _, w := range []int{1, 4, 8, 16} {
got := Pad(s, w)
if DisplayWidth(s) <= w && DisplayWidth(got) != w {
t.Errorf("Pad(%q, %d) has width %d, want %d", s, w, DisplayWidth(got), w)
}
if !strings.HasPrefix(got, s) {
t.Errorf("Pad(%q, %d) = %q, must not alter the content", s, w, got)
}
}
}
}
func TestPadLeft(t *testing.T) {
if got := PadLeft("5", 3); got != " 5" {
t.Fatalf("PadLeft = %q, want %q", got, " 5")
}
if got := PadLeft("⛅", 4); DisplayWidth(got) != 4 {
t.Fatalf("PadLeft of a wide glyph has width %d, want 4", DisplayWidth(got))
}
}
func TestPadDoesNotShrink(t *testing.T) {
if got := Pad("conditions", 4); got != "conditions" {
t.Fatalf("Pad must never truncate: got %q", got)
}
}
func TestTruncateNeverSplitsARune(t *testing.T) {
if got := Truncate("słońce", 3); got != "sło" {
t.Errorf("Truncate = %q, want %q", got, "sło")
}
// A wide glyph that does not fit is dropped whole, not halved.
if got := Truncate("a⛅", 2); got != "a" {
t.Errorf("Truncate = %q, want %q", got, "a")
}
}
func TestPaintGroupsRuns(t *testing.T) {
c := Styler(true)
cells := []Cell{
{Style: "31", Text: "a"}, {Style: "31", Text: "b"}, {Style: "31", Text: "c"},
{Style: "33", Text: "d"},
}
got := Paint(cells, c)
if n := strings.Count(got, "\033["); n != 4 { // 2 opens + 2 resets
t.Fatalf("expected one escape pair per run, got %d escapes in %q", n, got)
}
if strings.Count(got, "\033[31m") != 1 {
t.Errorf("the three red cells must share one escape: %q", got)
}
}
func TestPaintWithoutColourIsPlain(t *testing.T) {
c := Styler(false)
got := Paint([]Cell{{Style: "31", Text: "a"}, {Style: "33", Text: "b"}}, c)
if got != "ab" {
t.Fatalf("colour off must yield plain text, got %q", got)
}
}
// Colour must never reach for a 256-colour index.
func TestNoExtendedColourCodes(t *testing.T) {
for _, code := range []string{Blue, Cyan, Green, Yellow, Red, BrightBlue, BrightRed, BrightWhite} {
if strings.Contains(code, "38;5;") || strings.Contains(code, "48;5;") {
t.Errorf("%q is a 256-colour index; only slots 0-15 are allowed", code)
}
}
}
|