Pad/internal/browser/browser_test.go
Greg Pomerantz 6a43e3c0db Add e2e test harness for testing editor logic without Gio display
- internal/test/e2e/: FrameCapture, Harness, ElementAssertions, helpers
- internal/editor/logic.go: Add done channel and Done() method for graceful shutdown
- internal/editor/mock_setup.go: Mock filesystem for tests
- internal/browser/: Browser layout and search logic
- internal/io/: Worker pool for async tasks
- Update architecture docs and spec
2026-05-31 08:56:30 -04:00

430 lines
11 KiB
Go

package browser
import (
"fmt"
"testing"
"time"
)
// --- Test: Load Page From Index At Offset ---
func TestLoadPage_FromIndexAtOffset(t *testing.T) {
// Create a BrowserState with a populated index
s := NewBrowserState()
s.TotalEntries = 250
// Simulate an index with 250 entries (3 pages)
// Use zero-padded names so lexicographic order matches numeric order
allEntries := make([]Entry, 250)
for i := 0; i < 250; i++ {
allEntries[i] = NewEntry(
fmt.Sprintf("/path/file%03d.txt", i),
fmt.Sprintf("file%03d.txt", i),
int64(i*100),
time.Time{},
false,
)
}
// Build position map for the default sort mode (NameAsc)
s.SortIndex = &DirectoryIndex{
Entries: allEntries,
SortOrders: map[string][]int{"name_asc": buildPositionMap(allEntries, SortModeNameAsc)},
}
// Load page 1 (entries 100-199)
page := loadPageFromIndex(s, 1)
if page == nil {
t.Fatal("expected page to be loaded, got nil")
}
if page.Index != 1 {
t.Errorf("expected Index=1, got %d", page.Index)
}
if len(page.Entries) != PageSize {
t.Errorf("expected %d entries, got %d", PageSize, len(page.Entries))
}
// First entry of page 1 should be file100.txt
if page.Entries[0].Name != "file100.txt" {
t.Errorf("expected first entry name=file100.txt, got %s", page.Entries[0].Name)
}
// Last entry of page 1 should be file199.txt
if page.Entries[99].Name != "file199.txt" {
t.Errorf("expected last entry name=file199.txt, got %s", page.Entries[99].Name)
}
}
// --- Test: Load Page - Last Page Partial ---
func TestLoadPage_LastPagePartialFromIndex(t *testing.T) {
s := NewBrowserState()
s.TotalEntries = 150 // 1 full page + 50 entries in page 1
allEntries := make([]Entry, 150)
for i := 0; i < 150; i++ {
allEntries[i] = NewEntry(
fmt.Sprintf("/path/file%d.txt", i),
fmt.Sprintf("file%d.txt", i),
int64(i*100),
time.Time{},
false,
)
}
// Build position map for the default sort mode (NameAsc)
s.SortIndex = &DirectoryIndex{
Entries: allEntries,
SortOrders: map[string][]int{"name_asc": buildPositionMap(allEntries, SortModeNameAsc)},
}
// Load page 1 (last page, should have only 50 entries)
page := loadPageFromIndex(s, 1)
if page == nil {
t.Fatal("expected page to be loaded, got nil")
}
if page.Index != 1 {
t.Errorf("expected Index=1, got %d", page.Index)
}
if len(page.Entries) != 50 {
t.Errorf("expected 50 entries, got %d", len(page.Entries))
}
}
// --- Test: Load Page - Out Of Bounds ---
func TestLoadPage_OutOfBoundsFromIndex(t *testing.T) {
s := NewBrowserState()
s.TotalEntries = 150
allEntries := make([]Entry, 150)
for i := 0; i < 150; i++ {
allEntries[i] = NewEntry(
fmt.Sprintf("/path/file%d.txt", i),
fmt.Sprintf("file%d.txt", i),
int64(i*100),
time.Time{},
false,
)
}
// Build position map for the default sort mode (NameAsc)
s.SortIndex = &DirectoryIndex{
Entries: allEntries,
SortOrders: map[string][]int{"name_asc": buildPositionMap(allEntries, SortModeNameAsc)},
}
// Requesting page 5 (out of bounds) should return nil
page := loadPageFromIndex(s, 5)
if page != nil {
t.Errorf("expected nil for out-of-bounds page, got page with %d entries", len(page.Entries))
}
}
// --- Test: Compute Visible Page Range ---
func TestComputeVisiblePageRange(t *testing.T) {
s := NewBrowserState()
s.ScrollIndex = 250
s.VisibleCount = 20
minPage, maxPage := computeVisiblePageRange(s)
// ScrollIndex=250 → page 2
// VisibleCount=20 → entries 250-269 → pages 2-2 (partial)
// With prefetch: min=0, max=4
expectedMin := 0 // (250/100) - 2 = 0
expectedMax := 4 // (270/100) + 2 = 4
if minPage != expectedMin {
t.Errorf("expected minPage=%d, got %d", expectedMin, minPage)
}
if maxPage != expectedMax {
t.Errorf("expected maxPage=%d, got %d", expectedMax, maxPage)
}
}
// --- Test: Compute Visible Page Range - At Start ---
func TestComputeVisiblePageRange_AtStart(t *testing.T) {
s := NewBrowserState()
s.ScrollIndex = 0
s.VisibleCount = 10
minPage, maxPage := computeVisiblePageRange(s)
// Should clamp min to 0
if minPage != 0 {
t.Errorf("expected minPage=0, got %d", minPage)
}
// maxPage should be 0 + 2 = 2
if maxPage != 2 {
t.Errorf("expected maxPage=2, got %d", maxPage)
}
}
// --- Test: Get Entry By Index ---
func TestGetEntryByIndex(t *testing.T) {
s := NewBrowserState()
s.TotalEntries = 250
// Use zero-padded names so lexicographic order matches numeric order
allEntries := make([]Entry, 250)
for i := 0; i < 250; i++ {
allEntries[i] = NewEntry(
fmt.Sprintf("/path/file%03d.txt", i),
fmt.Sprintf("file%03d.txt", i),
int64(i*100),
time.Time{},
false,
)
}
// Build position map for the default sort mode (NameAsc)
s.SortIndex = &DirectoryIndex{
Entries: allEntries,
SortOrders: map[string][]int{"name_asc": buildPositionMap(allEntries, SortModeNameAsc)},
}
// Load some pages
s.Pages[0] = loadPageFromIndex(s, 0)
s.Pages[1] = loadPageFromIndex(s, 1)
// Get entry at index 150 (page 1, offset 50)
entry, ok := getEntryByIndex(s, 150)
if !ok {
t.Fatal("expected entry to be found")
}
if entry.Name != "file150.txt" {
t.Errorf("expected entry name=file150.txt, got %s", entry.Name)
}
}
// --- Test: Get Entry By Index - Unloaded Page ---
func TestGetEntryByIndex_UnloadedPage(t *testing.T) {
s := NewBrowserState()
s.TotalEntries = 250
allEntries := make([]Entry, 250)
for i := 0; i < 250; i++ {
allEntries[i] = NewEntry(
fmt.Sprintf("/path/file%d.txt", i),
fmt.Sprintf("file%d.txt", i),
int64(i*100),
time.Time{},
false,
)
}
// Build position map for the default sort mode (NameAsc)
s.SortIndex = &DirectoryIndex{
Entries: allEntries,
SortOrders: map[string][]int{"name_asc": buildPositionMap(allEntries, SortModeNameAsc)},
}
// Only load page 0
s.Pages[0] = loadPageFromIndex(s, 0)
// Try to get entry at index 150 (page 1, not loaded)
_, ok := getEntryByIndex(s, 150)
if ok {
t.Error("expected entry not to be found for unloaded page")
}
}
// --- Test: Get Entry By Index - Out Of Bounds ---
func TestGetEntryByIndex_OutOfBounds(t *testing.T) {
s := NewBrowserState()
s.TotalEntries = 100
_, ok := getEntryByIndex(s, 200)
if ok {
t.Error("expected entry not to be found for out-of-bounds index")
}
}
// --- Test: Needs Prefetch ---
func TestNeedsPrefetch(t *testing.T) {
s := NewBrowserState()
s.ScrollIndex = 500
s.VisibleCount = 20
s.TotalEntries = 2000
// Page 3 is within visible range + prefetch
needs, _ := needsPrefetch(s, 3)
if !needs {
t.Error("expected page 3 to need prefetch")
}
// Page 0 is far away, should not need prefetch
needs, _ = needsPrefetch(s, 0)
if needs {
t.Error("expected page 0 to not need prefetch")
}
// Page 25 is far away, should not need prefetch
needs, _ = needsPrefetch(s, 25)
if needs {
t.Error("expected page 25 to not need prefetch")
}
}
// --- Test: Navigate To Directory ---
func TestNavigateToDirectory(t *testing.T) {
s := NewBrowserState()
s.CurrentPath = "/old/path"
s.ScrollIndex = 100
s.SelectedIndex = 5
s.Pages[0] = NewPage(0, nil)
navigateToDirectory(s, "/new/path")
if s.CurrentPath != "/new/path" {
t.Errorf("expected CurrentPath=/new/path, got %s", s.CurrentPath)
}
if s.ScrollIndex != 0 {
t.Errorf("expected ScrollIndex=0, got %d", s.ScrollIndex)
}
if len(s.Pages) != 0 {
t.Errorf("expected Pages to be empty, got %d", len(s.Pages))
}
}
// --- Test: Compute Total Pages ---
func TestComputeTotalPages(t *testing.T) {
s := NewBrowserState()
s.TotalEntries = 0
if pages := computeTotalPages(s); pages != 0 {
t.Errorf("expected 0 pages for 0 entries, got %d", pages)
}
s.TotalEntries = 100
if pages := computeTotalPages(s); pages != 1 {
t.Errorf("expected 1 page for 100 entries, got %d", pages)
}
s.TotalEntries = 101
if pages := computeTotalPages(s); pages != 2 {
t.Errorf("expected 2 pages for 101 entries, got %d", pages)
}
s.TotalEntries = 250
if pages := computeTotalPages(s); pages != 3 {
t.Errorf("expected 3 pages for 250 entries, got %d", pages)
}
}
// --- Test: Clamp Scroll Index ---
func TestClampScrollIndex(t *testing.T) {
s := NewBrowserState()
s.TotalEntries = 500
s.VisibleCount = 20
// Normal case
s.ScrollIndex = 100
clampScrollIndex(s)
if s.ScrollIndex != 100 {
t.Errorf("expected ScrollIndex=100, got %d", s.ScrollIndex)
}
// Below minimum
s.ScrollIndex = -10
clampScrollIndex(s)
if s.ScrollIndex != 0 {
t.Errorf("expected ScrollIndex=0, got %d", s.ScrollIndex)
}
// Above maximum
s.ScrollIndex = 1000
clampScrollIndex(s)
if s.ScrollIndex != 480 {
t.Errorf("expected ScrollIndex=480, got %d", s.ScrollIndex)
}
}
// --- Test: Filter Entries By Query ---
func TestFilterEntriesByQuery(t *testing.T) {
entries := []Entry{
NewEntry("/path/file0.txt", "file0.txt", 0, time.Time{}, false),
NewEntry("/path/file1.txt", "file1.txt", 100, time.Time{}, false),
NewEntry("/path/file2.txt", "file2.txt", 200, time.Time{}, false),
NewEntry("/path/file10.txt", "file10.txt", 1000, time.Time{}, false),
NewEntry("/path/file100.txt", "file100.txt", 10000, time.Time{}, false),
}
// Search for "file1" - should match file1.txt, file10.txt, file100.txt
results := filterEntriesByQuery(entries, "file1")
if len(results) != 3 {
t.Errorf("expected 3 results for 'file1', got %d", len(results))
}
expectedNames := map[string]bool{"file1.txt": true, "file10.txt": true, "file100.txt": true}
for _, idx := range results {
if !expectedNames[entries[idx].Name] {
t.Errorf("unexpected result: %s", entries[idx].Name)
}
}
}
// --- Test: Filter Entries By Query - Empty Query ---
func TestFilterEntriesByQuery_EmptyQuery(t *testing.T) {
entries := []Entry{
NewEntry("/a", "a.txt", 100, time.Time{}, false),
NewEntry("/b", "b.txt", 200, time.Time{}, false),
}
results := filterEntriesByQuery(entries, "")
if len(results) != 0 {
t.Errorf("expected 0 results for empty query, got %d", len(results))
}
}
// --- Test: Filter Entries By Query - No Match ---
func TestFilterEntriesByQuery_NoMatch(t *testing.T) {
entries := []Entry{
NewEntry("/a", "a.txt", 100, time.Time{}, false),
NewEntry("/b", "b.txt", 200, time.Time{}, false),
}
results := filterEntriesByQuery(entries, "zzz")
if len(results) != 0 {
t.Errorf("expected 0 results for 'zzz', got %d", len(results))
}
}
// --- Test: Filter Entries By Query - Case Insensitive ---
func TestFilterEntriesByQuery_CaseInsensitive(t *testing.T) {
entries := []Entry{
NewEntry("/a", "Apple.txt", 100, time.Time{}, false),
NewEntry("/b", "banana.txt", 200, time.Time{}, false),
}
results := filterEntriesByQuery(entries, "apple")
if len(results) != 1 {
t.Errorf("expected 1 result for 'apple', got %d", len(results))
}
if results[0] != 0 {
t.Errorf("expected result at index 0, got %d", results[0])
}
}