Frame now carries view state (scale/focus/query) to the main goroutine, which reads only the frame-receiver-stored snapshot. Renderer owns Gio widget editors (registered by ID) and the draw scale. Autosave timer sends a token to the logic goroutine instead of touching state; Shutdown waits for the owner to exit before FlushAll. Tests access state only through owner-side Inspect/WithState helpers (harness + in-package). Fixed double-Harness.Run race in two e2e tests, made TestWorkerPool_PriorityPreemption deterministic, and raised the lazy-loading test timeout that was too short under -race. go test -race ./... is now green.
410 lines
12 KiB
Go
410 lines
12 KiB
Go
package e2e_test
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"pad/internal/browser"
|
|
"pad/internal/editor"
|
|
"pad/internal/test/e2e"
|
|
"pad/internal/ui"
|
|
)
|
|
|
|
// sortModeOf reads the browser sort mode on the logic goroutine (owner).
|
|
func sortModeOf(t *testing.T, h *e2e.Harness) browser.SortMode {
|
|
t.Helper()
|
|
v, err := h.Inspect(func(st *editor.State) any { return st.Browser.SortMode })
|
|
if err != nil {
|
|
t.Fatalf("Inspect: %v", err)
|
|
}
|
|
return v.(browser.SortMode)
|
|
}
|
|
|
|
// pageCountOf reads the number of cached browser pages on the logic goroutine.
|
|
func pageCountOf(t *testing.T, h *e2e.Harness) int {
|
|
t.Helper()
|
|
v, err := h.Inspect(func(st *editor.State) any { return len(st.Browser.Pages) })
|
|
if err != nil {
|
|
t.Fatalf("Inspect: %v", err)
|
|
}
|
|
return v.(int)
|
|
}
|
|
|
|
// goBrowser navigates to the browser page on the logic goroutine.
|
|
func goBrowser(t *testing.T, h *e2e.Harness) {
|
|
t.Helper()
|
|
if err := h.WithState(func(st *editor.State) { editor.GoToBrowser(nil) }); err != nil {
|
|
t.Fatalf("GoToBrowser: %v", err)
|
|
}
|
|
}
|
|
|
|
// toggleSort toggles the browser sort mode on the logic goroutine.
|
|
func toggleSort(t *testing.T, h *e2e.Harness) {
|
|
t.Helper()
|
|
if err := h.WithState(func(st *editor.State) { editor.ToggleSortOrder(nil) }); err != nil {
|
|
t.Fatalf("ToggleSortOrder: %v", err)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Navigate to browser page
|
|
goBrowser(t, h)
|
|
|
|
// 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 := sortModeOf(t, h)
|
|
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
|
|
// (since SendInput doesn't route to the correct handler in e2e context)
|
|
toggleSort(t, h)
|
|
|
|
// 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 := sortModeOf(t, h)
|
|
|
|
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", sortModeOf(t, h))
|
|
}
|
|
|
|
// TestSortModeToggleChangesEntryOrder verifies that toggling the sort mode
|
|
// actually changes the order of entries in the list, not just the label.
|
|
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)
|
|
}
|
|
|
|
// Navigate to browser page
|
|
goBrowser(t, h)
|
|
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 := sortModeOf(t, h)
|
|
|
|
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)
|
|
toggleSort(t, h)
|
|
|
|
// 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 := sortModeOf(t, h)
|
|
|
|
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)
|
|
}
|
|
|
|
// Navigate to browser page
|
|
goBrowser(t, h)
|
|
h.SendConfig(780, 1688)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Record the number of cached pages before toggle
|
|
pagesBefore := pageCountOf(t, h)
|
|
sortModeBefore := sortModeOf(t, h)
|
|
|
|
t.Logf("Before toggle: SortMode=%d, Pages=%d", sortModeBefore, pagesBefore)
|
|
|
|
// Toggle sort mode using editor's function (browser's was removed)
|
|
toggleSort(t, h)
|
|
|
|
// Force a new frame to be rendered with the updated state
|
|
h.SendConfig(780, 1688)
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
sortModeAfter := sortModeOf(t, h)
|
|
pagesAfter := pageCountOf(t, h)
|
|
|
|
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)
|
|
}
|
|
|
|
goBrowser(t, h)
|
|
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)
|
|
}
|
|
|
|
// Navigate to browser page
|
|
goBrowser(t, h)
|
|
h.SendConfig(780, 1688)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Record initial sort mode
|
|
initialSortMode := sortModeOf(t, h)
|
|
|
|
// Toggle sort mode using editor's function (browser's was removed)
|
|
toggleSort(t, h)
|
|
|
|
newSortMode := sortModeOf(t, h)
|
|
|
|
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 ↑"
|
|
}
|
|
}
|