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)) } }) }