| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | func TestVisibleWidthGraphemeClusters(t *testing.T) { |
| 9 | cases := []struct { |
| 10 | name string |
| 11 | s string |
| 12 | want int |
| 13 | }{ |
| 14 | {"ascii", "abc", 3}, |
| 15 | {"cjk", "中文", 4}, |
| 16 | {"emoji", "🔥", 2}, |
| 17 | // x/ansi counts the VS16 keycap as 2 (emoji presentation). Terminals |
| 18 | // disagree on VS16 width, but the point is consistency: wrapAnsi / |
| 19 | // clampWidth now measure via the same x/ansi, so box rails and wrapping |
| 20 | // agree — which a mixed uniseg(1)/ansi(2) split would break. |
| 21 | {"keycap", "1️⃣", 2}, |
| 22 | // The regression that motivated the switch: a ZWJ family is one cluster |
| 23 | // occupying one emoji's width, not the rune-by-rune sum (which was 8). |
| 24 | {"zwj-family", "👨👩👧👦", 2}, |
| 25 | {"ansi-stripped", "\x1b[31mab\x1b[0m", 2}, |
| 26 | {"mixed", "a中🔥", 5}, |
| 27 | } |
| 28 | for _, c := range cases { |
| 29 | if got := visibleWidth(c.s); got != c.want { |
| 30 | t.Errorf("%s: visibleWidth(%q) = %d, want %d", c.name, c.s, got, c.want) |
| 31 | } |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | // TestClampWidthHardwrap verifies clampWidth (a wrapper over ansi.Hardwrap) keeps |
| 36 | // every wrapped line within the column budget, hard-breaks CJK at the boundary, |
| 37 | // preserves ANSI escapes as zero width, and leaves in-width lines untouched. |
| 38 | func TestClampWidthHardwrap(t *testing.T) { |
| 39 | // CJK hard-breaks at the column boundary (each is 2 cols); no line over width. |
| 40 | for _, line := range strings.Split(clampWidth("中文字", 4), "\n") { |
| 41 | if visibleWidth(line) > 4 { |
| 42 | t.Errorf("cjk line %q exceeds width 4 (got %d)", line, visibleWidth(line)) |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // A line already within width is returned byte-for-byte. |
| 47 | if got := clampWidth("ab", 10); got != "ab" { |
| 48 | t.Errorf("in-width line altered: %q", got) |
| 49 | } |
| 50 | |
| 51 | // ANSI SGR escapes are zero width, so two visible chars fit a width-2 line. |
| 52 | styled := clampWidth("\x1b[31mab\x1b[0m", 2) |
| 53 | if visibleWidth(styled) > 2 { |
| 54 | t.Errorf("styled line exceeds width 2 (got %d): %q", visibleWidth(styled), styled) |
| 55 | } |
| 56 | |
| 57 | // Every wrapped line stays within the column budget. |
| 58 | for _, line := range strings.Split(clampWidth(strings.Repeat("中", 10), 6), "\n") { |
| 59 | if visibleWidth(line) > 6 { |
| 60 | t.Errorf("line %q exceeds width 6 (got %d)", line, visibleWidth(line)) |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 |