feat: added tests for the buffer and window

This commit is contained in:
Hayden Hargreaves 2026-03-06 18:27:16 -07:00
parent 15d847e3c8
commit 354fbc4f9b
2 changed files with 795 additions and 0 deletions

View File

@ -0,0 +1,302 @@
package core
import "testing"
// --------------------------------------------------
// Buffer Tests (generated by ClaudeCode)
// --------------------------------------------------
func TestBuffer_InsertLine(t *testing.T) {
t.Run("inserts at beginning", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "line 2"}).
Build()
buf.InsertLine(0, "new line")
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
if buf.Line(0) != "new line" {
t.Errorf("expected 'new line', got '%s'", buf.Line(0))
}
if buf.Line(1) != "line 1" {
t.Errorf("expected 'line 1' at index 1, got '%s'", buf.Line(1))
}
})
t.Run("inserts at end", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "line 2"}).
Build()
buf.InsertLine(2, "new line")
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
if buf.Line(2) != "new line" {
t.Errorf("expected 'new line' at end, got '%s'", buf.Line(2))
}
})
t.Run("inserts in middle", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "line 3"}).
Build()
buf.InsertLine(1, "line 2")
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
if buf.Line(1) != "line 2" {
t.Errorf("expected 'line 2' at index 1, got '%s'", buf.Line(1))
}
})
t.Run("handles empty buffer", func(t *testing.T) {
buf := NewBufferBuilder().Build()
buf.InsertLine(0, "first line")
if buf.LineCount() != 2 { // Original empty line + new line
t.Errorf("expected 2 lines, got %d", buf.LineCount())
}
})
}
func TestBuffer_DeleteLine(t *testing.T) {
t.Run("deletes from beginning", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"DELETE ME", "line 2", "line 3"}).
Build()
buf.DeleteLine(0)
if buf.LineCount() != 2 {
t.Errorf("expected 2 lines, got %d", buf.LineCount())
}
if buf.Line(0) != "line 2" {
t.Errorf("expected 'line 2' at index 0, got '%s'", buf.Line(0))
}
})
t.Run("deletes from middle", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "DELETE ME", "line 3"}).
Build()
buf.DeleteLine(1)
if buf.LineCount() != 2 {
t.Errorf("expected 2 lines, got %d", buf.LineCount())
}
if buf.Line(1) != "line 3" {
t.Errorf("expected 'line 3' at index 1, got '%s'", buf.Line(1))
}
})
t.Run("deletes from end", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "line 2", "DELETE ME"}).
Build()
buf.DeleteLine(2)
if buf.LineCount() != 2 {
t.Errorf("expected 2 lines, got %d", buf.LineCount())
}
if buf.Line(1) != "line 2" {
t.Errorf("expected 'line 2' at index 1, got '%s'", buf.Line(1))
}
})
t.Run("can delete all lines", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"only line"}).
Build()
buf.DeleteLine(0)
// Buffer allows being completely empty (0 lines)
if buf.LineCount() != 0 {
t.Errorf("expected 0 lines after deleting last line, got %d", buf.LineCount())
}
})
}
func TestBuffer_SetLine(t *testing.T) {
t.Run("updates existing line", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"old content"}).
Build()
buf.SetLine(0, "new content")
if buf.Line(0) != "new content" {
t.Errorf("expected 'new content', got '%s'", buf.Line(0))
}
})
t.Run("updates middle line", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "old", "line 3"}).
Build()
buf.SetLine(1, "new")
if buf.Line(1) != "new" {
t.Errorf("expected 'new', got '%s'", buf.Line(1))
}
// Verify other lines unchanged
if buf.Line(0) != "line 1" {
t.Error("line 0 should be unchanged")
}
if buf.Line(2) != "line 3" {
t.Error("line 2 should be unchanged")
}
})
t.Run("can set to empty string", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"has content"}).
Build()
buf.SetLine(0, "")
if buf.Line(0) != "" {
t.Errorf("expected empty line, got '%s'", buf.Line(0))
}
})
}
func TestBuffer_LineCount(t *testing.T) {
t.Run("empty buffer has one line", func(t *testing.T) {
buf := NewBufferBuilder().Build()
if buf.LineCount() != 1 {
t.Errorf("expected 1 line in empty buffer, got %d", buf.LineCount())
}
})
t.Run("counts multiple lines", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"a", "b", "c", "d", "e"}).
Build()
if buf.LineCount() != 5 {
t.Errorf("expected 5 lines, got %d", buf.LineCount())
}
})
t.Run("counts after insertions", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1"}).
Build()
buf.InsertLine(1, "line 2")
buf.InsertLine(2, "line 3")
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
})
t.Run("counts after deletions", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"a", "b", "c", "d", "e"}).
Build()
buf.DeleteLine(2)
buf.DeleteLine(2)
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines after 2 deletions, got %d", buf.LineCount())
}
})
}
func TestBuffer_Line(t *testing.T) {
t.Run("retrieves correct line", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"first", "second", "third"}).
Build()
if buf.Line(0) != "first" {
t.Errorf("expected 'first', got '%s'", buf.Line(0))
}
if buf.Line(1) != "second" {
t.Errorf("expected 'second', got '%s'", buf.Line(1))
}
if buf.Line(2) != "third" {
t.Errorf("expected 'third', got '%s'", buf.Line(2))
}
})
t.Run("handles special characters", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"hello\tworld", "foo\nbar"}).
Build()
if buf.Line(0) != "hello\tworld" {
t.Errorf("expected tabs preserved, got '%s'", buf.Line(0))
}
})
t.Run("handles unicode", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"hello 世界", "emoji 🎉"}).
Build()
if buf.Line(0) != "hello 世界" {
t.Errorf("expected unicode preserved, got '%s'", buf.Line(0))
}
if buf.Line(1) != "emoji 🎉" {
t.Errorf("expected emoji preserved, got '%s'", buf.Line(1))
}
})
}
func TestBufferBuilder(t *testing.T) {
t.Run("builds with default values", func(t *testing.T) {
buf := NewBufferBuilder().Build()
if buf.LineCount() != 1 {
t.Errorf("expected 1 empty line, got %d", buf.LineCount())
}
if buf.Filename != "" {
t.Errorf("expected empty filename, got '%s'", buf.Filename)
}
})
t.Run("builds with custom lines", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "line 2"}).
Build()
if buf.LineCount() != 2 {
t.Errorf("expected 2 lines, got %d", buf.LineCount())
}
})
t.Run("builds with filename", func(t *testing.T) {
buf := NewBufferBuilder().
WithFilename("test.txt").
Build()
if buf.Filename != "test.txt" {
t.Errorf("expected filename 'test.txt', got '%s'", buf.Filename)
}
})
t.Run("builds with filetype", func(t *testing.T) {
buf := NewBufferBuilder().
WithFiletype("go").
Build()
if buf.Filetype != "go" {
t.Errorf("expected filetype 'go', got '%s'", buf.Filetype)
}
})
}

View File

@ -0,0 +1,493 @@
package core
import "testing"
// --------------------------------------------------
// Window Tests (generated by ClaudeCode)
// --------------------------------------------------
func TestWindow_SetCursorLine(t *testing.T) {
t.Run("clamps cursor below zero", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "line 2", "line 3"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetCursorLine(-5)
if win.Cursor.Line != 0 {
t.Errorf("expected cursor at line 0, got %d", win.Cursor.Line)
}
})
t.Run("clamps cursor past end", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "line 2", "line 3"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetCursorLine(999)
if win.Cursor.Line != 2 { // 3 lines, max index is 2
t.Errorf("expected cursor at line 2, got %d", win.Cursor.Line)
}
})
t.Run("allows valid position", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"line 1", "line 2", "line 3"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetCursorLine(1)
if win.Cursor.Line != 1 {
t.Errorf("expected cursor at line 1, got %d", win.Cursor.Line)
}
})
t.Run("handles empty buffer", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetCursorLine(5)
if win.Cursor.Line != 0 {
t.Errorf("expected cursor at line 0 for empty buffer, got %d", win.Cursor.Line)
}
})
}
func TestWindow_SetCursorCol(t *testing.T) {
t.Run("clamps to line length", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"hello"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetCursorCol(999)
// "hello" is 5 chars, max col should be 5 (after last char for insert mode)
if win.Cursor.Col > 5 {
t.Errorf("expected cursor col <= 5, got %d", win.Cursor.Col)
}
})
t.Run("clamps below zero", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"hello"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetCursorCol(-10)
if win.Cursor.Col != 0 {
t.Errorf("expected cursor col 0, got %d", win.Cursor.Col)
}
})
t.Run("handles empty line", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{""}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetCursorCol(5)
if win.Cursor.Col != 0 {
t.Errorf("expected cursor at col 0 on empty line, got %d", win.Cursor.Col)
}
})
t.Run("allows cursor at end of line", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"hello"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetCursorCol(5) // After last char
if win.Cursor.Col != 5 {
t.Errorf("expected cursor at col 5, got %d", win.Cursor.Col)
}
})
}
func TestWindow_AdjustScroll(t *testing.T) {
t.Run("scrolls down when cursor goes below viewport", func(t *testing.T) {
// Create buffer with many lines
lines := make([]string, 100)
for i := range lines {
lines[i] = "line"
}
buf := NewBufferBuilder().WithLines(lines).Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithHeight(24).
Build()
// Start at top
win.SetCursorLine(0)
win.AdjustScroll()
initialScroll := win.ScrollY
// Move cursor way down
win.SetCursorLine(50)
win.AdjustScroll()
// Scroll should have increased
if win.ScrollY <= initialScroll {
t.Errorf("expected scroll to increase, was %d, now %d", initialScroll, win.ScrollY)
}
// Cursor should be visible
viewport := win.ViewportHeight()
if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport {
t.Errorf("cursor at %d not visible in scroll range [%d, %d)",
win.Cursor.Line, win.ScrollY, win.ScrollY+viewport)
}
})
t.Run("scrolls up when cursor goes above viewport", func(t *testing.T) {
lines := make([]string, 100)
for i := range lines {
lines[i] = "line"
}
buf := NewBufferBuilder().WithLines(lines).Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithHeight(24).
Build()
// Start at bottom
win.SetCursorLine(80)
win.AdjustScroll()
initialScroll := win.ScrollY
// Move cursor to top
win.SetCursorLine(5)
win.AdjustScroll()
// Scroll should have decreased
if win.ScrollY >= initialScroll {
t.Errorf("expected scroll to decrease, was %d, now %d", initialScroll, win.ScrollY)
}
// Cursor should be visible
viewport := win.ViewportHeight()
if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport {
t.Errorf("cursor at %d not visible in scroll range [%d, %d)",
win.Cursor.Line, win.ScrollY, win.ScrollY+viewport)
}
})
t.Run("respects scrolloff margin", func(t *testing.T) {
lines := make([]string, 100)
for i := range lines {
lines[i] = "line"
}
buf := NewBufferBuilder().WithLines(lines).Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithHeight(24).
Build()
// Set scrolloff to 5
opts := win.Options
opts.ScrollOff = 5
win.SetOptions(opts)
// Move to line 20 and adjust
win.SetCursorLine(20)
win.AdjustScroll()
viewport := win.ViewportHeight()
distFromTop := win.Cursor.Line - win.ScrollY
distFromBottom := (win.ScrollY + viewport - 1) - win.Cursor.Line
// At least one should respect scrolloff
if distFromTop < opts.ScrollOff && distFromBottom < opts.ScrollOff {
t.Errorf("scrolloff %d not respected: top=%d, bottom=%d",
opts.ScrollOff, distFromTop, distFromBottom)
}
})
t.Run("handles scrolloff larger than half viewport", func(t *testing.T) {
lines := make([]string, 100)
for i := range lines {
lines[i] = "line"
}
buf := NewBufferBuilder().WithLines(lines).Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithHeight(24).
Build()
// Set scrolloff larger than half viewport
opts := win.Options
opts.ScrollOff = 999
win.SetOptions(opts)
win.SetCursorLine(50)
win.AdjustScroll()
// Should not panic or error
viewport := win.ViewportHeight()
if win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport {
t.Error("cursor should still be visible with large scrolloff")
}
})
t.Run("handles small viewport", func(t *testing.T) {
lines := make([]string, 100)
for i := range lines {
lines[i] = "line"
}
buf := NewBufferBuilder().WithLines(lines).Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithHeight(5). // Very small
Build()
win.SetCursorLine(50)
win.AdjustScroll()
// Should not panic
viewport := win.ViewportHeight()
if viewport > 0 && (win.Cursor.Line < win.ScrollY || win.Cursor.Line >= win.ScrollY+viewport) {
t.Error("cursor should be visible in small viewport")
}
})
}
func TestWindow_ViewportHeight(t *testing.T) {
t.Run("calculates viewport height correctly", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithHeight(24).
Build()
// Height - 2 (status bar + command bar)
expected := 22
if win.ViewportHeight() != expected {
t.Errorf("expected viewport height %d, got %d", expected, win.ViewportHeight())
}
})
t.Run("handles small window", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithHeight(3).
Build()
// 3 - 2 = 1
expected := 1
if win.ViewportHeight() != expected {
t.Errorf("expected viewport height %d, got %d", expected, win.ViewportHeight())
}
})
t.Run("handles zero height", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithHeight(0).
Build()
// With height 0, viewport is 0 - 2 (status + command bars) = -2
// This is an edge case that shouldn't occur in practice, but shouldn't panic
result := win.ViewportHeight()
expected := -2
if result != expected {
t.Errorf("expected viewport height %d for zero height window, got %d", expected, result)
}
})
}
func TestWindow_SetOptions(t *testing.T) {
t.Run("updates options", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
newOpts := WinOptions{
Number: false,
RelativeNumber: false,
GutterSize: 10,
ScrollOff: 3,
}
win.SetOptions(newOpts)
if win.Options.Number != false {
t.Error("expected Number to be false")
}
if win.Options.RelativeNumber != false {
t.Error("expected RelativeNumber to be false")
}
if win.Options.GutterSize != 10 {
t.Errorf("expected GutterSize 10, got %d", win.Options.GutterSize)
}
if win.Options.ScrollOff != 3 {
t.Errorf("expected ScrollOff 3, got %d", win.Options.ScrollOff)
}
})
t.Run("can toggle individual options", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
// Get current options
opts := win.Options
originalNumber := opts.Number
// Toggle number
opts.Number = !opts.Number
win.SetOptions(opts)
if win.Options.Number == originalNumber {
t.Error("Number option should have toggled")
}
})
}
func TestWindow_SetAnchor(t *testing.T) {
t.Run("sets anchor line", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"a", "b", "c"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetAnchorLine(2)
if win.Anchor.Line != 2 {
t.Errorf("expected anchor line 2, got %d", win.Anchor.Line)
}
})
t.Run("sets anchor col", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"hello world"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetAnchorCol(5)
if win.Anchor.Col != 5 {
t.Errorf("expected anchor col 5, got %d", win.Anchor.Col)
}
})
}
func TestWindow_SetDimensions(t *testing.T) {
t.Run("updates width and height", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
win.SetDimensions(100, 50)
if win.Width != 100 {
t.Errorf("expected width 100, got %d", win.Width)
}
if win.Height != 50 {
t.Errorf("expected height 50, got %d", win.Height)
}
})
}
func TestWindowBuilder(t *testing.T) {
t.Run("builds with defaults", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
Build()
// Should have default options
if win.Options.Number != true {
t.Error("expected default Number to be true")
}
if win.Options.RelativeNumber != true {
t.Error("expected default RelativeNumber to be true")
}
if win.Options.ScrollOff != 8 {
t.Errorf("expected default ScrollOff 8, got %d", win.Options.ScrollOff)
}
if win.Options.GutterSize != 5 {
t.Errorf("expected default GutterSize 5, got %d", win.Options.GutterSize)
}
})
t.Run("builds with custom cursor position", func(t *testing.T) {
buf := NewBufferBuilder().
WithLines([]string{"a", "b", "c"}).
Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithCursorPos(2, 0).
Build()
if win.Cursor.Line != 2 {
t.Errorf("expected cursor line 2, got %d", win.Cursor.Line)
}
if win.Cursor.Col != 0 {
t.Errorf("expected cursor col 0, got %d", win.Cursor.Col)
}
})
t.Run("builds with custom dimensions", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win := NewWindowBuilder().
WithBuffer(&buf).
WithDimensions(120, 40).
Build()
if win.Width != 120 {
t.Errorf("expected width 120, got %d", win.Width)
}
if win.Height != 40 {
t.Errorf("expected height 40, got %d", win.Height)
}
})
t.Run("assigns unique IDs", func(t *testing.T) {
buf := NewBufferBuilder().Build()
win1 := NewWindowBuilder().WithBuffer(&buf).Build()
win2 := NewWindowBuilder().WithBuffer(&buf).Build()
if win1.Id == win2.Id {
t.Errorf("expected unique IDs, both were %d", win1.Id)
}
})
}