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.
117 lines
3.1 KiB
Go
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")
|
|
}
|
|
}
|