Download
101 lines 3.3 KiBGoModified 2026-10-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
package diffview

import (
	"strconv"
	"strings"
	"testing"

	"github.com/charmbracelet/x/ansi"
)

func TestRenderDiff(t *testing.T) {
	if from, to := changed(`say("hello there")`, `say("hello")`); from != 10 || to != 16 {
		t.Errorf("changed = %d, %d; want 10, 16", from, to)
	}
	if from, to := changed("abc", "xyz"); from != to {
		t.Errorf("a wholly changed line should have no emphasis, got %d, %d", from, to)
	}
	long := "+" + strings.Repeat("x", 100)
	out := Render("@@ -1 +1 @@\n-old\n"+long+"\n context", 0, 40, Dark)
	lines := strings.Split(out, "\n")
	if len(lines) != 6 {
		t.Fatalf("got %d lines, want 6:\n%s", len(lines), out)
	}
	for _, l := range lines[1:5] {
		if w := ansi.StringWidth(l); w != 40 {
			t.Errorf("row %q is %d wide, want 40", ansi.Strip(l), w)
		}
	}
	if got := strings.Join([]string{ansi.Strip(lines[2]), ansi.Strip(lines[3]), ansi.Strip(lines[4])}, ""); strings.ReplaceAll(got, " ", "") != long {
		t.Errorf("the wrapped line lost text: %q", got)
	}
}

// Inside a hunk, "---" is a removed line starting "--" (an SQL comment),
// not a file header, and is shown as removed.
func TestRenderDiffDashLines(t *testing.T) {
	out := Render("--- f\n+++ f\n@@ -1 +1 @@\n--- drop table\n+++ added\n", 0, 40, Dark)
	lines := strings.Split(out, "\n")
	if len(lines) != 5 {
		t.Fatalf("got %d lines:\n%s", len(lines), out)
	}
	for i, l := range lines {
		// Changed lines are rows padded to the width; headers are not.
		if row := ansi.StringWidth(l) == 40; row != (i >= 3) {
			t.Errorf("line %d (%q) shown as a row: %v", i, ansi.Strip(l), row)
		}
	}
}

// A second file's headers, after a hunk that ends in removed lines, are
// headers, not changed lines.
func TestRenderSecondFile(t *testing.T) {
	out := Render("--- a\n+++ a\n@@ -1,2 +1,1 @@\n x\n-y\n--- b\n+++ b\n@@ -1 +1 @@\n-old\n+new\n\\ No newline at end of file\n", 0, 40, Dark)
	lines := strings.Split(out, "\n")
	if len(lines) != 11 {
		t.Fatalf("got %d lines:\n%s", len(lines), out)
	}
	for i, l := range lines {
		// Changed lines are rows padded to the width; headers are not.
		if row := ansi.StringWidth(l) == 40; row != (i == 4 || i == 8 || i == 9) {
			t.Errorf("line %d (%q) shown as a row: %v", i, ansi.Strip(l), row)
		}
	}
}

func TestVisible(t *testing.T) {
	cases := map[string]string{
		"ls\x1b]0;rm -rf ~\x07": `ls\x1b]0;rm -rf ~\a`,
		"a\rb\r\nc":             `a\rb\r` + "\nc", // CRLF shows: a diff may change only that
		"bidi\u202eevil":        `bidi\u202eevil`,
		"zero\u200bwidth":       `zero\u200bwidth`,
		"c1\u009bcontrol":       `c1\u009bcontrol`,
		"tab\there é ✓":         "tab    here é ✓",
	}
	for in, want := range cases {
		if got := Visible(in); got != want {
			t.Errorf("Visible(%q) = %q, want %q", in, got, want)
		}
	}
}

// Visible output is printable characters and newlines only, line by line.
func FuzzVisible(f *testing.F) {
	for _, s := range []string{
		"plain\ntext", "visible\x1b]0;x hidden\nline2", "c1\u009b31m \u009d0;t\u009c",
		"\x9b\xff invalid", "bidi\u202eevil\u2066x\u200f", "tag\U000e0041", "ㅤx͏️⠀",
	} {
		f.Add(s)
	}
	f.Fuzz(func(t *testing.T, s string) {
		out := Visible(s)
		for _, r := range out {
			if r != '\n' && (!strconv.IsPrint(r) || blank(r)) {
				t.Fatalf("Visible(%q) = %q: holds %U", s, out, r)
			}
		}
		if strings.Count(out, "\n") != strings.Count(s, "\n") {
			t.Fatalf("Visible(%q) = %q: lines changed", s, out)
		}
	})
}