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 _, sortedIdx := range state.Browser.SearchResults { positionMap := state.Browser.GetSortedIndices() rawIdx := positionMap[sortedIdx] if rawIdx < len(state.Browser.SortIndex.Entries) { entry := state.Browser.SortIndex.Entries[rawIdx] if !strings.Contains(strings.ToLower(entry.Name), strings.ToLower(query)) { t.Errorf("pre-sort: index %d (raw %d) -> %q does not match query %q", sortedIdx, rawIdx, 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 // The problem in the test might be that it expects the *indices* // to be the same, but the indices represent sorted positions. // When the sort mode changes, the position map changes, // so the *index* of the entry "Documents" (which matches "doc") // will change! // Let's print the entries to see if they are still correct, // ignoring the index values themselves. for _, sortedIdx := range state.Browser.SearchResults { if sortedIdx >= len(state.Browser.SortIndex.Entries) { t.Errorf("search result index %d is out of bounds (total: %d)", sortedIdx, state.Browser.TotalEntries) continue } // Map sortedIdx back to rawIdx to check the actual entry positionMap := state.Browser.GetSortedIndices() rawIdx := positionMap[sortedIdx] entry := state.Browser.SortIndex.Entries[rawIdx] if !strings.Contains(strings.ToLower(entry.Name), strings.ToLower(query)) { t.Errorf("post-sort: search result sortedIdx %d -> rawIdx %d -> %q does not match query %q", sortedIdx, rawIdx, 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) } }