Gim/internal/program/program_builder_test.go
Hayden Hargreaves d4980c5532
All checks were successful
Run Test Suite / test (push) Successful in 15s
feat: created (tested) program_builder.
Also adjusted some of the IO tests for writing and force writing.
2026-03-10 14:18:20 -07:00

1067 lines
28 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package program
import (
"os"
"path/filepath"
"strings"
"testing"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
// ==================================================
// EmptyProgram Tests
// ==================================================
func TestEmptyProgram(t *testing.T) {
t.Run("creates program with readonly buffer", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
if builder.model == nil {
t.Fatal("model should not be nil")
}
buffers := builder.model.Buffers()
if len(buffers) != 1 {
t.Errorf("expected 1 buffer, got %d", len(buffers))
}
buf := buffers[0]
if !buf.ReadOnly {
t.Error("buffer should be readonly")
}
})
t.Run("creates program with one window", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
windows := builder.model.Windows()
if len(windows) != 1 {
t.Errorf("expected 1 window, got %d", len(windows))
}
})
t.Run("sets active window", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
activeWindow := builder.model.ActiveWindow()
if activeWindow == nil {
t.Fatal("active window should not be nil")
}
})
t.Run("window points to buffer", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
buf := builder.model.Buffers()[0]
win := builder.model.Windows()[0]
if win.Buffer != buf {
t.Error("window buffer should point to the created buffer")
}
})
t.Run("active buffer is accessible", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
buf := builder.model.ActiveBuffer()
if buf == nil {
t.Fatal("active buffer should not be nil")
}
if !buf.ReadOnly {
t.Error("active buffer should be readonly")
}
})
t.Run("buffer has default settings", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
settings := builder.model.Settings()
if settings.TabStop <= 0 {
t.Error("TabStop should be initialized to positive value")
}
})
t.Run("buffer is empty", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 1 {
t.Errorf("expected 1 empty line, got %d lines", buf.LineCount())
}
if buf.Line(0) != "" {
t.Errorf("expected empty line, got %q", buf.Line(0))
}
})
t.Run("multiple calls create independent programs", func(t *testing.T) {
builder1 := NewProgramBuilder().EmptyProgram()
builder2 := NewProgramBuilder().EmptyProgram()
buf1 := builder1.model.ActiveBuffer()
buf2 := builder2.model.ActiveBuffer()
if buf1 == buf2 {
t.Error("different programs should have different buffer instances")
}
if builder1.model == builder2.model {
t.Error("different programs should have different model instances")
}
})
}
// ==================================================
// FileProgram Tests - Nonexistent Files
// ==================================================
func TestFileProgramNonexistent(t *testing.T) {
t.Run("creates buffer for nonexistent file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newfile.txt")
builder := NewProgramBuilder()
builder.FileProgram(filename)
if builder.model == nil {
t.Fatal("model should not be nil")
}
buf := builder.model.ActiveBuffer()
if buf.Filename != filename {
t.Errorf("filename = %q, want %q", buf.Filename, filename)
}
})
t.Run("sets correct filetype for nonexistent file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.go")
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Filetype != ".go" {
t.Errorf("filetype = %q, want %q", buf.Filetype, ".go")
}
})
t.Run("sets buffer type to FileBuffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Type != core.FileBuffer {
t.Errorf("buffer type = %v, want FileBuffer", buf.Type)
}
})
t.Run("buffer is empty for nonexistent file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newfile.txt")
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
// BufferBuilder creates buffers with 1 empty line by default
if buf.LineCount() != 1 {
t.Errorf("expected 1 empty line for new file, got %d", buf.LineCount())
}
if buf.Line(0) != "" {
t.Errorf("expected empty line, got %q", buf.Line(0))
}
})
t.Run("creates window for nonexistent file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newfile.txt")
builder := NewProgramBuilder()
builder.FileProgram(filename)
windows := builder.model.Windows()
if len(windows) != 1 {
t.Errorf("expected 1 window, got %d", len(windows))
}
})
t.Run("window buffer matches created buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newfile.txt")
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.Buffers()[0]
win := builder.model.Windows()[0]
if win.Buffer != buf {
t.Error("window should point to the created buffer")
}
})
t.Run("handles file without extension", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "Makefile")
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Filetype != "" {
t.Errorf("filetype should be empty for extensionless file, got %q", buf.Filetype)
}
})
t.Run("handles complex extensions", func(t *testing.T) {
tmpDir := t.TempDir()
tests := []struct {
filename string
expected string
}{
{"file.tar.gz", ".gz"},
{"file.test.js", ".js"},
{"file.d.ts", ".ts"},
{"archive.tar.bz2", ".bz2"},
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
fullPath := filepath.Join(tmpDir, tt.filename)
builder := NewProgramBuilder()
builder.FileProgram(fullPath)
buf := builder.model.ActiveBuffer()
if buf.Filetype != tt.expected {
t.Errorf("filetype = %q, want %q", buf.Filetype, tt.expected)
}
})
}
})
t.Run("handles filename with spaces", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "my file with spaces.txt")
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Filename != filename {
t.Errorf("filename = %q, want %q", buf.Filename, filename)
}
})
t.Run("handles unicode filename", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "テスト_файл_测试.txt")
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Filename != filename {
t.Errorf("filename = %q, want %q", buf.Filename, filename)
}
})
}
// ==================================================
// FileProgram Tests - Existing Files
// ==================================================
func TestFileProgramExisting(t *testing.T) {
t.Run("loads content from existing file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
content := "line 1\nline 2\nline 3\n"
os.WriteFile(filename, []byte(content), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
if buf.Line(0) != "line 1" {
t.Errorf("line 0 = %q, want %q", buf.Line(0), "line 1")
}
if buf.Line(1) != "line 2" {
t.Errorf("line 1 = %q, want %q", buf.Line(1), "line 2")
}
if buf.Line(2) != "line 3" {
t.Errorf("line 2 = %q, want %q", buf.Line(2), "line 3")
}
})
t.Run("sets filename for existing file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "existing.txt")
os.WriteFile(filename, []byte("content"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Filename != filename {
t.Errorf("filename = %q, want %q", buf.Filename, filename)
}
})
t.Run("sets filetype from extension", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.go")
os.WriteFile(filename, []byte("package main"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Filetype != ".go" {
t.Errorf("filetype = %q, want %q", buf.Filetype, ".go")
}
})
t.Run("loads empty file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "empty.txt")
os.WriteFile(filename, []byte(""), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
// Empty file creates buffer with default 1 empty line since SetLines isn't called
if buf.LineCount() != 1 {
t.Errorf("expected 1 empty line for empty file, got %d", buf.LineCount())
}
if buf.Line(0) != "" {
t.Errorf("expected empty line, got %q", buf.Line(0))
}
})
t.Run("loads file without trailing newline", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "notrailing.txt")
os.WriteFile(filename, []byte("line 1\nline 2"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 2 {
t.Errorf("expected 2 lines, got %d", buf.LineCount())
}
if buf.Line(1) != "line 2" {
t.Errorf("last line = %q, want %q", buf.Line(1), "line 2")
}
})
t.Run("loads file with only newlines", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newlines.txt")
os.WriteFile(filename, []byte("\n\n\n"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
for i := 0; i < 3; i++ {
if buf.Line(i) != "" {
t.Errorf("line %d should be empty, got %q", i, buf.Line(i))
}
}
})
t.Run("strips carriage return from CRLF", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "crlf.txt")
os.WriteFile(filename, []byte("line 1\r\nline 2\r\nline 3\r\n"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
for i := 0; i < buf.LineCount(); i++ {
line := buf.Line(i)
if strings.Contains(line, "\r") {
t.Errorf("line %d contains \\r: %q", i, line)
}
}
if buf.Line(0) != "line 1" {
t.Errorf("line 0 = %q, want %q", buf.Line(0), "line 1")
}
})
t.Run("handles mixed line endings", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "mixed.txt")
os.WriteFile(filename, []byte("unix\nwindows\r\nunix again\n"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
for i := 0; i < buf.LineCount(); i++ {
if strings.Contains(buf.Line(i), "\r") {
t.Errorf("line %d contains \\r: %q", i, buf.Line(i))
}
}
})
t.Run("converts tabs to spaces based on tabstop", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "tabs.txt")
os.WriteFile(filename, []byte("\tindented\n\t\tdouble"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
settings := builder.model.Settings()
// Default tabstop should be used
expectedSpaces := strings.Repeat(" ", settings.TabStop)
expected0 := expectedSpaces + "indented"
expected1 := expectedSpaces + expectedSpaces + "double"
if buf.Line(0) != expected0 {
t.Errorf("line 0 = %q, want %q", buf.Line(0), expected0)
}
if buf.Line(1) != expected1 {
t.Errorf("line 1 = %q, want %q", buf.Line(1), expected1)
}
})
t.Run("loads file with unicode content", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "unicode.txt")
content := "Hello 世界\nこんにちは\nEmoji: 😀\n"
os.WriteFile(filename, []byte(content), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
if !strings.Contains(buf.Line(0), "世界") {
t.Errorf("unicode not preserved in line 0: %q", buf.Line(0))
}
if !strings.Contains(buf.Line(1), "こんにちは") {
t.Errorf("unicode not preserved in line 1: %q", buf.Line(1))
}
if !strings.Contains(buf.Line(2), "😀") {
t.Errorf("emoji not preserved in line 2: %q", buf.Line(2))
}
})
t.Run("loads file with special characters", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "special.txt")
content := "line with\ttab\nsymbols: !@#$%^&*()\n"
os.WriteFile(filename, []byte(content), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 2 {
t.Errorf("expected 2 lines, got %d", buf.LineCount())
}
// Tab should be converted to spaces
if !strings.Contains(buf.Line(0), "tab") {
t.Errorf("content not preserved in line 0: %q", buf.Line(0))
}
if buf.Line(1) != "symbols: !@#$%^&*()" {
t.Errorf("line 1 = %q, want 'symbols: !@#$%%^&*()'", buf.Line(1))
}
})
t.Run("loads large file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "large.txt")
// Create file with many lines
var content strings.Builder
lineCount := 10000
for i := 0; i < lineCount; i++ {
content.WriteString(strings.Repeat("x", 80))
content.WriteString("\n")
}
os.WriteFile(filename, []byte(content.String()), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != lineCount {
t.Errorf("expected %d lines, got %d", lineCount, buf.LineCount())
}
// Verify first and last lines
if len(buf.Line(0)) != 80 {
t.Errorf("first line length = %d, want 80", len(buf.Line(0)))
}
if len(buf.Line(lineCount-1)) != 80 {
t.Errorf("last line length = %d, want 80", len(buf.Line(lineCount-1)))
}
})
t.Run("handles file with long lines", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "longlines.txt")
longLine := strings.Repeat("x", 10000)
content := longLine + "\nshort\n" + longLine
os.WriteFile(filename, []byte(content), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
if len(buf.Line(0)) != 10000 {
t.Errorf("first long line length = %d, want 10000", len(buf.Line(0)))
}
if buf.Line(1) != "short" {
t.Errorf("middle line = %q, want 'short'", buf.Line(1))
}
})
t.Run("buffer is not readonly for file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
os.WriteFile(filename, []byte("content"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.ReadOnly {
t.Error("file buffer should not be readonly by default")
}
})
t.Run("buffer is not modified initially", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
os.WriteFile(filename, []byte("content"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Modified {
t.Error("buffer should not be modified after loading")
}
})
t.Run("creates single window", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
os.WriteFile(filename, []byte("content"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
windows := builder.model.Windows()
if len(windows) != 1 {
t.Errorf("expected 1 window, got %d", len(windows))
}
})
t.Run("creates single buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
os.WriteFile(filename, []byte("content"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buffers := builder.model.Buffers()
if len(buffers) != 1 {
t.Errorf("expected 1 buffer, got %d", len(buffers))
}
})
}
// ==================================================
// FileProgram Tests - Error Handling
// ==================================================
func TestFileProgramErrors(t *testing.T) {
t.Run("panics on permission denied", func(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("test requires non-root user")
}
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "noperm.txt")
// Create file then remove read permission
os.WriteFile(filename, []byte("content"), 0644)
os.Chmod(filename, 0000)
defer os.Chmod(filename, 0644) // Cleanup
builder := NewProgramBuilder()
// Should panic due to permission error
defer func() {
if r := recover(); r == nil {
t.Error("expected panic for permission denied")
}
}()
builder.FileProgram(filename)
})
t.Run("handles file in nonexistent directory gracefully", func(t *testing.T) {
// Nonexistent directory - file doesn't exist
filename := "/nonexistent/directory/file.txt"
builder := NewProgramBuilder()
builder.FileProgram(filename)
// Should not panic, creates buffer for new file
buf := builder.model.ActiveBuffer()
if buf.Filename != filename {
t.Errorf("filename = %q, want %q", buf.Filename, filename)
}
// Buffer should have 1 empty line (default for new buffers)
if buf.LineCount() != 1 {
t.Errorf("expected 1 empty line for nonexistent file, got %d", buf.LineCount())
}
if buf.Line(0) != "" {
t.Errorf("expected empty line, got %q", buf.Line(0))
}
})
}
// ==================================================
// Builder Chain and Build Tests
// ==================================================
func TestProgramBuilderChain(t *testing.T) {
t.Run("EmptyProgram returns builder for chaining", func(t *testing.T) {
builder := NewProgramBuilder()
result := builder.EmptyProgram()
if result != builder {
t.Error("EmptyProgram should return the builder for chaining")
}
})
t.Run("FileProgram returns builder for chaining", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
builder := NewProgramBuilder()
result := builder.FileProgram(filename)
if result != builder {
t.Error("FileProgram should return the builder for chaining")
}
})
t.Run("WithOpt adds program option", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
// Add a program option
opt := tea.WithAltScreen()
builder.WithOpt(opt)
if len(builder.opts) != 1 {
t.Errorf("expected 1 option, got %d", len(builder.opts))
}
})
t.Run("WithOpt returns builder for chaining", func(t *testing.T) {
builder := NewProgramBuilder()
result := builder.WithOpt(tea.WithAltScreen())
if result != builder {
t.Error("WithOpt should return the builder for chaining")
}
})
t.Run("multiple WithOpt calls accumulate options", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
builder.WithOpt(tea.WithAltScreen())
builder.WithOpt(tea.WithMouseCellMotion())
if len(builder.opts) != 2 {
t.Errorf("expected 2 options, got %d", len(builder.opts))
}
})
t.Run("chained calls work together", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
os.WriteFile(filename, []byte("content"), 0644)
prog := NewProgramBuilder().
FileProgram(filename).
WithOpt(tea.WithAltScreen()).
Build()
if prog == nil {
t.Fatal("Build should return a program")
}
})
t.Run("Build returns tea.Program", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
prog := builder.Build()
if prog == nil {
t.Fatal("Build should return a program, got nil")
}
// Verify it's actually a tea.Program
if _, ok := interface{}(prog).(*tea.Program); !ok {
t.Errorf("Build should return *tea.Program, got %T", prog)
}
})
}
// ==================================================
// Model State Tests
// ==================================================
func TestModelState(t *testing.T) {
t.Run("model has default settings", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
settings := builder.model.Settings()
if settings.TabStop == 0 {
t.Error("TabStop should be initialized")
}
})
t.Run("model has default registers", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
registers := builder.model.Registers()
if registers == nil {
t.Error("Registers should be initialized")
}
// Check for default register
if _, exists := registers['"']; !exists {
t.Error("default register should exist")
}
})
t.Run("model starts in normal mode", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
mode := builder.model.Mode()
if mode != core.NormalMode {
t.Errorf("mode = %v, want NormalMode", mode)
}
})
t.Run("buffers list matches window buffer", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.txt")
os.WriteFile(filename, []byte("content"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buffers := builder.model.Buffers()
activeBuffer := builder.model.ActiveBuffer()
if len(buffers) != 1 {
t.Fatalf("expected 1 buffer in list, got %d", len(buffers))
}
if buffers[0] != activeBuffer {
t.Error("buffer in list should match active buffer")
}
})
t.Run("active window points to active buffer", func(t *testing.T) {
builder := NewProgramBuilder()
builder.EmptyProgram()
activeWindow := builder.model.ActiveWindow()
activeBuffer := builder.model.ActiveBuffer()
if activeWindow.Buffer != activeBuffer {
t.Error("active window buffer should match active buffer")
}
})
}
// ==================================================
// Integration Tests
// ==================================================
func TestIntegration(t *testing.T) {
t.Run("complete workflow - create empty program", func(t *testing.T) {
prog := NewProgramBuilder().
EmptyProgram().
WithOpt(tea.WithAltScreen()).
Build()
if prog == nil {
t.Fatal("program should not be nil")
}
})
t.Run("complete workflow - load existing file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "test.go")
content := "package main\n\nfunc main() {\n}\n"
os.WriteFile(filename, []byte(content), 0644)
builder := NewProgramBuilder().
FileProgram(filename).
WithOpt(tea.WithAltScreen())
// Verify buffer before building
buf := builder.model.ActiveBuffer()
if buf.Filename != filename {
t.Errorf("filename = %q, want %q", buf.Filename, filename)
}
if buf.Filetype != ".go" {
t.Errorf("filetype = %q, want .go", buf.Filetype)
}
if buf.LineCount() != 4 {
t.Errorf("expected 4 lines, got %d", buf.LineCount())
}
// Build program
prog := builder.Build()
if prog == nil {
t.Fatal("program should not be nil")
}
})
t.Run("complete workflow - create new file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "newfile.js")
builder := NewProgramBuilder().
FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Filename != filename {
t.Errorf("filename = %q, want %q", buf.Filename, filename)
}
if buf.Filetype != ".js" {
t.Errorf("filetype = %q, want .js", buf.Filetype)
}
// New buffer has 1 empty line by default
if buf.LineCount() != 1 {
t.Errorf("expected 1 empty line for new file, got %d", buf.LineCount())
}
prog := builder.Build()
if prog == nil {
t.Fatal("program should not be nil")
}
})
t.Run("multiple file loads are independent", func(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "file1.txt")
file2 := filepath.Join(tmpDir, "file2.txt")
os.WriteFile(file1, []byte("content 1"), 0644)
os.WriteFile(file2, []byte("content 2"), 0644)
builder1 := NewProgramBuilder().FileProgram(file1)
builder2 := NewProgramBuilder().FileProgram(file2)
buf1 := builder1.model.ActiveBuffer()
buf2 := builder2.model.ActiveBuffer()
if buf1 == buf2 {
t.Error("different files should have different buffers")
}
if buf1.Filename == buf2.Filename {
t.Error("buffers should have different filenames")
}
if buf1.Line(0) == buf2.Line(0) {
t.Error("buffers should have different content")
}
})
}
// ==================================================
// Edge Cases
// ==================================================
func TestEdgeCases(t *testing.T) {
t.Run("empty string filename", func(t *testing.T) {
builder := NewProgramBuilder()
builder.FileProgram("")
buf := builder.model.ActiveBuffer()
if buf.Filename != "" {
t.Errorf("filename should be empty string, got %q", buf.Filename)
}
// Should treat as new file with no name - has default 1 empty line
if buf.LineCount() != 1 {
t.Errorf("expected 1 empty line, got %d", buf.LineCount())
}
if buf.Line(0) != "" {
t.Errorf("expected empty line, got %q", buf.Line(0))
}
})
t.Run("relative path", func(t *testing.T) {
// Create file in temp dir but use relative path
tmpDir := t.TempDir()
absPath := filepath.Join(tmpDir, "test.txt")
os.WriteFile(absPath, []byte("content"), 0644)
// Change to temp dir
oldWd, _ := os.Getwd()
defer os.Chdir(oldWd)
os.Chdir(tmpDir)
builder := NewProgramBuilder()
builder.FileProgram("test.txt")
buf := builder.model.ActiveBuffer()
if buf.Filename != "test.txt" {
t.Errorf("filename = %q, want 'test.txt'", buf.Filename)
}
if buf.LineCount() != 1 {
t.Errorf("expected 1 line, got %d", buf.LineCount())
}
})
t.Run("file with only spaces", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "spaces.txt")
os.WriteFile(filename, []byte(" \n \n "), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 3 {
t.Errorf("expected 3 lines, got %d", buf.LineCount())
}
// Spaces should be preserved
if buf.Line(0) != " " {
t.Errorf("line 0 = %q, want ' '", buf.Line(0))
}
})
t.Run("file ending with multiple newlines", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "trailing.txt")
os.WriteFile(filename, []byte("line 1\nline 2\n\n\n"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.LineCount() != 4 {
t.Errorf("expected 4 lines, got %d", buf.LineCount())
}
if buf.Line(0) != "line 1" {
t.Errorf("line 0 = %q", buf.Line(0))
}
// Last lines should be empty
if buf.Line(2) != "" || buf.Line(3) != "" {
t.Error("trailing newlines should create empty lines")
}
})
t.Run("dot file", func(t *testing.T) {
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, ".hidden")
os.WriteFile(filename, []byte("secret"), 0644)
builder := NewProgramBuilder()
builder.FileProgram(filename)
buf := builder.model.ActiveBuffer()
if buf.Filename != filename {
t.Errorf("filename = %q, want %q", buf.Filename, filename)
}
if buf.LineCount() != 1 {
t.Errorf("expected 1 line, got %d", buf.LineCount())
}
if buf.Line(0) != "secret" {
t.Errorf("content = %q, want 'secret'", buf.Line(0))
}
})
}