summaryrefslogtreecommitdiff
path: root/internal/render/width_test.go
blob: f20038e721d5717a893326523e9e7584c77c0567 (plain) (blame)
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)
		}
	}
}