Pad/internal/browser/lazy_loading_test.go
Greg Pomerantz 16740b0de0 Browser: keep the file list live (re-read on change, re-sort, no idle frames)
The browser list was cached: files added/removed/renamed, or whose mtime
changed, did not show up (and did not re-sort) until the user navigated
away and back in. In particular, opening a file and returning showed a
stale list — a new file created elsewhere only appeared after navigating
up and back down.

The browser now re-reads the current directory while it is on screen:

- A short (~1 s) timer re-reads the visible directory so external changes
  (including inbound syncs) appear live, re-sorted in the current sort
  mode. A landing re-index repopulates the visible+prefetch pages
  synchronously (no flicker), preserves the scroll offset, and re-filters
  an active search without a scroll jump.
- Returning to the browser from the editor triggers one immediate re-read
  so the list is current the moment you land back.
- Bounded to one read per directory: a refresh while a read of the same
  directory is in flight coalesces, and a stale result for a directory the
  user has since left is dropped rather than clobbering the current view.
- A failed background refresh keeps the last good view (the next tick
  retries); a failed navigation still recovers to the previous directory.
- Change detection: a refresh that finds the directory unchanged rebuilds
  nothing and emits no frame, preserving the event-driven emission
  contract (TestNoFramesWhileIdle_Browser) while the browser idles.

Verified on-device: external add while sitting in the browser appears and
re-sorts to the top within ~1 s; an external add+remove made while in the
editor is reflected immediately on return; an external delete drops the
row within ~1 s.
2026-09-02 18:23:16 -04:00

117 lines
3.1 KiB
Go

package browser
import (
"fmt"
"testing"
"time"
"pad/internal/io/pool"
"pad/internal/io/pool/mock"
)
// TestLazyLoadingLargeDirectory verifies that page-based loading works
// correctly for large directories (10,000 entries), ensuring only visible
// and prefetched pages are kept in memory.
func TestLazyLoadingLargeDirectory(t *testing.T) {
state := NewBrowserState()
state.TotalEntries = 10000
state.EntryHeight = 48.0
state.VisibleCount = 10
mfs := mock.NewFileSystem()
wp := pool.NewWorkerPool(4)
wp.Start()
// Populate mock filesystem with 10k entries
mfs.AddDir("/dir", time.Now())
for i := 0; i < 10000; i++ {
mfs.AddFile(fmt.Sprintf("/dir/file_%d.txt", i), []byte("data"), time.Now())
}
bm, err := NewBrowserManager(state, wp, mfs)
if err != nil {
t.Fatalf("NewBrowserManager failed: %v", err)
}
// 1. Navigate to directory
bm.NavigateTo("/dir")
// Helper to get result with timeout
getResult := func(timeout time.Duration) (pool.Result, bool) {
select {
case res := <-wp.ResultChan():
return res, true
case <-time.After(timeout):
return pool.Result{}, false
}
}
// Wait for index build result and process it. The visible+prefetch pages
// are populated synchronously from the fresh index inside HandleResult (no
// separate initial LoadPages result is dispatched).
res, ok := getResult(15 * time.Second)
if !ok {
t.Fatal("Timeout waiting for BuildIndex result")
}
bm.HandleResult(res)
// 2. Verify only initial pages loaded (visible + prefetch)
if len(state.Pages) == 0 {
t.Error("Expected pages to be loaded")
}
// 3. Scroll down to page 50
state.ScrollOffset = 50 * 100 * state.EntryHeight // Scroll to middle
bm.OnScroll()
// Wait for load pages result for scroll
res, ok = getResult(15 * time.Second)
if !ok {
t.Fatal("Timeout waiting for scroll LoadPages result")
}
bm.HandleResult(res)
// 5. Verify old pages are evicted (e.g., page 0)
if page, ok := state.Pages[0]; ok && page.Loaded {
t.Error("Expected page 0 to be evicted")
}
}
// TestPrefetchOnScroll verifies that scroll triggers prefetch for
// pages within PrefetchDist.
func TestPrefetchOnScroll(t *testing.T) {
state := NewBrowserState()
state.TotalEntries = 1000
state.EntryHeight = 48.0
state.VisibleCount = 10
mfs := mock.NewFileSystem()
wp := pool.NewWorkerPool(4)
wp.Start()
// Populate mock filesystem
mfs.AddDir("/dir", time.Now())
for i := 0; i < 1000; i++ {
mfs.AddFile(fmt.Sprintf("/dir/file_%d.txt", i), []byte("data"), time.Now())
}
bm, err := NewBrowserManager(state, wp, mfs)
if err != nil {
t.Fatalf("NewBrowserManager failed: %v", err)
}
bm.NavigateTo("/dir")
// Process BuildIndex (visible+prefetch pages are populated synchronously).
bm.HandleResult(<-wp.ResultChan())
// Current visible ~page 0. Pages 0, 1, 2 should be loaded.
// Scroll to page 5.
state.ScrollOffset = 5 * 100 * state.EntryHeight
bm.OnScroll()
// Process prefetch load
bm.HandleResult(<-wp.ResultChan())
// Prefetch should load pages around 5 (e.g., 3, 4, 5, 6, 7)
if _, ok := state.Pages[6]; !ok {
t.Error("Expected page 6 to be prefetched")
}
}