| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // TestRenderEmpty covers the contract that empty / whitespace-only input |
| 9 | // returns "" — callers rely on this to skip a redraw when there's nothing |
| 10 | // substantive to show. |
| 11 | func TestRenderEmpty(t *testing.T) { |
| 12 | r := newMarkdownRenderer(80) |
| 13 | for _, in := range []string{"", " ", "\n", "\t\n \n"} { |
| 14 | if got := r.Render(in); got != "" { |
| 15 | t.Errorf("Render(%q) = %q, want empty", in, got) |
| 16 | } |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | // TestRenderConstructsRound-trip checks each major construct emits something |
| 21 | // styled while preserving the underlying text. We don't assert exact ANSI |
| 22 | // sequences (palette could shift) — only that key visible text survives and |
| 23 | // that we don't degrade to literal markdown. |
| 24 | func TestRenderConstructs(t *testing.T) { |
| 25 | r := newMarkdownRenderer(80) |
| 26 | cases := []struct { |
| 27 | name string |
| 28 | in string |
| 29 | contains []string |
| 30 | notRaw []string // substrings that must NOT appear (raw markdown leaking through) |
| 31 | }{ |
| 32 | { |
| 33 | name: "heading", |
| 34 | in: "# Hello\n", |
| 35 | contains: []string{"Hello"}, |
| 36 | notRaw: []string{"# Hello", "## "}, |
| 37 | }, |
| 38 | { |
| 39 | name: "heading h2 drops prefix", |
| 40 | in: "## Section\n", |
| 41 | contains: []string{"Section"}, |
| 42 | notRaw: []string{"## ", "###"}, |
| 43 | }, |
| 44 | { |
| 45 | name: "bold", |
| 46 | in: "this is **important** text", |
| 47 | contains: []string{"important", "this is", "text"}, |
| 48 | }, |
| 49 | { |
| 50 | name: "italic", |
| 51 | in: "see *here* for details", |
| 52 | contains: []string{"here", "see", "for details"}, |
| 53 | }, |
| 54 | { |
| 55 | name: "code span", |
| 56 | in: "use `os.Setenv` to set", |
| 57 | contains: []string{"os.Setenv", "use", "to set"}, |
| 58 | }, |
| 59 | { |
| 60 | name: "unordered list", |
| 61 | in: "- one\n- two\n- three\n", |
| 62 | contains: []string{"one", "two", "three", "•"}, |
| 63 | }, |
| 64 | { |
| 65 | name: "ordered list", |
| 66 | in: "1. first\n2. second\n", |
| 67 | contains: []string{"first", "second", "1.", "2."}, |
| 68 | }, |
| 69 | { |
| 70 | name: "fenced code", |
| 71 | in: "```go\nfunc main() {}\n```\n", |
| 72 | contains: []string{"func main()"}, |
| 73 | }, |
| 74 | { |
| 75 | name: "thematic break", |
| 76 | in: "above\n\n---\n\nbelow", |
| 77 | contains: []string{"above", "below", "─"}, |
| 78 | }, |
| 79 | { |
| 80 | name: "gfm table", |
| 81 | in: "| name | size |\n|------|------|\n| a | 12 |\n| bb | 345 |\n", |
| 82 | contains: []string{"name", "size", "a", "12", "bb", "345", "│"}, |
| 83 | notRaw: []string{"|------|"}, // raw separator must be transformed |
| 84 | }, |
| 85 | } |
| 86 | for _, tc := range cases { |
| 87 | t.Run(tc.name, func(t *testing.T) { |
| 88 | out := r.Render(tc.in) |
| 89 | for _, want := range tc.contains { |
| 90 | if !strings.Contains(out, want) { |
| 91 | t.Errorf("Render(%q) missing %q\n--- output ---\n%s", tc.in, want, out) |
| 92 | } |
| 93 | } |
| 94 | for _, leak := range tc.notRaw { |
| 95 | if strings.Contains(out, leak) { |
| 96 | t.Errorf("Render(%q) leaked raw markdown %q", tc.in, leak) |
| 97 | } |
| 98 | } |
| 99 | }) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | // TestWrapAnsiCJK proves the wrap counter treats CJK as 2 cols, so a line of |
| 104 | // Chinese characters wraps at half the column count. |
| 105 | func TestWrapAnsiCJK(t *testing.T) { |
| 106 | // Width 10 = room for 5 Chinese characters per row. |
| 107 | in := strings.Repeat("中", 8) |
| 108 | out := wrapAnsi(in, 10) |
| 109 | lines := strings.Split(out, "\n") |
| 110 | if len(lines) < 2 { |
| 111 | t.Fatalf("expected wrap, got 1 line: %q", out) |
| 112 | } |
| 113 | if visibleWidth(lines[0]) > 10 { |
| 114 | t.Errorf("first line exceeds width: %d > 10", visibleWidth(lines[0])) |
| 115 | } |
| 116 | } |
| 117 |