Pad/internal/browser/search_test.go
Greg Pomerantz 06b1444207 gofmt: format all remaining files with the go1.27 toolchain
The tree was formatted with an older gofmt; go1.27's gofmt additionally
wants: EOF exactly one newline (no trailing blank lines), imports sorted
alphabetically within a block, mixed-precedence binary expressions
re-spaced for grouping ((a+b)/c), single-field composite literals
un-aligned, adjacent one-line method signatures aligned, and one-line
bodies containing a compound statement expanded. Applied repo-wide
(31 files under internal/); pure formatting, no semantic changes —
build and the full test suite pass.
2026-08-23 10:03:27 -04:00

434 lines
12 KiB
Go

package browser
import (
"fmt"
"testing"
"time"
)
// --- Helper: create a browser state with named entries for search testing ---
func makeSearchState(t *testing.T, entries []Entry) *BrowserState {
t.Helper()
s := NewBrowserState()
s.TotalEntries = len(entries)
s.VisibleCount = 20
s.CurrentPath = "/test"
s.EntryHeight = 48.0
// Distribute entries across pages
for pageIdx := 0; pageIdx <= (len(entries)-1)/PageSize; pageIdx++ {
start := pageIdx * PageSize
end := start + PageSize
if end > len(entries) {
end = len(entries)
}
s.Pages[pageIdx] = NewPage(pageIdx, entries[start:end])
}
return s
}
// --- Test: Search - Empty Query ---
func TestSearch_EmptyQuery(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "alpha.txt", 100, time.Time{}, false),
NewEntry("/b", "beta.txt", 200, time.Time{}, false),
NewEntry("/c", "gamma.txt", 300, time.Time{}, false),
})
// First, set a non-empty query and results to simulate prior search
s.Query = "alpha"
s.SearchResults = []int{0}
// Clear the search
HandleSearch(s, "")
if s.Query != "" {
t.Errorf("expected Query to be empty, got %q", s.Query)
}
if s.SearchResults != nil {
t.Errorf("expected SearchResults to be nil after empty query, got %v", s.SearchResults)
}
// Scroll should not change when clearing search
if s.GetScrollIndex() != 0 {
t.Errorf("expected ScrollIndex=0, got %d", s.GetScrollIndex())
}
}
// --- Test: Search - Case Insensitive ---
func TestSearch_CaseInsensitive(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "Apple.txt", 100, time.Time{}, false),
NewEntry("/b", "banana.txt", 200, time.Time{}, false),
NewEntry("/c", "CHERRY.txt", 300, time.Time{}, false),
NewEntry("/d", "date.txt", 400, time.Time{}, false),
})
HandleSearch(s, "apple")
if len(s.SearchResults) != 1 {
t.Errorf("expected 1 result for 'apple', got %d: %v", len(s.SearchResults), s.SearchResults)
}
if len(s.SearchResults) > 0 && s.SearchResults[0] != 0 {
t.Errorf("expected first result at index 0 (Apple.txt), got %d", s.SearchResults[0])
}
// Uppercase query should also match lowercase entries
HandleSearch(s, "BANANA")
if len(s.SearchResults) != 1 {
t.Errorf("expected 1 result for 'BANANA', got %d: %v", len(s.SearchResults), s.SearchResults)
}
if len(s.SearchResults) > 0 && s.SearchResults[0] != 1 {
t.Errorf("expected first result at index 1 (banana.txt), got %d", s.SearchResults[0])
}
// Mixed case
HandleSearch(s, "ChErRy")
if len(s.SearchResults) != 1 {
t.Errorf("expected 1 result for 'ChErRy', got %d: %v", len(s.SearchResults), s.SearchResults)
}
}
// --- Test: Search - Partial Match ---
func TestSearch_PartialMatch(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "abc.dat", 100, time.Time{}, false),
NewEntry("/b", "xabc.dat", 200, time.Time{}, false),
NewEntry("/c", "abcdef.dat", 300, time.Time{}, false),
NewEntry("/d", "xyz.dat", 400, time.Time{}, false),
NewEntry("/e", "bca.dat", 500, time.Time{}, false),
})
HandleSearch(s, "abc")
// "abc" should match: abc.dat (0), xabc.dat (1), abcdef.dat (2)
// Should NOT match: xyz.dat (3), bca.dat (4)
if len(s.SearchResults) != 3 {
t.Errorf("expected 3 results for 'abc', got %d: %v", len(s.SearchResults), s.SearchResults)
}
expectedIndices := map[int]bool{0: true, 1: true, 2: true}
for _, idx := range s.SearchResults {
if !expectedIndices[idx] {
t.Errorf("unexpected result at index %d", idx)
}
}
// Test single character partial match
HandleSearch(s, "x")
// "x" should match: xabc.dat (1), xyz.dat (3)
// Should NOT match: abc.dat (0), abcdef.dat (2), bca.dat (4)
expectedIndices = map[int]bool{1: true, 3: true}
if len(s.SearchResults) != len(expectedIndices) {
t.Errorf("expected %d results for 'x', got %d: %v", len(expectedIndices), len(s.SearchResults), s.SearchResults)
}
}
// --- Test: Search - No Match ---
func TestSearch_NoMatch(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "alpha.txt", 100, time.Time{}, false),
NewEntry("/b", "beta.txt", 200, time.Time{}, false),
NewEntry("/c", "gamma.txt", 300, time.Time{}, false),
})
HandleSearch(s, "zzz")
if len(s.SearchResults) != 0 {
t.Errorf("expected 0 results for 'zzz', got %d: %v", len(s.SearchResults), s.SearchResults)
}
// Scroll should not change when there are no results
if s.GetScrollIndex() != 0 {
t.Errorf("expected ScrollIndex=0 when no match, got %d", s.GetScrollIndex())
}
}
// --- Test: Search - Jump To First ---
func TestSearch_JumpToFirst(t *testing.T) {
// Create enough entries to make results scrollable
entries := make([]Entry, 100)
for i := 0; i < 100; i++ {
name := fmt.Sprintf("file_%d", i)
entries[i] = NewEntry(fmt.Sprintf("/%d", i), name, 100, time.Time{}, false)
}
s := makeSearchState(t, entries)
// Start with scroll at position 0
s.ScrollOffset = 0
// Search for "file_3" — should find 12 results (3, 30-39)
HandleSearch(s, "file_3")
if len(s.SearchResults) == 0 {
t.Fatalf("expected results for 'file_3', got 0")
}
// The first result should be index 3 (file_3).
// With 12 results, it fits in 20 visible. ScrollIndex should be 0.
if s.GetScrollIndex() != 0 {
t.Errorf("expected ScrollIndex=0 (clamped), got %d", s.GetScrollIndex())
}
}
// --- Test: Search - Large Directory ---
func TestSearch_LargeDirectory(t *testing.T) {
// Create 10k entries for performance testing
const count = 10000
entries := make([]Entry, count)
for i := 0; i < count; i++ {
entries[i] = NewEntry(
fmt.Sprintf("/test/file_%05d.txt", i),
fmt.Sprintf("file_%05d.txt", i),
int64(i*100),
time.Time{},
false,
)
}
// Sprinkle some entries with "special" in the name
for i := 0; i < 50; i++ {
idx := i * 200 // spread across the dataset
entries[idx] = NewEntry(
fmt.Sprintf("/test/special_%03d.txt", i),
fmt.Sprintf("special_%03d.txt", i),
int64(i*100),
time.Time{},
false,
)
}
s := makeSearchState(t, entries)
start := time.Now()
HandleSearch(s, "special")
elapsed := time.Since(start)
// Should find all 50 "special" entries
if len(s.SearchResults) != 50 {
t.Errorf("expected 50 results for 'special', got %d", len(s.SearchResults))
}
// Time budget: 100ms for searching 10k entries
if elapsed > 100*time.Millisecond {
t.Errorf("search took %v, expected < 100ms", elapsed)
}
}
// --- Test: Search - Across Page Boundaries ---
func TestSearch_AcrossPageBoundaries(t *testing.T) {
// Create entries spanning multiple pages
entries := make([]Entry, 300)
for i := 0; i < 300; i++ {
name := fmt.Sprintf("file_%03d.txt", i)
// Every 50th file gets "match" in the name
if i%50 == 0 {
name = fmt.Sprintf("match_%03d.txt", i)
}
entries[i] = NewEntry(
fmt.Sprintf("/test/%s", name),
name,
int64(i*100),
time.Time{},
false,
)
}
s := makeSearchState(t, entries)
HandleSearch(s, "match")
// Should find entries at indices 0, 50, 100, 150, 200, 250
// These span pages 0, 0, 1, 1, 2, 2
expectedCount := 6
if len(s.SearchResults) != expectedCount {
t.Errorf("expected %d results spanning pages, got %d: %v", expectedCount, len(s.SearchResults), s.SearchResults)
}
expectedIndices := []int{0, 50, 100, 150, 200, 250}
for i, expected := range expectedIndices {
if i >= len(s.SearchResults) || s.SearchResults[i] != expected {
t.Errorf("result[%d] = %d, want %d", i, s.SearchResults[i], expected)
}
}
}
// --- Test: Search - Special Characters ---
func TestSearch_SpecialCharacters(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "file.name.dat", 100, time.Time{}, false),
NewEntry("/b", "file-name.dat", 200, time.Time{}, false),
NewEntry("/c", "file_name.dat", 300, time.Time{}, false),
NewEntry("/d", "file(name).dat", 400, time.Time{}, false),
})
// Dot should match all entries (all have .dat extension)
HandleSearch(s, ".")
if len(s.SearchResults) != 4 {
t.Errorf("expected 4 results for '.', got %d: %v", len(s.SearchResults), s.SearchResults)
}
// Hyphen should match "file-name.dat" only
HandleSearch(s, "-")
if len(s.SearchResults) != 1 {
t.Errorf("expected 1 result for '-', got %d: %v", len(s.SearchResults), s.SearchResults)
}
if len(s.SearchResults) > 0 && s.SearchResults[0] != 1 {
t.Errorf("expected result at index 1, got %d", s.SearchResults[0])
}
// Underscore should match "file_name.dat" only
HandleSearch(s, "_")
if len(s.SearchResults) != 1 {
t.Errorf("expected 1 result for '_', got %d: %v", len(s.SearchResults), s.SearchResults)
}
// Parentheses should match "file(name).dat" only
HandleSearch(s, "(")
if len(s.SearchResults) != 1 {
t.Errorf("expected 1 result for '(', got %d: %v", len(s.SearchResults), s.SearchResults)
}
}
// --- Test: Search - Clears Previous Results ---
func TestSearch_ClearsPreviousResults(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "alpha.txt", 100, time.Time{}, false),
NewEntry("/b", "beta.txt", 200, time.Time{}, false),
NewEntry("/c", "gamma.txt", 300, time.Time{}, false),
})
// First search
HandleSearch(s, "alpha")
if len(s.SearchResults) != 1 {
t.Fatalf("expected 1 result for 'alpha', got %d", len(s.SearchResults))
}
// Second search should replace results, not append
HandleSearch(s, "beta")
if len(s.SearchResults) != 1 {
t.Errorf("expected 1 result after new search, got %d: %v", len(s.SearchResults), s.SearchResults)
}
if s.SearchResults[0] != 1 {
t.Errorf("expected result at index 1, got %d", s.SearchResults[0])
}
if s.Query != "beta" {
t.Errorf("expected Query='beta', got %q", s.Query)
}
}
// --- Test: Search - Only Loaded Pages ---
func TestSearch_OnlyLoadedPages(t *testing.T) {
// Create state with only some pages loaded
s := NewBrowserState()
s.TotalEntries = 300
s.VisibleCount = 20
s.CurrentPath = "/test"
// Only load page 0 with entries containing "searchme"
entries0 := make([]Entry, PageSize)
for i := 0; i < PageSize; i++ {
name := fmt.Sprintf("file_%03d.txt", i)
if i < 5 {
name = fmt.Sprintf("searchme_%03d.txt", i)
}
entries0[i] = NewEntry(fmt.Sprintf("/test/%s", name), name, int64(i*100), time.Time{}, false)
}
s.Pages[0] = NewPage(0, entries0)
// Page 1 and 2 are NOT loaded — search should only find results in loaded pages
HandleSearch(s, "searchme")
// Should only find the 5 entries in page 0 (loaded pages only)
if len(s.SearchResults) != 5 {
t.Errorf("expected 5 results (only from loaded pages), got %d: %v", len(s.SearchResults), s.SearchResults)
}
}
// --- Test: Search - Directory Entries ---
func TestSearch_DirectoryEntries(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "mydir", 0, time.Time{}, true),
NewEntry("/b", "myfile.txt", 100, time.Time{}, false),
NewEntry("/c", "mydir2", 0, time.Time{}, true),
NewEntry("/d", "other.txt", 200, time.Time{}, false),
})
// Search should match both files and directories
HandleSearch(s, "mydir")
if len(s.SearchResults) != 2 {
t.Errorf("expected 2 results for 'mydir', got %d: %v", len(s.SearchResults), s.SearchResults)
}
}
// --- Test: Search - Numeric Names ---
func TestSearch_NumericNames(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "123.txt", 100, time.Time{}, false),
NewEntry("/b", "456.txt", 200, time.Time{}, false),
NewEntry("/c", "1234.txt", 300, time.Time{}, false),
NewEntry("/d", "789.txt", 400, time.Time{}, false),
})
HandleSearch(s, "123")
// Should match "123.txt" (0) and "1234.txt" (2)
if len(s.SearchResults) != 2 {
t.Errorf("expected 2 results for '123', got %d: %v", len(s.SearchResults), s.SearchResults)
}
}
// --- Test: Search - Empty Directory ---
func TestSearch_EmptyDirectory(t *testing.T) {
s := NewBrowserState()
s.TotalEntries = 0
s.VisibleCount = 20
HandleSearch(s, "anything")
if len(s.SearchResults) != 0 {
t.Errorf("expected 0 results for empty directory, got %d", len(s.SearchResults))
}
if s.GetScrollIndex() != 0 {
t.Errorf("expected ScrollIndex=0, got %d", s.GetScrollIndex())
}
}
// --- Test: Search - Whitespace Handling ---
func TestSearch_WhitespaceHandling(t *testing.T) {
s := makeSearchState(t, []Entry{
NewEntry("/a", "hello world.txt", 100, time.Time{}, false),
NewEntry("/b", "helloworld.txt", 200, time.Time{}, false),
NewEntry("/c", "hello.txt", 300, time.Time{}, false),
})
// Search with spaces
HandleSearch(s, "hello world")
if len(s.SearchResults) != 1 {
t.Errorf("expected 1 result for 'hello world', got %d: %v", len(s.SearchResults), s.SearchResults)
}
// Search single word
HandleSearch(s, "hello")
if len(s.SearchResults) != 3 {
t.Errorf("expected 3 results for 'hello', got %d: %v", len(s.SearchResults), s.SearchResults)
}
}