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.
317 lines
10 KiB
Go
317 lines
10 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.
|
|
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 (owner-side)
|
|
if err := h.WithState(func(st *editor.State) { editor.GoToBrowser(nil) }); err != nil {
|
|
t.Fatalf("GoToBrowser: %v", err)
|
|
}
|
|
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.
|
|
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 (owner-side)
|
|
if err := h.WithState(func(st *editor.State) { editor.GoToBrowser(nil) }); err != nil {
|
|
t.Fatalf("GoToBrowser: %v", err)
|
|
}
|
|
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 (owner-side)
|
|
if err := h.WithState(func(st *editor.State) { editor.ToggleSortOrder(nil) }); err != nil {
|
|
t.Fatalf("ToggleSortOrder: %v", err)
|
|
}
|
|
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)
|
|
}
|
|
|
|
// Navigate to browser (owner-side)
|
|
if err := h.WithState(func(st *editor.State) { editor.GoToBrowser(nil) }); err != nil {
|
|
t.Fatalf("GoToBrowser: %v", err)
|
|
}
|
|
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 (owner-side snapshot)
|
|
_, err = h.Inspect(func(st *editor.State) any {
|
|
searchResultsBefore := make([]int, len(st.Browser.SearchResults))
|
|
copy(searchResultsBefore, st.Browser.SearchResults)
|
|
t.Logf("Search results before sort change: %v", searchResultsBefore)
|
|
|
|
// Verify all pre-sort results are valid matches
|
|
for _, sortedIdx := range st.Browser.SearchResults {
|
|
positionMap := st.Browser.GetSortedIndices()
|
|
rawIdx := positionMap[sortedIdx]
|
|
if rawIdx < len(st.Browser.SortIndex.Entries) {
|
|
entry := st.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)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Inspect: %v", err)
|
|
}
|
|
|
|
// Toggle sort mode (owner-side)
|
|
if err := h.WithState(func(st *editor.State) { editor.ToggleSortOrder(nil) }); err != nil {
|
|
t.Fatalf("ToggleSortOrder: %v", err)
|
|
}
|
|
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.
|
|
// (Owner-side snapshot: all reads happen on the logic goroutine.)
|
|
_, err = h.Inspect(func(st *editor.State) any {
|
|
for _, sortedIdx := range st.Browser.SearchResults {
|
|
if sortedIdx >= len(st.Browser.SortIndex.Entries) {
|
|
t.Errorf("search result index %d is out of bounds (total: %d)",
|
|
sortedIdx, st.Browser.TotalEntries)
|
|
continue
|
|
}
|
|
|
|
// Map sortedIdx back to rawIdx to check the actual entry
|
|
positionMap := st.Browser.GetSortedIndices()
|
|
rawIdx := positionMap[sortedIdx]
|
|
|
|
entry := st.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)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Inspect: %v", err)
|
|
}
|
|
}
|
|
|
|
// 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 (owner-side)
|
|
if err := h.WithState(func(st *editor.State) { editor.GoToBrowser(nil) }); err != nil {
|
|
t.Fatalf("GoToBrowser: %v", err)
|
|
}
|
|
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)
|
|
}
|
|
}
|