All checks were successful
Run Test Suite / test (push) Successful in 15s
Also adjusted some of the IO tests for writing and force writing.
1067 lines
28 KiB
Go
1067 lines
28 KiB
Go
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))
|
||
}
|
||
})
|
||
}
|