package uv import ( "strings" "testing" "github.com/charmbracelet/x/ansi" "github.com/rivo/uniseg" ) func TestBufferUniseg(t *testing.T) { cases := []struct { name string input string expected string }{ { name: "empty buffer", input: "", expected: "", }, { name: "single line", input: "Hello, World!", expected: "Hello, World!", }, { name: "multiple lines", input: "Hello, World!\nThis is a test.\nGoodbye!", expected: "Hello, World!\nThis is a test.\nGoodbye!", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { buf := NewBuffer(width(c.input), height(c.input)) for y, line := range strings.Split(c.input, "\n") { var x int seg := uniseg.NewGraphemes(line) for seg.Next() { cell := &Cell{ Content: seg.Str(), Width: seg.Width(), } buf.SetCell(x, y, cell) x += cell.Width } } if got := TrimSpace(buf.String()); got != c.expected { t.Errorf("expected %q, got %q", c.expected, got) } }) } } // TestLineSet tests the Set method of Line func TestLineSet(t *testing.T) { tests := []struct { name string lineLen int x int cell *Cell validate func(t *testing.T, l Line) }{ { name: "set simple cell", lineLen: 10, x: 5, cell: &Cell{Content: "a", Width: 1}, validate: func(t *testing.T, l Line) { if l[5].Content != "a" { t.Errorf("expected 'a' at position 5, got %q", l[5].Content) } }, }, { name: "set out of bounds negative", lineLen: 10, x: -1, cell: &Cell{Content: "a", Width: 1}, validate: func(t *testing.T, l Line) { // Should not panic, just return }, }, { name: "set out of bounds positive", lineLen: 10, x: 10, cell: &Cell{Content: "a", Width: 1}, validate: func(t *testing.T, l Line) { // Should not panic, just return }, }, { name: "overwrite wide cell", lineLen: 10, x: 2, cell: &Cell{Content: "你", Width: 2}, validate: func(t *testing.T, l Line) { // First set a wide cell l.Set(2, &Cell{Content: "你", Width: 2}) // Then overwrite it l.Set(2, &Cell{Content: "a", Width: 1}) if l[2].Content != "a" { t.Errorf("expected 'a' at position 2, got %q", l[2].Content) } }, }, { name: "overwrite middle of wide cell", lineLen: 10, x: 3, cell: &Cell{Content: "a", Width: 1}, validate: func(t *testing.T, l Line) { // First set a wide cell at position 2 l.Set(2, &Cell{Content: "你", Width: 2}) // Then overwrite the middle l.Set(3, &Cell{Content: "a", Width: 1}) if l[3].Content != "a" { t.Errorf("expected 'a' at position 3, got %q", l[3].Content) } // The wide cell should be cleared if l[2].Content != " " { t.Errorf("expected empty at position 2, got %q", l[2].Content) } }, }, { name: "set wide cell at end", lineLen: 10, x: 9, cell: &Cell{Content: "你", Width: 2}, validate: func(t *testing.T, l Line) { l.Set(9, &Cell{Content: "你", Width: 2}) if l[9].Content != " " { // We replace the wide cell with empty spaces because it // doesn't fit in the line. t.Errorf("expected ' ' at position 9, got %q", l[9].Content) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := make(Line, tt.lineLen) l.Set(tt.x, tt.cell) if tt.validate != nil { tt.validate(t, l) } }) } } // TestLineString tests the String method of Line func TestLineString(t *testing.T) { tests := []struct { name string setup func() Line expected string }{ { name: "empty line", setup: func() Line { return NewLine(5) }, expected: "", }, { name: "simple text", setup: func() Line { l := make(Line, 5) l[0] = Cell{Content: "H", Width: 1} l[1] = Cell{Content: "e", Width: 1} l[2] = Cell{Content: "l", Width: 1} l[3] = Cell{Content: "l", Width: 1} l[4] = Cell{Content: "o", Width: 1} return l }, expected: "Hello", }, { name: "with wide characters", setup: func() Line { l := make(Line, 6) l[0] = Cell{Content: "你", Width: 2} l[1] = Cell{} // continuation l[2] = Cell{Content: "好", Width: 2} l[3] = Cell{} // continuation l[4] = Cell{Content: "!", Width: 1} l[5] = Cell{Content: " ", Width: 1} return l }, expected: "你好!", }, { name: "trailing spaces trimmed", setup: func() Line { l := make(Line, 10) l[0] = Cell{Content: "H", Width: 1} l[1] = Cell{Content: "i", Width: 1} // Rest are empty return l }, expected: "Hi", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := tt.setup() got := l.String() if got != tt.expected { t.Errorf("String() = %q, want %q", got, tt.expected) } }) } } // TestBufferMethods tests various Buffer methods func TestBufferMethods(t *testing.T) { t.Run("Width", func(t *testing.T) { // Test empty buffer b := &Buffer{} if w := b.Width(); w != 0 { t.Errorf("Width() = %d, want 0", w) } // Test buffer with lines b = NewBuffer(10, 5) if w := b.Width(); w != 10 { t.Errorf("Width() = %d, want 10", w) } }) t.Run("CellAt", func(t *testing.T) { b := NewBuffer(10, 5) b.SetCell(2, 1, &Cell{Content: "X", Width: 1}) // Test valid position if c := b.CellAt(2, 1); c == nil || c.Content != "X" { t.Errorf("CellAt(2, 1) = %v, want X", c) } // Test out of bounds if c := b.CellAt(-1, 0); c != nil { t.Errorf("CellAt(-1, 0) = %v, want nil", c) } if c := b.CellAt(0, -1); c != nil { t.Errorf("CellAt(0, -1) = %v, want nil", c) } if c := b.CellAt(10, 0); c != nil { t.Errorf("CellAt(10, 0) = %v, want nil", c) } if c := b.CellAt(0, 5); c != nil { t.Errorf("CellAt(0, 5) = %v, want nil", c) } }) t.Run("SetCell", func(t *testing.T) { b := NewBuffer(10, 5) // Test setting cell b.SetCell(2, 1, &Cell{Content: "A", Width: 1}) if c := b.CellAt(2, 1); c == nil || c.Content != "A" { t.Errorf("SetCell failed, got %v", c) } // Test out of bounds b.SetCell(-1, 0, &Cell{Content: "B", Width: 1}) b.SetCell(0, -1, &Cell{Content: "C", Width: 1}) b.SetCell(10, 0, &Cell{Content: "D", Width: 1}) b.SetCell(0, 5, &Cell{Content: "E", Width: 1}) // Should not panic // Test nil cell b.SetCell(3, 1, nil) // Should not panic }) t.Run("Resize", func(t *testing.T) { b := NewBuffer(10, 5) b.SetCell(2, 1, &Cell{Content: "X", Width: 1}) // Resize smaller b.Resize(5, 3) if b.Width() != 5 || b.Height() != 3 { t.Errorf("Resize(5, 3) failed, got %dx%d", b.Width(), b.Height()) } // Resize larger b.Resize(15, 10) if b.Width() != 15 || b.Height() != 10 { t.Errorf("Resize(15, 10) failed, got %dx%d", b.Width(), b.Height()) } // Resize to same size b.Resize(15, 10) if b.Width() != 15 || b.Height() != 10 { t.Errorf("Resize(15, 10) failed, got %dx%d", b.Width(), b.Height()) } }) t.Run("FillArea", func(t *testing.T) { b := NewBuffer(10, 5) cell := &Cell{Content: "X", Width: 1} // Fill a rectangle b.FillArea(cell, Rect(2, 1, 3, 2)) // Check filled area for y := 1; y < 3; y++ { for x := 2; x < 5; x++ { if c := b.CellAt(x, y); c == nil || c.Content != "X" { t.Errorf("FillArea failed at (%d, %d)", x, y) } } } // Check unfilled area if c := b.CellAt(1, 1); c != nil && c.Content == "X" { t.Error("FillArea filled outside rectangle") } // Test with nil cell b.FillArea(nil, Rect(0, 0, 2, 2)) // Should not panic }) t.Run("TouchLine", func(t *testing.T) { b := NewRenderBuffer(10, 5) // Touch valid line b.TouchLine(2, 1, 3) // Should mark line as dirty // Touch out of bounds b.TouchLine(0, -1, 1) b.TouchLine(0, 5, 1) // Should not panic }) t.Run("Touch", func(t *testing.T) { b := NewRenderBuffer(10, 5) b.Touch(2, 1) // Should mark cell as dirty }) t.Run("Clear", func(t *testing.T) { b := NewBuffer(10, 5) b.SetCell(2, 1, &Cell{Content: "X", Width: 1}) b.Clear() // Check all cells are cleared for y := 0; y < b.Height(); y++ { for x := 0; x < b.Width(); x++ { if c := b.CellAt(x, y); c != nil && c.Content != " " { t.Errorf("Clear failed at (%d, %d)", x, y) } } } }) t.Run("Clone", func(t *testing.T) { b := NewBuffer(10, 5) b.SetCell(2, 1, &Cell{Content: "X", Width: 1}) clone := b.Clone() // Check clone has same content if c := clone.CellAt(2, 1); c == nil || c.Content != "X" { t.Error("Clone failed to copy content") } // Modify clone shouldn't affect original clone.SetCell(2, 1, &Cell{Content: "Y", Width: 1}) if c := b.CellAt(2, 1); c == nil || c.Content != "X" { t.Error("Clone is not independent") } }) t.Run("CloneArea", func(t *testing.T) { b := NewBuffer(10, 5) b.SetCell(2, 1, &Cell{Content: "X", Width: 1}) b.SetCell(3, 2, &Cell{Content: "Y", Width: 1}) clone := b.CloneArea(Rect(2, 1, 2, 2)) // Check clone has correct size if clone.Width() != 2 || clone.Height() != 2 { t.Errorf("CloneArea size = %dx%d, want 2x2", clone.Width(), clone.Height()) } // Check content if c := clone.CellAt(0, 0); c == nil || c.Content != "X" { t.Error("CloneArea failed to copy X") } if c := clone.CellAt(1, 1); c == nil || c.Content != "Y" { t.Error("CloneArea failed to copy Y") } }) t.Run("Draw", func(t *testing.T) { // Create source buffer src := NewBuffer(3, 3) src.SetCell(1, 1, &Cell{Content: "S", Width: 1}) // Create destination screen buffer (which implements Screen interface) dst := NewScreenBuffer(10, 5) dst.SetCell(2, 2, &Cell{Content: "D", Width: 1}) // Draw source onto destination at area (1,1) to (4,4) src.Draw(&dst, Rect(1, 1, 4, 4)) // Check drawn content if c := dst.CellAt(2, 2); c == nil || c.Content != "S" { t.Error("Draw failed to copy content") } // Check original destination content is preserved outside draw area if c := dst.CellAt(0, 0); c != nil && c.Content != " " { t.Error("Draw modified content outside draw area") } }) t.Run("Render", func(t *testing.T) { b := NewBuffer(5, 2) b.SetCell(0, 0, &Cell{Content: "H", Width: 1}) b.SetCell(1, 0, &Cell{Content: "i", Width: 1}) b.SetCell(0, 1, &Cell{Content: "!", Width: 1}) got := b.Render() // The exact output depends on the rendering logic if !strings.Contains(got, "Hi") { t.Errorf("Render output doesn't contain expected text") } }) } // TestBufferLineOperations tests line insertion and deletion func TestBufferLineOperations(t *testing.T) { t.Run("InsertLine", func(t *testing.T) { b := NewBuffer(5, 3) b.SetCell(0, 0, &Cell{Content: "A", Width: 1}) b.SetCell(0, 1, &Cell{Content: "B", Width: 1}) b.SetCell(0, 2, &Cell{Content: "C", Width: 1}) // Insert line at position 1 b.InsertLine(1, 1, nil) // Check that B moved to line 2 if c := b.CellAt(0, 2); c == nil || c.Content != "B" { t.Error("InsertLine failed to move line down") } // Check that new line is empty if c := b.CellAt(0, 1); c != nil && c.Content != " " { t.Error("InsertLine didn't create empty line") } }) t.Run("InsertLineArea", func(t *testing.T) { b := NewBuffer(5, 5) b.SetCell(0, 1, &Cell{Content: "A", Width: 1}) b.SetCell(0, 2, &Cell{Content: "B", Width: 1}) // Insert line in area b.InsertLineArea(2, 1, nil, Rect(0, 1, 5, 4)) // Check that B moved down if c := b.CellAt(0, 3); c == nil || c.Content != "B" { t.Error("InsertLineArea failed to move line down") } }) t.Run("DeleteLine", func(t *testing.T) { b := NewBuffer(5, 3) b.SetCell(0, 0, &Cell{Content: "A", Width: 1}) b.SetCell(0, 1, &Cell{Content: "B", Width: 1}) b.SetCell(0, 2, &Cell{Content: "C", Width: 1}) // Delete line at position 1 b.DeleteLine(1, 1, nil) // Check that C moved to line 1 if c := b.CellAt(0, 1); c == nil || c.Content != "C" { t.Error("DeleteLine failed to move line up") } // Check that last line is empty if c := b.CellAt(0, 2); c != nil && c.Content != " " { t.Error("DeleteLine didn't clear last line") } }) t.Run("DeleteLineArea", func(t *testing.T) { b := NewBuffer(5, 5) b.SetCell(0, 1, &Cell{Content: "A", Width: 1}) b.SetCell(0, 2, &Cell{Content: "B", Width: 1}) b.SetCell(0, 3, &Cell{Content: "C", Width: 1}) // Delete line in area b.DeleteLineArea(2, 1, nil, Rect(0, 1, 5, 4)) // Check that C moved up if c := b.CellAt(0, 2); c == nil || c.Content != "C" { t.Error("DeleteLineArea failed to move line up") } }) } // TestBufferCellOperations tests cell insertion and deletion func TestBufferCellOperations(t *testing.T) { t.Run("InsertCell", func(t *testing.T) { b := NewBuffer(5, 2) l := b.Line(0) l[0] = Cell{Content: "A", Width: 1} l[1] = Cell{Content: "B", Width: 1} l[2] = Cell{Content: "C", Width: 1} // Insert cell at position 1 b.InsertCell(1, 0, 1, nil) // Check that B moved right if c := b.CellAt(2, 0); c == nil || c.Content != "B" { t.Error("InsertCell failed to move cell right") } }) t.Run("InsertCellArea", func(t *testing.T) { b := NewBuffer(5, 3) l := b.Line(1) l[1] = Cell{Content: "A", Width: 1} l[2] = Cell{Content: "B", Width: 1} // Insert cell in area b.InsertCellArea(1, 1, 1, nil, Rect(1, 1, 4, 2)) // Check that A moved right if c := b.CellAt(2, 1); c == nil || c.Content != "A" { t.Error("InsertCellArea failed to move cell right") } }) t.Run("DeleteCell", func(t *testing.T) { b := NewBuffer(5, 2) l := b.Line(0) l[0] = Cell{Content: "A", Width: 1} l[1] = Cell{Content: "B", Width: 1} l[2] = Cell{Content: "C", Width: 1} // Delete cell at position 1 b.DeleteCell(1, 0, 1, nil) // Check that C moved left if c := b.CellAt(1, 0); c == nil || c.Content != "C" { t.Error("DeleteCell failed to move cell left") } }) t.Run("DeleteCellArea", func(t *testing.T) { b := NewBuffer(5, 3) l := b.Line(1) l[1] = Cell{Content: "A", Width: 1} l[2] = Cell{Content: "B", Width: 1} l[3] = Cell{Content: "C", Width: 1} // Delete cell in area b.DeleteCellArea(2, 1, 1, nil, Rect(1, 1, 4, 2)) // Check that C moved left if c := b.CellAt(2, 1); c == nil || c.Content != "C" { t.Error("DeleteCellArea failed to move cell left") } }) } // TestScreenBuffer tests ScreenBuffer specific functionality func TestScreenBuffer(t *testing.T) { t.Run("NewScreenBuffer", func(t *testing.T) { sb := NewScreenBuffer(10, 5) if sb.Width() != 10 || sb.Height() != 5 { t.Errorf("NewScreenBuffer size = %dx%d, want 10x5", sb.Width(), sb.Height()) } }) t.Run("WidthMethod", func(t *testing.T) { sb := NewScreenBuffer(10, 5) // Get the default width method method := sb.WidthMethod() if method == nil { t.Error("WidthMethod returned nil") } // The method should be set to ansi.WcWidth by default // We can test it works by checking a simple character if width := method.StringWidth("a"); width != 1 { t.Errorf("Default width method returned %d for 'a', want 1", width) } }) } // TestLineRenderLine tests the renderLine function func TestLineRenderLine(t *testing.T) { tests := []struct { name string setup func() Line validate func(t *testing.T, output string) }{ { name: "render with styles", setup: func() Line { l := make(Line, 5) l[0] = Cell{Content: "H", Width: 1, Style: Style{Fg: ansi.Red}} l[1] = Cell{Content: "i", Width: 1} return l }, validate: func(t *testing.T, output string) { // Should contain style sequences if !strings.Contains(output, "H") || !strings.Contains(output, "i") { t.Error("renderLine failed to include content") } }, }, { name: "render with hyperlink", setup: func() Line { l := make(Line, 5) l[0] = Cell{Content: "L", Width: 1, Link: Link{URL: "http://example.com"}} l[1] = Cell{Content: "i", Width: 1, Link: Link{URL: "http://example.com"}} l[2] = Cell{Content: "n", Width: 1, Link: Link{URL: "http://example.com"}} l[3] = Cell{Content: "k", Width: 1, Link: Link{URL: "http://example.com"}} return l }, validate: func(t *testing.T, output string) { if !strings.Contains(output, "Link") { t.Error("renderLine failed to include link content") } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := tt.setup() var sb strings.Builder renderLine(&sb, l) output := sb.String() if tt.validate != nil { tt.validate(t, output) } }) } } func TestRenderLinePreservesTrailingSpaces(t *testing.T) { // renderLine should preserve trailing empty cells as spaces. // A line of width 10 with "Hi" at positions 0-1 and empty cells at 2-9 // should render all 10 visible characters: "Hi" followed by 8 spaces. l := NewLine(10) l[0] = Cell{Content: "H", Width: 1} l[1] = Cell{Content: "i", Width: 1} // Positions 2-9 remain EmptyCell (space, width 1, no style) output := l.Render() // Strip any ANSI sequences to count visible characters. visible := ansi.Strip(output) if len(visible) != 10 { t.Errorf("Render() produced %d visible chars, want 10; output=%q visible=%q", len(visible), output, visible) } } func TestRenderLinePreservesStyledTrailingSpaces(t *testing.T) { // When trailing cells have explicit styling (e.g. background color), // renderLine must emit them with their ANSI attributes, not drop them. l := NewLine(5) l[0] = Cell{Content: "A", Width: 1} // Positions 1-4: spaces with a red background. for i := 1; i < 5; i++ { l[i] = Cell{Content: " ", Width: 1, Style: Style{Bg: ansi.Red}} } output := l.Render() visible := ansi.Strip(output) if len(visible) != 5 { t.Errorf("Render() produced %d visible chars, want 5; output=%q visible=%q", len(visible), output, visible) } // The output must contain the background color sequence for the trailing // spaces. Derive the expected sequence from the style itself rather than // hardcoding an SGR number. red := Style{Bg: ansi.Red} wantSeq := red.Diff(&Style{}) if !strings.Contains(output, wantSeq) { t.Errorf("Render() missing background SGR %q for trailing styled spaces; output=%q", wantSeq, output) } } func TestRenderLineWideCellTail(t *testing.T) { // A line ending in a wide cell's continuation column (a zero cell) must // not disturb pending empty cells: the zero cell is skipped, and the // buffered spaces are still flushed at end of line. l := NewLine(6) l[0] = Cell{Content: "A", Width: 1} // Positions 1-3 remain EmptyCell. l[4] = Cell{Content: "你", Width: 2} l[5] = Cell{} // continuation column of the wide cell output := l.Render() visible := ansi.Strip(output) // "A", 3 buffered spaces, then the wide grapheme. The continuation // column contributes nothing. if want := "A 你"; visible != want { t.Errorf("Render() = %q, want %q; output=%q", visible, want, output) } if w := uniseg.StringWidth(visible); w != 6 { t.Errorf("Render() visible width = %d, want 6; output=%q", w, output) } } func BenchmarkBufferSetCell(b *testing.B) { buf := NewBuffer(80, 24) cell := &Cell{Content: "A", Width: 1} b.ResetTimer() for i := 0; i < b.N; i++ { x := i % 80 y := (i / 80) % 24 buf.SetCell(x, y, cell) } } func BenchmarkBufferResize(b *testing.B) { buf := NewBuffer(80, 24) b.ResetTimer() for i := 0; i < b.N; i++ { width := 80 + (i % 20) height := 24 + (i % 10) buf.Resize(width, height) bufWidth, bufHeight := buf.Width(), buf.Height() if bufWidth != width || bufHeight != height { b.Errorf("Resize failed: got %dx%d, want %dx%d", bufWidth, bufHeight, width, height) } } } func width(s string) int { width := 0 for _, line := range strings.Split(s, "\n") { width = max(width, ansi.StringWidth(line)) } return width } func height(s string) int { return strings.Count(s, "\n") + 1 }