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) } } }