feat(browser): Implement lazy loading and E2E tests for browser
This commit is contained in:
parent
3a2e21e4b3
commit
94db42d2ad
122
internal/browser/lazy_loading_test.go
Normal file
122
internal/browser/lazy_loading_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
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
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(4)
|
||||||
|
wp.Start()
|
||||||
|
|
||||||
|
// Populate mock filesystem with 10k entries
|
||||||
|
fs.AddDir("/dir", time.Now())
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
fs.AddFile(fmt.Sprintf("/dir/file_%d.txt", i), []byte("data"), time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
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
|
||||||
|
res, ok := getResult(2 * time.Second)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Timeout waiting for BuildIndex result")
|
||||||
|
}
|
||||||
|
bm.HandleResult(res)
|
||||||
|
|
||||||
|
// Wait for initial page load result and process it
|
||||||
|
res, ok = getResult(2 * time.Second)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Timeout waiting for initial LoadPages 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(2 * 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
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(4)
|
||||||
|
wp.Start()
|
||||||
|
|
||||||
|
// Populate mock filesystem
|
||||||
|
fs.AddDir("/dir", time.Now())
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
fs.AddFile(fmt.Sprintf("/dir/file_%d.txt", i), []byte("data"), time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bm.NavigateTo("/dir")
|
||||||
|
// Process BuildIndex and initial load
|
||||||
|
bm.HandleResult(<-wp.ResultChan())
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
153
internal/browser/manager.go
Normal file
153
internal/browser/manager.go
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"pad/internal/io/pool"
|
||||||
|
"pad/internal/io/pool/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrDirNotFound = fmt.Errorf("directory not found")
|
||||||
|
|
||||||
|
type BrowserManager struct {
|
||||||
|
state *BrowserState
|
||||||
|
workerPool *pool.WorkerPool
|
||||||
|
fs *mock.FileSystem
|
||||||
|
dirPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBrowserManager(state *BrowserState, wp *pool.WorkerPool, fs *mock.FileSystem) (*BrowserManager, error) {
|
||||||
|
return &BrowserManager{
|
||||||
|
state: state,
|
||||||
|
workerPool: wp,
|
||||||
|
fs: fs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bm *BrowserManager) NavigateTo(dirPath string) {
|
||||||
|
fmt.Printf("NavigateTo: %s\n", dirPath)
|
||||||
|
bm.dirPath = dirPath
|
||||||
|
navigateToDirectory(bm.state, dirPath)
|
||||||
|
bm.state.Loading = true
|
||||||
|
|
||||||
|
// Dispatch BuildIndexTask to the worker pool.
|
||||||
|
// In a real app, this would be a task that reads and sorts the directory.
|
||||||
|
task := pool.NewBuildIndexTask(dirPath, bm.fs)
|
||||||
|
bm.workerPool.Dispatch(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bm *BrowserManager) OnScroll() {
|
||||||
|
if bm.state.TotalEntries == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify unloaded pages in the visible + prefetch range
|
||||||
|
minPage, maxPage := computeVisiblePageRange(bm.state)
|
||||||
|
var pagesToLoad []int
|
||||||
|
for i := minPage; i <= maxPage; i++ {
|
||||||
|
if page, ok := bm.state.Pages[i]; !ok || !page.Loaded {
|
||||||
|
pagesToLoad = append(pagesToLoad, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("OnScroll: minPage=%d, maxPage=%d, pagesToLoad=%v\n", minPage, maxPage, pagesToLoad)
|
||||||
|
|
||||||
|
// Dispatch LoadPagesTask for unloaded pages
|
||||||
|
if len(pagesToLoad) > 0 {
|
||||||
|
fmt.Printf("OnScroll: Dispatching LoadPagesTask for pages %v\n", pagesToLoad)
|
||||||
|
task := pool.NewLoadPagesTask(bm.dirPath, pagesToLoad, bm.fs)
|
||||||
|
bm.workerPool.Dispatch(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evict distant pages to save memory
|
||||||
|
bm.state.EvictPages()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bm *BrowserManager) HandleResult(result pool.Result) {
|
||||||
|
if !result.IsBrowserResult() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result.TaskType {
|
||||||
|
case pool.TypeBuildIndex:
|
||||||
|
if result.Success {
|
||||||
|
bm.handleBuildIndexSuccess(result)
|
||||||
|
} else {
|
||||||
|
bm.handleError(result)
|
||||||
|
}
|
||||||
|
case pool.TypeLoadPages:
|
||||||
|
if result.Success {
|
||||||
|
bm.handleLoadPagesSuccess(result)
|
||||||
|
} else {
|
||||||
|
bm.handleError(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bm *BrowserManager) handleBuildIndexSuccess(result pool.Result) {
|
||||||
|
fmt.Printf("handleBuildIndexSuccess: TotalEntries=%d\n", len(result.Data.([]mock.DirEntry)))
|
||||||
|
bm.state.Loading = false
|
||||||
|
|
||||||
|
// In the mock implementation, BuildIndexTask returns []mock.DirEntry.
|
||||||
|
// We need to convert these to browser.Entry and build the SortIndex.
|
||||||
|
entries := result.Data.([]mock.DirEntry)
|
||||||
|
|
||||||
|
var browserEntries []Entry
|
||||||
|
for _, e := range entries {
|
||||||
|
info, _ := e.Info()
|
||||||
|
browserEntries = append(browserEntries, Entry{
|
||||||
|
Path: filepath.Join(bm.dirPath, e.Name()),
|
||||||
|
Name: e.Name(),
|
||||||
|
Size: info.Size(),
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
IsDir: e.IsDir(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the sorted index with position maps
|
||||||
|
bm.state.SortIndex = &DirectoryIndex{
|
||||||
|
Path: bm.dirPath,
|
||||||
|
EntryCount: len(browserEntries),
|
||||||
|
Entries: browserEntries,
|
||||||
|
SortOrders: make(map[string][]int),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute position maps for all sort modes
|
||||||
|
for mode := SortMode(0); mode < SortMode(modeCount()); mode++ {
|
||||||
|
key := sortModeKey(mode)
|
||||||
|
if key != "" {
|
||||||
|
bm.state.SortIndex.SortOrders[key] = buildPositionMap(browserEntries, mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bm.state.TotalEntries = len(browserEntries)
|
||||||
|
fmt.Printf("handleBuildIndexSuccess: TotalEntries set to %d\n", bm.state.TotalEntries)
|
||||||
|
|
||||||
|
// Trigger initial page load
|
||||||
|
bm.OnScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bm *BrowserManager) handleLoadPagesSuccess(result pool.Result) {
|
||||||
|
// In a real implementation, LoadPagesTask would return the actual entry data.
|
||||||
|
// In our current mock/placeholder, it just returns the page indices.
|
||||||
|
// We'll load the data from the SortIndex.
|
||||||
|
pageIndices := result.Data.([]int)
|
||||||
|
|
||||||
|
for _, idx := range pageIndices {
|
||||||
|
page := loadPageFromIndex(bm.state, idx)
|
||||||
|
if page != nil {
|
||||||
|
bm.state.Pages[idx] = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bm *BrowserManager) handleError(result pool.Result) {
|
||||||
|
fmt.Printf("handleError: TaskType=%s, Error=%v\n", result.TaskType, result.Error)
|
||||||
|
bm.state.Loading = false
|
||||||
|
// For now, just reset the state or log the error.
|
||||||
|
// In production, we'd show a toast or error message.
|
||||||
|
if bm.state.CurrentPath == bm.dirPath {
|
||||||
|
bm.state.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
305
internal/browser/manager_test.go
Normal file
305
internal/browser/manager_test.go
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"pad/internal/io/pool"
|
||||||
|
"pad/internal/io/pool/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestNewBrowserManager verifies that a BrowserManager can be constructed
|
||||||
|
// with valid parameters and has correct initial state.
|
||||||
|
func TestNewBrowserManager(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm == nil {
|
||||||
|
t.Fatal("NewBrowserManager returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify initial state
|
||||||
|
if bm.state != state {
|
||||||
|
t.Errorf("BrowserManager.state = %p, want %p", bm.state, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bm.dirPath != "" {
|
||||||
|
t.Errorf("BrowserManager.dirPath = %q, want empty string", bm.dirPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNavigateTo verifies that NavigateTo dispatches a BuildIndexTask
|
||||||
|
// and resets browser state appropriately.
|
||||||
|
func TestNavigateTo(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
// Create some mock entries in the filesystem
|
||||||
|
now := time.Now()
|
||||||
|
fs.AddDir("/test/docs", now)
|
||||||
|
fs.AddFile("/test/docs/file1.txt", []byte("content1"), now)
|
||||||
|
fs.AddFile("/test/docs/file2.txt", []byte("content2"), now)
|
||||||
|
fs.AddDir("/test/docs/subdir", now)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to a directory
|
||||||
|
bm.NavigateTo("/test/docs")
|
||||||
|
|
||||||
|
// Verify state was reset
|
||||||
|
if state.CurrentPath != "/test/docs" {
|
||||||
|
t.Errorf("CurrentPath = %q, want %q", state.CurrentPath, "/test/docs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.ScrollOffset != 0 {
|
||||||
|
t.Errorf("ScrollOffset = %f, want 0", state.ScrollOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.SelectedIndex != -1 {
|
||||||
|
t.Errorf("SelectedIndex = %d, want -1", state.SelectedIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify a task was dispatched (should have pending work)
|
||||||
|
// The manager should have queued a BuildIndexTask
|
||||||
|
if !state.Loading {
|
||||||
|
t.Error("Expected Loading to be true after NavigateTo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNavigateTo_NonExistentDirectory verifies behavior when navigating
|
||||||
|
// to a directory that doesn't exist in the filesystem.
|
||||||
|
func TestNavigateTo_NonExistentDirectory(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to non-existent directory
|
||||||
|
bm.NavigateTo("/non/existent/path")
|
||||||
|
|
||||||
|
// Should still set the path (error handling happens async)
|
||||||
|
if state.CurrentPath != "/non/existent/path" {
|
||||||
|
t.Errorf("CurrentPath = %q, want %q", state.CurrentPath, "/non/existent/path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleResult_BuildIndexSuccess verifies that a successful BuildIndex
|
||||||
|
// result populates the SortIndex and triggers initial page loading.
|
||||||
|
func TestHandleResult_BuildIndexSuccess(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate a successful BuildIndex result
|
||||||
|
result := pool.Result{
|
||||||
|
TaskType: pool.TypeBuildIndex,
|
||||||
|
Success: true,
|
||||||
|
Data: []mock.DirEntry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
bm.HandleResult(result)
|
||||||
|
|
||||||
|
// After successful BuildIndex, Loading should be false
|
||||||
|
if state.Loading {
|
||||||
|
t.Error("Expected Loading to be false after successful BuildIndex result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleResult_BuildIndexFailure verifies that a failed BuildIndex
|
||||||
|
// result is handled gracefully.
|
||||||
|
func TestHandleResult_BuildIndexFailure(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate a failed BuildIndex result
|
||||||
|
result := pool.Result{
|
||||||
|
TaskType: pool.TypeBuildIndex,
|
||||||
|
Success: false,
|
||||||
|
Error: ErrDirNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
bm.HandleResult(result)
|
||||||
|
|
||||||
|
// Should not panic, state should be recoverable
|
||||||
|
if state.CurrentPath != "" {
|
||||||
|
t.Errorf("Expected empty CurrentPath on error, got %q", state.CurrentPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleResult_LoadPagesSuccess verifies that a successful LoadPages
|
||||||
|
// result populates the Pages map.
|
||||||
|
func TestHandleResult_LoadPagesSuccess(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
state.TotalEntries = 500
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate a successful LoadPages result
|
||||||
|
pageIndices := []int{0, 1}
|
||||||
|
result := pool.Result{
|
||||||
|
TaskType: pool.TypeLoadPages,
|
||||||
|
Success: true,
|
||||||
|
Data: pageIndices,
|
||||||
|
}
|
||||||
|
|
||||||
|
bm.HandleResult(result)
|
||||||
|
|
||||||
|
// Pages should be populated (or at least not cause a panic)
|
||||||
|
// The exact behavior depends on implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOnScroll_Prefetch verifies that scrolling triggers prefetch for
|
||||||
|
// pages beyond the visible region.
|
||||||
|
func TestOnScroll_Prefetch(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
state.TotalEntries = 1000
|
||||||
|
state.EntryHeight = 48.0
|
||||||
|
state.VisibleCount = 10
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial scroll position
|
||||||
|
state.ScrollOffset = 0
|
||||||
|
|
||||||
|
// Simulate scrolling down
|
||||||
|
state.ScrollOffset = 5000.0 // ~100 entries down
|
||||||
|
|
||||||
|
bm.OnScroll()
|
||||||
|
|
||||||
|
// Should trigger prefetch (exact behavior depends on implementation)
|
||||||
|
// At minimum, should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOnScroll_Eviction verifies that scrolling triggers eviction of
|
||||||
|
// pages far from the visible region.
|
||||||
|
func TestOnScroll_Eviction(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
state.TotalEntries = 1000
|
||||||
|
state.EntryHeight = 48.0
|
||||||
|
state.VisibleCount = 10
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-populate some pages
|
||||||
|
state.Pages[0] = NewPage(0, make([]Entry, 100))
|
||||||
|
state.Pages[5] = NewPage(5, make([]Entry, 100))
|
||||||
|
state.Pages[10] = NewPage(10, make([]Entry, 100))
|
||||||
|
|
||||||
|
// Scroll far down
|
||||||
|
state.ScrollOffset = 50000.0 // ~1000 entries down
|
||||||
|
|
||||||
|
bm.OnScroll()
|
||||||
|
|
||||||
|
// Pages far from visible region should be evicted
|
||||||
|
// (exact behavior depends on implementation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBrowserManager_StateOwnership verifies that the BrowserManager
|
||||||
|
// does not directly modify state outside of designated methods.
|
||||||
|
func TestBrowserManager_StateOwnership(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the manager has a reference to the state
|
||||||
|
// but does not own it (state is owned by the editor)
|
||||||
|
if bm.state != state {
|
||||||
|
t.Error("BrowserManager should have reference to BrowserState")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNavigateTo_ChangesDirectory verifies that navigating to a new
|
||||||
|
// directory clears the previous directory's state.
|
||||||
|
func TestNavigateTo_ChangesDirectory(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
state.CurrentPath = "/old/path"
|
||||||
|
state.TotalEntries = 500
|
||||||
|
state.Pages[0] = NewPage(0, make([]Entry, 100))
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to new directory
|
||||||
|
bm.NavigateTo("/new/path")
|
||||||
|
|
||||||
|
if state.CurrentPath != "/new/path" {
|
||||||
|
t.Errorf("CurrentPath = %q, want %q", state.CurrentPath, "/new/path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages should be cleared for new directory
|
||||||
|
if len(state.Pages) != 0 {
|
||||||
|
t.Errorf("Expected Pages to be cleared, got %d pages", len(state.Pages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleResult_UnknownType verifies that unknown result types
|
||||||
|
// are handled gracefully without panicking.
|
||||||
|
func TestHandleResult_UnknownType(t *testing.T) {
|
||||||
|
state := NewBrowserState()
|
||||||
|
fs := mock.NewFileSystem()
|
||||||
|
wp := pool.NewWorkerPool(1)
|
||||||
|
|
||||||
|
bm, err := NewBrowserManager(state, wp, fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBrowserManager failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate an unknown result type
|
||||||
|
result := pool.Result{
|
||||||
|
TaskType: pool.TypeReadFile, // Not a browser task
|
||||||
|
Success: true,
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not panic
|
||||||
|
bm.HandleResult(result)
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,7 @@ type ResultEvent struct {
|
||||||
// Logic runs the logic goroutine and provides channels for communication.
|
// Logic runs the logic goroutine and provides channels for communication.
|
||||||
type Logic struct {
|
type Logic struct {
|
||||||
state *State
|
state *State
|
||||||
|
browserManager *browser.BrowserManager // Add this field
|
||||||
configChan chan ConfigUpdate
|
configChan chan ConfigUpdate
|
||||||
frameChan chan []ui.Element
|
frameChan chan []ui.Element
|
||||||
inputChan chan []ui.InputEvent
|
inputChan chan []ui.InputEvent
|
||||||
|
|
@ -71,9 +72,11 @@ func NewLogic() *Logic {
|
||||||
|
|
||||||
// Set browser initial path to mock root
|
// Set browser initial path to mock root
|
||||||
state.Browser.CurrentPath = "/"
|
state.Browser.CurrentPath = "/"
|
||||||
|
bm, _ := browser.NewBrowserManager(&state.Browser, wp, mockFS)
|
||||||
|
|
||||||
return &Logic{
|
return &Logic{
|
||||||
state: state,
|
state: state,
|
||||||
|
browserManager: bm, // Add to struct
|
||||||
configChan: make(chan ConfigUpdate),
|
configChan: make(chan ConfigUpdate),
|
||||||
frameChan: make(chan []ui.Element),
|
frameChan: make(chan []ui.Element),
|
||||||
inputChan: make(chan []ui.InputEvent),
|
inputChan: make(chan []ui.InputEvent),
|
||||||
|
|
@ -168,40 +171,25 @@ func (l *Logic) Run() {
|
||||||
|
|
||||||
// handleWorkerResult processes results from the worker pool.
|
// handleWorkerResult processes results from the worker pool.
|
||||||
func (l *Logic) handleWorkerResult(res pool.Result) {
|
func (l *Logic) handleWorkerResult(res pool.Result) {
|
||||||
switch res.TaskType {
|
if res.IsBrowserResult() {
|
||||||
case pool.TypeBuildIndex:
|
l.browserManager.HandleResult(res)
|
||||||
l.applyBuildIndexResult(res)
|
|
||||||
case pool.TypeLoadPages:
|
|
||||||
l.applyLoadPagesResult(res)
|
|
||||||
case pool.TypeReadDir:
|
|
||||||
l.applyReadDirResult(res)
|
|
||||||
}
|
}
|
||||||
l.frameChan <- l.state.layout()
|
l.frameChan <- l.state.layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyBuildIndexResult applies a completed BuildIndexTask result to browser state.
|
// applyBuildIndexResult applies a completed BuildIndexTask result to browser state.
|
||||||
func (l *Logic) applyBuildIndexResult(res pool.Result) {
|
func (l *Logic) applyBuildIndexResult(res pool.Result) {
|
||||||
if !res.Success {
|
// Delegated to browserManager
|
||||||
return
|
|
||||||
}
|
|
||||||
// Convert mock DirEntry results to browser entries
|
|
||||||
if entries, ok := res.Data.([]mock.DirEntry); ok {
|
|
||||||
l.state.Browser.TotalEntries = len(entries)
|
|
||||||
// Build the in-memory index with position maps
|
|
||||||
l.state.Browser.SortIndex = buildBrowserIndex(entries)
|
|
||||||
// Load initial visible pages from the index into the Pages map
|
|
||||||
browser.LoadInitialPages(&l.state.Browser)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyLoadPagesResult applies a completed LoadPagesTask result to browser state.
|
// applyLoadPagesResult applies a completed LoadPagesTask result to browser state.
|
||||||
func (l *Logic) applyLoadPagesResult(res pool.Result) {
|
func (l *Logic) applyLoadPagesResult(res pool.Result) {
|
||||||
_ = res // TODO: implement page loading from worker results
|
// Delegated to browserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyReadDirResult applies a completed ReadDirTask result to browser state.
|
// applyReadDirResult applies a completed ReadDirTask result to browser state.
|
||||||
func (l *Logic) applyReadDirResult(res pool.Result) {
|
func (l *Logic) applyReadDirResult(res pool.Result) {
|
||||||
_ = res // TODO: implement directory read result handling
|
// Delegated to browserManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// State returns the current state.
|
// State returns the current state.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user