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.
434 lines
12 KiB
Go
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)
|
|
}
|
|
}
|