200 lines
7.2 KiB
Go
200 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/charmbracelet/x/ansi"
|
|
)
|
|
|
|
func TestParseCommentBodyExtractsSuggestionFences(t *testing.T) {
|
|
body := "Use the clearer name:\n\n```suggestion:-0+0\n better_name = value\n```\n\nThis also matches the API."
|
|
got := parseCommentBody(body)
|
|
if len(got.Suggestions) != 1 || got.Suggestions[0] != " better_name = value" {
|
|
t.Fatalf("suggestions = %#v", got.Suggestions)
|
|
}
|
|
if strings.Contains(got.Prose, "```") || !strings.Contains(got.Prose, "Use the clearer name") ||
|
|
!strings.Contains(got.Prose, "matches the API") {
|
|
t.Fatalf("prose = %q", got.Prose)
|
|
}
|
|
}
|
|
|
|
func TestReviewedSourceLinesUseHistoricalSelection(t *testing.T) {
|
|
comment := ReviewComment{
|
|
DiffHunk: "@@ -38,5 +38,5 @@\n context\n old_name = value\n return old_name\n context\n context",
|
|
OriginalStartLine: 39,
|
|
OriginalLine: 40,
|
|
}
|
|
got := reviewedSourceLines(ReviewThread{DiffSide: "RIGHT"}, comment)
|
|
want := []string{"old_name = value", "return old_name"}
|
|
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
|
t.Fatalf("reviewed source = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSuggestionPreservesRelativeIndent(t *testing.T) {
|
|
removed, added := normalizeSuggestion(
|
|
[]string{" if old:", " run_old()"},
|
|
" if new:\n run_new()",
|
|
)
|
|
if strings.Join(removed, "\n") != "if old:\n run_old()" {
|
|
t.Fatalf("removed = %#v", removed)
|
|
}
|
|
if strings.Join(added, "\n") != "if new:\n run_new()" {
|
|
t.Fatalf("added = %#v", added)
|
|
}
|
|
}
|
|
|
|
func TestDetailRendersSuggestionAsRemovalAndAddition(t *testing.T) {
|
|
m := NewApp(nil, "o", "r", false, 50, 10)
|
|
m.width = 100
|
|
m.details = PRDetails{Threads: []ReviewThread{{
|
|
Path: "example.py",
|
|
Line: 10,
|
|
DiffSide: "RIGHT",
|
|
Comments: []ReviewComment{{
|
|
Author: "reviewer",
|
|
Body: "Use this instead:\n```suggestion\nnew_value = compute()\nreturn new_value\n```",
|
|
DiffHunk: "@@ -10,2 +10,2 @@\nold_value = compute()\nreturn old_value",
|
|
OriginalLine: 11,
|
|
OriginalStartLine: 10,
|
|
}},
|
|
}}}
|
|
|
|
var (
|
|
removed, added int
|
|
rendered strings.Builder
|
|
)
|
|
for _, line := range m.detailLines(60) {
|
|
rendered.WriteString(ansi.Strip(line.rail + line.fixed + line.text))
|
|
rendered.WriteByte('\n')
|
|
switch line.suggestionChange {
|
|
case '-':
|
|
removed++
|
|
case '+':
|
|
added++
|
|
}
|
|
}
|
|
if removed != 2 || added != 2 {
|
|
t.Fatalf("suggestion rows: removed=%d added=%d\n%s", removed, added, rendered.String())
|
|
}
|
|
if strings.Contains(rendered.String(), "```suggestion") ||
|
|
!strings.Contains(rendered.String(), "│ @reviewer") ||
|
|
!strings.Contains(rendered.String(), "│ suggested change") {
|
|
t.Fatalf("suggestion was not rendered structurally:\n%s", rendered.String())
|
|
}
|
|
}
|
|
|
|
func TestRenderedSuggestionsAreCachedWithoutSharingMutableLines(t *testing.T) {
|
|
renderedSuggestions.clear()
|
|
reviewed := []string{"old_value = compute()", "return old_value"}
|
|
first := renderSuggestion(
|
|
"example.py", reviewed, "new_value = compute()\nreturn new_value", 50,
|
|
)
|
|
if len(renderedSuggestions.entries) != 1 {
|
|
t.Fatalf("suggestion cache entries = %d, want 1", len(renderedSuggestions.entries))
|
|
}
|
|
first.removed[0].rail = "mutated"
|
|
second := renderSuggestion(
|
|
"example.py", reviewed, "new_value = compute()\nreturn new_value", 50,
|
|
)
|
|
if second.removed[0].rail != "" {
|
|
t.Fatal("caller mutation changed cached suggestion lines")
|
|
}
|
|
if len(renderedSuggestions.entries) != 1 {
|
|
t.Fatalf("cache miss for unchanged suggestion: %d entries", len(renderedSuggestions.entries))
|
|
}
|
|
}
|
|
|
|
func TestSuggestionBackgroundIsDirectionalWithoutTextUnderline(t *testing.T) {
|
|
removed := suggestionHighlight(" - ", "old", 12, '-')
|
|
added := suggestionHighlight(" + ", "new", 12, '+')
|
|
if !strings.Contains(removed, suggestionRemoveBackground) ||
|
|
!strings.Contains(added, suggestionAddBackground) {
|
|
t.Fatalf("suggestion decorations missing: removed=%q added=%q", removed, added)
|
|
}
|
|
if strings.Contains(removed, "\x1b[4m") || strings.Contains(added, "\x1b[4m") ||
|
|
strings.Contains(removed, "\x1b[4;") || strings.Contains(added, "\x1b[4;") ||
|
|
strings.Contains(removed, "\x1b[58;") || strings.Contains(added, "\x1b[58;") {
|
|
t.Fatal("suggestion rendering still enables terminal underlining")
|
|
}
|
|
if ansi.StringWidth(removed) != 12 || ansi.StringWidth(added) != 12 {
|
|
t.Fatal("suggestion backgrounds do not fill the row")
|
|
}
|
|
if !strings.Contains(removed, "\x1b[0m"+suggestionRemoveBackground+" ") ||
|
|
!strings.Contains(added, "\x1b[0m"+suggestionAddBackground+" ") {
|
|
t.Fatal("padded row remainder is still underlined")
|
|
}
|
|
}
|
|
|
|
func TestMultipleSuggestionsStayInsideCommentBlock(t *testing.T) {
|
|
m := NewApp(nil, "o", "r", false, 50, 10)
|
|
m.width = 100
|
|
m.details = PRDetails{Threads: []ReviewThread{{
|
|
Path: "main.go", Line: 1, DiffSide: "RIGHT",
|
|
Comments: []ReviewComment{{
|
|
Author: "alice", OriginalLine: 1,
|
|
DiffHunk: "@@ -1 +1 @@\nold",
|
|
Body: "```suggestion\nfirst\n```\n```suggestion\nsecond\n```",
|
|
}},
|
|
}}}
|
|
var rendered strings.Builder
|
|
for _, line := range m.detailLines(60) {
|
|
rendered.WriteString(ansi.Strip(line.rail + line.fixed + line.text))
|
|
rendered.WriteByte('\n')
|
|
}
|
|
if !strings.Contains(rendered.String(), "│ @alice") ||
|
|
!strings.Contains(rendered.String(), "│ suggested change 1/2") ||
|
|
!strings.Contains(rendered.String(), "│ suggested change 2/2") {
|
|
t.Fatalf("suggestions escaped the comment block:\n%s", rendered.String())
|
|
}
|
|
}
|
|
|
|
func TestSuggestionGutterHasNoLeadingPadding(t *testing.T) {
|
|
lines := wrapSuggestionLine("main.go", "replacement()", '+', fullCodeRange("replacement()"), 30)
|
|
if len(lines) == 0 || ansi.Strip(lines[0].fixed) != "+ " {
|
|
t.Fatalf("suggestion gutter = %q, want compact marker", ansi.Strip(lines[0].fixed))
|
|
}
|
|
}
|
|
|
|
func TestSuggestionChangedRangesAlignInsertedLines(t *testing.T) {
|
|
removed := []string{"def setup():", " if ready:", " run()"}
|
|
added := []string{"def setup():", " super().setup()", " if ready:", " run()"}
|
|
removedRanges, addedRanges := suggestionChangedRanges(removed, added)
|
|
|
|
for i, changed := range removedRanges {
|
|
if changed.End != changed.Start {
|
|
t.Fatalf("unchanged removed line %d marked as changed: %#v", i, changed)
|
|
}
|
|
}
|
|
for _, index := range []int{0, 2, 3} {
|
|
if changed := addedRanges[index]; changed.End != changed.Start {
|
|
t.Fatalf("unchanged added line %d marked as changed: %#v", index, changed)
|
|
}
|
|
}
|
|
if addedRanges[1] != fullCodeRange(added[1]) {
|
|
t.Fatalf("inserted line range = %#v", addedRanges[1])
|
|
}
|
|
}
|
|
|
|
func TestSuggestionChangedRangeIsCharacterPrecise(t *testing.T) {
|
|
removed, added := changedRange("value = old_name()", "value = new_name()")
|
|
if got := "value = old_name()"[removed.Start:removed.End]; got != "old" {
|
|
t.Fatalf("removed range selected %q", got)
|
|
}
|
|
if got := "value = new_name()"[added.Start:added.End]; got != "new" {
|
|
t.Fatalf("added range selected %q", got)
|
|
}
|
|
}
|
|
|
|
func TestChangedSnippetGetsDarkerBackground(t *testing.T) {
|
|
code := highlightedSource("go", "value := oldName()")
|
|
decorated := highlightCodeRange(code, codeRange{Start: 9, End: 16}, '-')
|
|
if !strings.Contains(decorated, "\x1b[48;2;55;0;0m") {
|
|
t.Fatalf("darker intra-line background missing: %q", decorated)
|
|
}
|
|
if ansi.Strip(decorated) != "value := oldName()" {
|
|
t.Fatalf("highlight altered source: %q", ansi.Strip(decorated))
|
|
}
|
|
}
|