aboutsummaryrefslogtreecommitdiff
path: root/internal/render/width_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/render/width_test.go')
-rw-r--r--internal/render/width_test.go112
1 files changed, 112 insertions, 0 deletions
diff --git a/internal/render/width_test.go b/internal/render/width_test.go
new file mode 100644
index 0000000..f20038e
--- /dev/null
+++ b/internal/render/width_test.go
@@ -0,0 +1,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)
+ }
+ }
+}