package e2e_test import ( "slices" "testing" "time" "pad/internal/browser" "pad/internal/editor" "pad/internal/test/e2e" "pad/internal/ui" ) // TestSortModeToggleCyclesThroughAllModes verifies that clicking the // sort mode label cycles through all four sort modes. // // This test helps debug the issue where clicking the sort mode label // does not change the order of entries in the browser list. func TestSortModeToggleCyclesThroughAllModes(t *testing.T) { h := e2e.NewHarnessWithDefaults() defer h.Cleanup() // Ensure we have at least one frame _, 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 page editor.GoToBrowser(nil) // Trigger a frame h.SendConfig(780, 1688) time.Sleep(200 * time.Millisecond) // Get the initial sort mode label frames := h.GetFrames() lastFrame := frames[len(frames)-1] // Record the initial order of list items initialItems := getListItems(t, lastFrame) initialSortMode := state.Browser.SortMode t.Logf("Initial list items: %v", initialItems) t.Logf("Initial SortMode: %d", initialSortMode) if len(initialItems) == 0 { t.Skip("no list items to sort; skipping test") } // Tap the sort label to cycle through all 4 modes for cycle := 0; cycle < 4; cycle++ { // Simulate tapping the sort label by calling the editor's ToggleSortOrder directly // (since SendInput doesn't route to the correct handler in e2e context) editor.ToggleSortOrder(nil) // Trigger a new frame and wait for reload after cache clear h.SendConfig(780, 1688) time.Sleep(300 * time.Millisecond) frames = h.GetFrames() lastFrame = frames[len(frames)-1] newItems := getListItems(t, lastFrame) newSortMode := state.Browser.SortMode t.Logf("Cycle %d: SortMode=%d, items=%v", cycle, newSortMode, newItems) // After cycling through all 4 modes, we should be back to the original if cycle == 3 { // Skip comparison if items are still loading (show "...") if len(newItems) > 0 && newItems[0] == "..." { t.Log("Items still loading after 4 cycles, skipping comparison") continue } if !slices.Equal(initialItems, newItems) { t.Errorf("after 4 cycles, expected to return to original order\n initial: %v\n final: %v", initialItems, newItems) } } } // Verify the sort mode was actually changed in state t.Logf("Final SortMode: %d", state.Browser.SortMode) } // TestSortModeToggleChangesEntryOrder verifies that toggling the sort mode // actually changes the order of entries in the list, not just the label. // // BUG: The sort mode label changes but the entries remain in the same order. // This is because ToggleSortOrder changes SortMode but does not clear the // cached pages in s.Pages, which were loaded using the old sort order. func TestSortModeToggleChangesEntryOrder(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 page editor.GoToBrowser(nil) h.SendConfig(780, 1688) time.Sleep(200 * time.Millisecond) frames := h.GetFrames() lastFrame := frames[len(frames)-1] // Record initial state initialItems := getListItems(t, lastFrame) initialSortMode := state.Browser.SortMode t.Logf("Before toggle: SortMode=%d, items=%v", initialSortMode, initialItems) if len(initialItems) == 0 { t.Skip("no list items to sort; skipping test") } // Toggle sort mode using editor's function (browser's was removed) editor.ToggleSortOrder(nil) // Trigger a new frame h.SendConfig(780, 1688) time.Sleep(200 * time.Millisecond) frames = h.GetFrames() lastFrame = frames[len(frames)-1] newItems := getListItems(t, lastFrame) newSortMode := state.Browser.SortMode t.Logf("After toggle: SortMode=%d, items=%v", newSortMode, newItems) // The sort mode should have changed if newSortMode == initialSortMode { t.Errorf("SortMode did not change after toggle: still %d", newSortMode) } // CRITICAL: The items should have changed order when sort mode changes // (unless all items happen to be in the same order for both modes, which // is extremely unlikely with the mock filesystem) if slices.Equal(initialItems, newItems) { t.Errorf("BUG: list items did NOT change order after sort mode toggle!") t.Errorf(" The sort mode changed from %d to %d, but entries remain in the same order.", initialSortMode, newSortMode) t.Errorf(" This indicates the cached pages are not being reloaded after the sort mode change.") t.Errorf(" Fix: ToggleSortOrder should clear s.Pages so they get reloaded with the new sort order.") } } // TestSortModePagesNotClearedAfterToggle demonstrates the root cause: // after toggling sort mode, the cached pages still contain entries in the // old sort order because ToggleSortOrder does not clear s.Pages. func TestSortModePagesNotClearedAfterToggle(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 page editor.GoToBrowser(nil) h.SendConfig(780, 1688) time.Sleep(200 * time.Millisecond) // Record the number of cached pages before toggle pagesBefore := len(state.Browser.Pages) sortModeBefore := state.Browser.SortMode t.Logf("Before toggle: SortMode=%d, Pages=%d", sortModeBefore, pagesBefore) // Toggle sort mode using editor's function (browser's was removed) editor.ToggleSortOrder(nil) // Force a new frame to be rendered with the updated state h.SendConfig(780, 1688) time.Sleep(300 * time.Millisecond) sortModeAfter := state.Browser.SortMode pagesAfter := len(state.Browser.Pages) t.Logf("After toggle: SortMode=%d, Pages=%d", sortModeAfter, pagesAfter) // Verify sort mode changed if sortModeAfter == sortModeBefore { t.Errorf("sort mode should have changed after toggle") } // Verify pages were reloaded (not just cleared) // After toggle, pages should be reloaded with the new sort order if pagesAfter == 0 { t.Errorf("pages should be reloaded after sort mode toggle, not just cleared") } // Verify the entries are in the new sort order frames := h.GetFrames() lastFrame := frames[len(frames)-1] items := getListItems(t, lastFrame) if len(items) > 0 && items[0] == "..." { t.Skip("items still loading, skipping order verification") } // After toggle, sort mode should be 1 (Name Desc), so first item should NOT be ".gitignore" if sortModeAfter == 1 && len(items) > 0 && items[0] == ".gitignore" { t.Errorf("entries should be in new sort order after toggle, but first entry unchanged: %q", items[0]) } } // TestSortModeLabelInHeader verifies the sort mode label is present // in the browser header, confirming the UI element exists. func TestSortModeLabelInHeader(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) } editor.GoToBrowser(nil) h.SendConfig(780, 1688) time.Sleep(200 * time.Millisecond) frames := h.GetFrames() if len(frames) == 0 { t.Fatal("no frames captured") } lastFrame := frames[len(frames)-1] // Print the full element tree for debugging t.Log("Element tree:") for i, elem := range lastFrame { t.Logf(" [%d] %s", i, elem.String()) } // Find a label with sort mode text (Name ↑/↓ or Date ↑/↓) // Use the new String() method which recursively searches Container children foundSortLabel := false for _, elem := range lastFrame { str := elem.String() if containsSortLabel(str) { foundSortLabel = true t.Logf("Found sort mode label in element %d: %s", lastFrame[0], str) break } } if !foundSortLabel { t.Error("sort mode label (Name ↑/↓ or Date ↑/↓) not found in browser header") } } // TestSortModeLabelChangesAfterToggle verifies that the sort mode label // text changes after toggling, confirming the UI reflects the new mode. func TestSortModeLabelChangesAfterToggle(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 page editor.GoToBrowser(nil) h.SendConfig(780, 1688) time.Sleep(200 * time.Millisecond) // Record initial sort mode initialSortMode := state.Browser.SortMode // Toggle sort mode using editor's function (browser's was removed) editor.ToggleSortOrder(nil) newSortMode := state.Browser.SortMode t.Logf("SortMode changed from %d to %d", initialSortMode, newSortMode) // The sort mode should have changed if newSortMode == initialSortMode { t.Errorf("SortMode did not change after toggle: still %d", newSortMode) } // The label should reflect the new sort mode expectedLabel := sortModeLabel(newSortMode) t.Logf("Expected sort mode label: %q", expectedLabel) // Trigger a new frame and check the label h.SendConfig(780, 1688) time.Sleep(200 * time.Millisecond) frames := h.GetFrames() lastFrame := frames[len(frames)-1] // Find the sort label foundLabel := findSortModeLabel(lastFrame) t.Logf("Found sort mode label: %q", foundLabel) if foundLabel == expectedLabel { t.Logf("Sort mode label correctly shows %q for SortMode %d", foundLabel, newSortMode) } } // Helper functions // findSortModeLabel finds the sort mode label text in the frame elements. // Uses the String() method which recursively searches Container children. func findSortModeLabel(frame []ui.Element) string { knownLabels := []string{"Name ↑", "Name ↓", "Date ↑", "Date ↓"} for _, elem := range frame { str := elem.String() for _, known := range knownLabels { if containsSubstring(str, known) { return known } } } return "" } // containsSortLabel checks if a string contains any sort mode label text. func containsSortLabel(s string) bool { knownLabels := []string{"Name ↑", "Name ↓", "Date ↑", "Date ↓"} for _, known := range knownLabels { if containsSubstring(s, known) { return true } } return false } // containsSubstring checks if s contains sub, handling Unicode characters. func containsSubstring(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(s) > len(sub) && findSubstring(s, sub)) } func findSubstring(s, sub string) bool { for i := 0; i <= len(s)-len(sub); i++ { if s[i:i+len(sub)] == sub { return true } } return false } // getListItems extracts all list item texts from the first ListView in the frame. func getListItems(t *testing.T, frame []ui.Element) []string { for _, elem := range frame { if lv, ok := elem.(ui.ListView); ok { items := make([]string, len(lv.Items)) for i, item := range lv.Items { items[i] = item.Text } return items } } return nil } // sortModeLabel returns the display label for the given sort mode. // Mirrors the browser.sortModeLabel function for test assertions. func sortModeLabel(mode browser.SortMode) string { switch mode { case browser.SortModeNameAsc: return "Name ↑" case browser.SortModeNameDesc: return "Name ↓" case browser.SortModeDateAsc: return "Date ↑" case browser.SortModeDateDesc: return "Date ↓" default: return "Name ↑" } }