Pad/internal/test/e2e/search_test.go

285 lines
8.7 KiB
Go

package e2e_test
import (
"slices"
"strings"
"testing"
"time"
"pad/internal/browser"
"pad/internal/editor"
"pad/internal/test/e2e"
)
// TestSearchFiltersList verifies that typing in the search box filters the
// browser list so that only matching entries appear.
//
// BUG: computeVisibleEntries does not check SearchResults; it shows all
// entries in the visible window regardless of the search query.
func TestSearchFiltersList(t *testing.T) {
h := e2e.NewHarnessWithDefaults()
defer h.Cleanup()
_, err := h.WaitForFrameCount(1, 5*time.Second)
if err != nil {
t.Fatalf("timeout waiting for initial frames: %v", err)
}
// Navigate to browser page
editor.GoToBrowser(nil)
h.SendConfig(780, 1688)
time.Sleep(200 * time.Millisecond)
// Get initial list items (no filter)
initialItems := getListItems(t, e2e.GetLastFrame(h))
t.Logf("Initial items (%d): %v", len(initialItems), initialItems)
if len(initialItems) == 0 {
t.Skip("no list items; skipping")
}
// Search for "doc" — should match: Documents (directory)
// config.yaml does NOT contain "doc"
query := "doc"
h.SendSearchQuery(query)
time.Sleep(200 * time.Millisecond)
// Wait for new frame
_, err = h.WaitForFrameCount(h.FrameCount()+1, 5*time.Second)
if err != nil {
t.Logf("no new frame after search, checking current frame")
}
searchedItems := getListItems(t, e2e.GetLastFrame(h))
t.Logf("After searching '%s' (%d items): %v", query, len(searchedItems), searchedItems)
// All visible items should contain the query (case-insensitive)
for _, item := range searchedItems {
if item == "..." {
continue // loading placeholder
}
if !strings.Contains(strings.ToLower(item), strings.ToLower(query)) {
t.Errorf("search bug: item %q does not contain '%s' but was shown in search results", item, query)
}
}
// Documents should be in the results
foundDocuments := false
for _, item := range searchedItems {
if item == "Documents" {
foundDocuments = true
break
}
}
if !foundDocuments && len(searchedItems) > 0 {
t.Errorf("search bug: 'Documents' should be in search results for query '%s'", query)
}
}
// TestSearchWithSortModeChange verifies that changing sort mode while
// a search query is active preserves the filter.
//
// BUG: ToggleSortOrder clears pages but doesn't recompute SearchResults.
// After a sort change, stale SearchResults indices point to different entries.
func TestSearchWithSortModeChange(t *testing.T) {
h := e2e.NewHarnessWithDefaults()
defer h.Cleanup()
_, err := h.WaitForFrameCount(1, 5*time.Second)
if err != nil {
t.Fatalf("timeout waiting for initial frames: %v", err)
}
// Navigate to browser page
editor.GoToBrowser(nil)
h.SendConfig(780, 1688)
time.Sleep(200 * time.Millisecond)
// Apply a search query
query := "doc"
h.SendSearchQuery(query)
time.Sleep(200 * time.Millisecond)
_, err = h.WaitForFrameCount(h.FrameCount()+1, 5*time.Second)
if err != nil {
t.Logf("no new frame after search, checking current frame")
}
afterSearchItems := getListItems(t, e2e.GetLastFrame(h))
t.Logf("After search '%s' (%d items): %v", query, len(afterSearchItems), afterSearchItems)
// Verify all items contain the query
for _, item := range afterSearchItems {
if item == "..." {
continue
}
if !strings.Contains(strings.ToLower(item), strings.ToLower(query)) {
t.Errorf("search bug: item %q does not contain '%s' but was shown", item, query)
}
}
// Now toggle sort mode WHILE search is active
editor.ToggleSortOrder(nil)
h.SendConfig(780, 1688)
time.Sleep(300 * time.Millisecond)
afterSortItems := getListItems(t, e2e.GetLastFrame(h))
t.Logf("After sort toggle (%d items): %v", len(afterSortItems), afterSortItems)
// After sort toggle, the items should STILL be filtered by the search query
for _, item := range afterSortItems {
if item == "..." {
continue
}
if !strings.Contains(strings.ToLower(item), strings.ToLower(query)) {
t.Errorf("BUG: after sort toggle with active search, item %q does not contain '%s'. Search filter was lost!", item, query)
}
}
}
// TestSearchDirect verifies HandleSearch computes correct indices.
func TestSearchDirect(t *testing.T) {
state := browser.NewBrowserState()
state.CurrentPath = "/"
state.VisibleCount = 30
// Create entries with known names
entries := []browser.Entry{
browser.NewEntry("/alpha.txt", "alpha.txt", 100, time.Now(), false),
browser.NewEntry("/beta.txt", "beta.txt", 200, time.Now(), false),
browser.NewEntry("/gamma.doc", "gamma.doc", 300, time.Now(), false),
browser.NewEntry("/delta.log", "delta.log", 400, time.Now(), false),
browser.NewEntry("/epsilon.txt", "epsilon.txt", 500, time.Now(), false),
browser.NewEntry("/zeta.doc", "zeta.doc", 600, time.Now(), false),
}
state.TotalEntries = len(entries)
state.SortIndex = &browser.DirectoryIndex{
Path: "/",
EntryCount: len(entries),
Entries: entries,
SortOrders: map[string][]int{
"name_asc": {0, 1, 2, 3, 4, 5},
},
}
state.SortMode = browser.SortModeNameAsc
// Load initial pages
browser.LoadInitialPages(state)
// Search for "txt" — should match: alpha.txt, beta.txt, epsilon.txt
browser.HandleSearch(state, "txt")
expectedMatches := 3 // alpha.txt, beta.txt, epsilon.txt
if len(state.SearchResults) != expectedMatches {
t.Errorf("expected %d search results, got %d: %v",
expectedMatches, len(state.SearchResults), state.SearchResults)
}
// Verify each result index points to a matching entry
for _, idx := range state.SearchResults {
if idx >= len(entries) {
t.Errorf("search result index %d out of bounds (total: %d)", idx, len(entries))
continue
}
entry := entries[idx]
if !strings.Contains(strings.ToLower(entry.Name), "txt") {
t.Errorf("search result index %d points to %q which does not contain 'txt'", idx, entry.Name)
}
}
}
// TestSortModeChangePreservesSearchFilter verifies at the state level that
// changing sort mode with an active search query maintains correct filtering.
func TestSortModeChangePreservesSearchFilter(t *testing.T) {
h := e2e.NewHarnessWithDefaults()
defer h.Cleanup()
_, err := h.WaitForFrameCount(1, 5*time.Second)
if err != nil {
t.Fatalf("timeout waiting for initial frames: %v", err)
}
state := h.State()
// Navigate to browser
editor.GoToBrowser(nil)
h.SendConfig(780, 1688)
time.Sleep(200 * time.Millisecond)
// Apply search
query := "doc"
h.SendSearchQuery(query)
time.Sleep(200 * time.Millisecond)
// Record search results before sort change
searchResultsBefore := make([]int, len(state.Browser.SearchResults))
copy(searchResultsBefore, state.Browser.SearchResults)
t.Logf("Search results before sort change: %v", searchResultsBefore)
// Verify all pre-sort results are valid matches
for _, idx := range state.Browser.SearchResults {
if idx < len(state.Browser.SortIndex.Entries) {
entry := state.Browser.SortIndex.Entries[idx]
if !strings.Contains(strings.ToLower(entry.Name), strings.ToLower(query)) {
t.Errorf("pre-sort: index %d -> %q does not match query %q", idx, entry.Name, query)
}
}
}
// Toggle sort mode
editor.ToggleSortOrder(nil)
h.SendConfig(780, 1688)
time.Sleep(300 * time.Millisecond)
// After sort change, search results should still be valid
for _, idx := range state.Browser.SearchResults {
if idx >= len(state.Browser.SortIndex.Entries) {
t.Errorf("search result index %d is out of bounds (total: %d)",
idx, state.Browser.TotalEntries)
continue
}
entry := state.Browser.SortIndex.Entries[idx]
if !strings.Contains(strings.ToLower(entry.Name), strings.ToLower(query)) {
t.Errorf("post-sort: search result index %d -> %q does not match query %q",
idx, entry.Name, query)
}
}
}
// TestSearchClearRestoresFullList verifies that clearing the search query
// restores the full unfiltered list.
func TestSearchClearRestoresFullList(t *testing.T) {
h := e2e.NewHarnessWithDefaults()
defer h.Cleanup()
_, err := h.WaitForFrameCount(1, 5*time.Second)
if err != nil {
t.Fatalf("timeout waiting for initial frames: %v", err)
}
// Navigate to browser page
editor.GoToBrowser(nil)
h.SendConfig(780, 1688)
time.Sleep(200 * time.Millisecond)
initialItems := getListItems(t, e2e.GetLastFrame(h))
t.Logf("Initial items (%d): %v", len(initialItems), initialItems)
// Search for something specific
h.SendSearchQuery("doc")
time.Sleep(200 * time.Millisecond)
// Clear search
h.SendSearchQuery("")
time.Sleep(200 * time.Millisecond)
clearedItems := getListItems(t, e2e.GetLastFrame(h))
t.Logf("After clear (%d items): %v", len(clearedItems), clearedItems)
// After clearing, should show all entries again
if !slices.Equal(initialItems, clearedItems) {
t.Errorf("after clearing search, list should match initial list.\n initial: %v\n cleared: %v", initialItems, clearedItems)
}
}