| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "github.com/charmbracelet/colorprofile" |
| 8 | "github.com/charmbracelet/x/ansi" |
| 9 | |
| 10 | "reasonix/internal/event" |
| 11 | ) |
| 12 | |
| 13 | func TestDiffBodyDropsHeadersKeepsLineNumbers(t *testing.T) { |
| 14 | d := event.FileDiff{Diff: "--- a/x.go\n+++ b/x.go\n@@ -7 +7 @@\n-old\n+new\n", Added: 1, Removed: 1} |
| 15 | joined := strings.Join(diffBody(d, "x.go", 80, 40), "\n") |
| 16 | if strings.Contains(joined, "--- a/") || strings.Contains(joined, "+++ b/") || strings.Contains(joined, "@@") { |
| 17 | t.Fatalf("file/hunk headers should be dropped, got:\n%s", joined) |
| 18 | } |
| 19 | for _, want := range []string{"old", "new", "7"} { |
| 20 | if !strings.Contains(joined, want) { |
| 21 | t.Fatalf("missing %q (code or line number) in:\n%s", want, joined) |
| 22 | } |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | func TestDiffBodyFolds(t *testing.T) { |
| 27 | var b strings.Builder |
| 28 | b.WriteString("--- a/x\n+++ b/x\n@@ -1,8 +1,8 @@\n") |
| 29 | for i := 0; i < 8; i++ { |
| 30 | b.WriteString("+line\n") |
| 31 | } |
| 32 | body := diffBody(event.FileDiff{Diff: b.String()}, "x", 80, 5) |
| 33 | if len(body) != 5 { |
| 34 | t.Fatalf("want 5 rows (4 content + footer), got %d:\n%s", len(body), strings.Join(body, "\n")) |
| 35 | } |
| 36 | // 8 rendered add rows minus the 4 kept = 4 folded. |
| 37 | if !strings.Contains(body[len(body)-1], "4") { |
| 38 | t.Fatalf("footer should report 4 folded lines, got %q", body[len(body)-1]) |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | func TestDiffBodyNoFoldWhenShort(t *testing.T) { |
| 43 | d := event.FileDiff{Diff: "@@ -1 +1 @@\n+a\n"} |
| 44 | if got := len(diffBody(d, "x", 80, 40)); got != 1 { |
| 45 | t.Fatalf("want 1 unfolded row, got %d", got) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | func TestDiffBlockHeader(t *testing.T) { |
| 50 | d := event.FileDiff{Diff: "@@ -1 +1 @@\n-a\n+b\n", Added: 1, Removed: 1} |
| 51 | block := diffBlock("edit_file", `{"path":"pkg/x.go"}`, d, 80, 40) |
| 52 | if len(block) == 0 || !strings.Contains(block[0], "Update") || !strings.Contains(block[0], "pkg/x.go") { |
| 53 | t.Fatalf("header should name verb + path, got %q", block[0]) |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | func TestDiffBlockNilWithoutDiff(t *testing.T) { |
| 58 | if diffBlock("write_file", `{"path":"x"}`, event.FileDiff{}, 80, 40) != nil { |
| 59 | t.Fatal("no diff should yield no block") |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func TestDiffPath(t *testing.T) { |
| 64 | if got := diffPath(`{"path":"a/b.go","old_string":"x"}`); got != "a/b.go" { |
| 65 | t.Fatalf("got %q", got) |
| 66 | } |
| 67 | if got := diffPath(`not json`); got != "" { |
| 68 | t.Fatalf("malformed args should yield empty path, got %q", got) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | func TestDiffBarReappliesBackground(t *testing.T) { |
| 73 | defer func(prev colorprofile.Profile) { activeColorProfile = prev }(activeColorProfile) |
| 74 | activeColorProfile = colorprofile.ANSI256 |
| 75 | |
| 76 | line := diffBar('+', "a + b", "x.go", 40, bgDiffAdd, fgDiffAdd, 12, 3) |
| 77 | // Syntax highlighting emits multiple \033[0m resets; each must re-arm the bar |
| 78 | // background, so the bg sequence appears more than once and the row ends reset. |
| 79 | if strings.Count(line, bgDiffAdd) < 2 { |
| 80 | t.Fatalf("background not re-applied after chroma resets: %q", line) |
| 81 | } |
| 82 | if !strings.HasSuffix(line, ansiReset) { |
| 83 | t.Fatalf("row should end with a reset: %q", line) |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | func TestActiveDiffChromaStyleFollowsCLITheme(t *testing.T) { |
| 88 | previous := activeCLITheme |
| 89 | defer func() { activeCLITheme = previous }() |
| 90 | |
| 91 | tests := []struct { |
| 92 | name string |
| 93 | theme cliPalette |
| 94 | want string |
| 95 | }{ |
| 96 | {name: "dark", theme: cliDarkTheme, want: "github-dark"}, |
| 97 | {name: "light", theme: cliLightTheme, want: "github"}, |
| 98 | } |
| 99 | for _, tt := range tests { |
| 100 | t.Run(tt.name, func(t *testing.T) { |
| 101 | activeCLITheme = tt.theme |
| 102 | if got := activeDiffChromaStyle().Name; got != tt.want { |
| 103 | t.Fatalf("diff syntax style = %q, want %q", got, tt.want) |
| 104 | } |
| 105 | }) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | func TestHighlightCodeUpdatesOnThemeSwitch(t *testing.T) { |
| 110 | previousTheme := activeCLITheme |
| 111 | previousProfile := activeColorProfile |
| 112 | defer func() { |
| 113 | activeCLITheme = previousTheme |
| 114 | activeColorProfile = previousProfile |
| 115 | }() |
| 116 | activeColorProfile = colorprofile.ANSI256 |
| 117 | |
| 118 | code := `const answer = "value"` |
| 119 | activeCLITheme = cliLightTheme |
| 120 | light := highlightCode("example.ts", code) |
| 121 | activeCLITheme = cliDarkTheme |
| 122 | dark := highlightCode("example.ts", code) |
| 123 | |
| 124 | if light == dark { |
| 125 | t.Fatalf("light and dark themes produced identical highlighting: %q", light) |
| 126 | } |
| 127 | for name, got := range map[string]string{"light": light, "dark": dark} { |
| 128 | if plain := ansi.Strip(got); plain != code { |
| 129 | t.Fatalf("%s theme changed code text: got %q, want %q", name, plain, code) |
| 130 | } |
| 131 | } |
| 132 | } |
| 133 |