The tree was formatted with an older gofmt; go1.27's gofmt additionally wants: EOF exactly one newline (no trailing blank lines), imports sorted alphabetically within a block, mixed-precedence binary expressions re-spaced for grouping ((a+b)/c), single-field composite literals un-aligned, adjacent one-line method signatures aligned, and one-line bodies containing a compound statement expanded. Applied repo-wide (31 files under internal/); pure formatting, no semantic changes — build and the full test suite pass.
452 lines
11 KiB
Go
452 lines
11 KiB
Go
package browser
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// --- Test: Load Page From Index At Offset ---
|
|
|
|
func TestLoadPage_FromIndexAtOffset(t *testing.T) {
|
|
// Create a BrowserState with a populated index
|
|
s := NewBrowserState()
|
|
s.TotalEntries = 250
|
|
|
|
// Simulate an index with 250 entries (3 pages)
|
|
// Use zero-padded names so lexicographic order matches numeric order
|
|
allEntries := make([]Entry, 250)
|
|
for i := 0; i < 250; i++ {
|
|
allEntries[i] = NewEntry(
|
|
fmt.Sprintf("/path/file%03d.txt", i),
|
|
fmt.Sprintf("file%03d.txt", i),
|
|
int64(i*100),
|
|
time.Time{},
|
|
false,
|
|
)
|
|
}
|
|
|
|
// Build position maps for sort modes
|
|
s.SortIndex = &DirectoryIndex{
|
|
Entries: allEntries,
|
|
SortOrders: map[string][]int{
|
|
"name_asc": buildPositionMap(allEntries, SortModeNameAsc),
|
|
"date_desc": buildPositionMap(allEntries, SortModeDateDesc),
|
|
},
|
|
}
|
|
|
|
// Load page 1 (entries 100-199)
|
|
page := loadPageFromIndex(s, 1)
|
|
|
|
if page == nil {
|
|
t.Fatal("expected page to be loaded, got nil")
|
|
}
|
|
if page.Index != 1 {
|
|
t.Errorf("expected Index=1, got %d", page.Index)
|
|
}
|
|
if len(page.Entries) != PageSize {
|
|
t.Errorf("expected %d entries, got %d", PageSize, len(page.Entries))
|
|
}
|
|
// First entry of page 1 should be file100.txt
|
|
if page.Entries[0].Name != "file100.txt" {
|
|
t.Errorf("expected first entry name=file100.txt, got %s", page.Entries[0].Name)
|
|
}
|
|
// Last entry of page 1 should be file199.txt
|
|
if page.Entries[99].Name != "file199.txt" {
|
|
t.Errorf("expected last entry name=file199.txt, got %s", page.Entries[99].Name)
|
|
}
|
|
}
|
|
|
|
// --- Test: Load Page - Last Page Partial ---
|
|
|
|
func TestLoadPage_LastPagePartialFromIndex(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.TotalEntries = 150 // 1 full page + 50 entries in page 1
|
|
|
|
allEntries := make([]Entry, 150)
|
|
for i := 0; i < 150; i++ {
|
|
allEntries[i] = NewEntry(
|
|
fmt.Sprintf("/path/file%d.txt", i),
|
|
fmt.Sprintf("file%d.txt", i),
|
|
int64(i*100),
|
|
time.Time{},
|
|
false,
|
|
)
|
|
}
|
|
|
|
// Build position maps for sort modes
|
|
s.SortIndex = &DirectoryIndex{
|
|
Entries: allEntries,
|
|
SortOrders: map[string][]int{
|
|
"name_asc": buildPositionMap(allEntries, SortModeNameAsc),
|
|
"date_desc": buildPositionMap(allEntries, SortModeDateDesc),
|
|
},
|
|
}
|
|
|
|
// Load page 1 (last page, should have only 50 entries)
|
|
page := loadPageFromIndex(s, 1)
|
|
|
|
if page == nil {
|
|
t.Fatal("expected page to be loaded, got nil")
|
|
}
|
|
if page.Index != 1 {
|
|
t.Errorf("expected Index=1, got %d", page.Index)
|
|
}
|
|
if len(page.Entries) != 50 {
|
|
t.Errorf("expected 50 entries, got %d", len(page.Entries))
|
|
}
|
|
}
|
|
|
|
// --- Test: Load Page - Out Of Bounds ---
|
|
|
|
func TestLoadPage_OutOfBoundsFromIndex(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.TotalEntries = 150
|
|
|
|
allEntries := make([]Entry, 150)
|
|
for i := 0; i < 150; i++ {
|
|
allEntries[i] = NewEntry(
|
|
fmt.Sprintf("/path/file%d.txt", i),
|
|
fmt.Sprintf("file%d.txt", i),
|
|
int64(i*100),
|
|
time.Time{},
|
|
false,
|
|
)
|
|
}
|
|
|
|
// Build position maps for sort modes
|
|
s.SortIndex = &DirectoryIndex{
|
|
Entries: allEntries,
|
|
SortOrders: map[string][]int{
|
|
"name_asc": buildPositionMap(allEntries, SortModeNameAsc),
|
|
"date_desc": buildPositionMap(allEntries, SortModeDateDesc),
|
|
},
|
|
}
|
|
|
|
// Requesting page 5 (out of bounds) should return nil
|
|
page := loadPageFromIndex(s, 5)
|
|
|
|
if page != nil {
|
|
t.Errorf("expected nil for out-of-bounds page, got page with %d entries", len(page.Entries))
|
|
}
|
|
}
|
|
|
|
// --- Test: Compute Visible Page Range ---
|
|
|
|
func TestComputeVisiblePageRange(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.EntryHeight = 48.0
|
|
s.ScrollOffset = 250 * 48 // 250 entries worth of pixels
|
|
s.VisibleCount = 20
|
|
|
|
minPage, maxPage := computeVisiblePageRange(s)
|
|
|
|
// ScrollOffset=250*48 → GetScrollIndex()=250 → page 2
|
|
// VisibleCount=20 → entries 250-269 → pages 2-2 (partial)
|
|
// With prefetch: min=0, max=4
|
|
expectedMin := 0 // (250/100) - 2 = 0
|
|
expectedMax := 4 // (270/100) + 2 = 4
|
|
|
|
if minPage != expectedMin {
|
|
t.Errorf("expected minPage=%d, got %d", expectedMin, minPage)
|
|
}
|
|
if maxPage != expectedMax {
|
|
t.Errorf("expected maxPage=%d, got %d", expectedMax, maxPage)
|
|
}
|
|
}
|
|
|
|
// --- Test: Compute Visible Page Range - At Start ---
|
|
|
|
func TestComputeVisiblePageRange_AtStart(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.EntryHeight = 48.0
|
|
s.ScrollOffset = 0
|
|
s.VisibleCount = 10
|
|
|
|
minPage, maxPage := computeVisiblePageRange(s)
|
|
|
|
// Should clamp min to 0
|
|
if minPage != 0 {
|
|
t.Errorf("expected minPage=0, got %d", minPage)
|
|
}
|
|
// maxPage should be 0 + 2 = 2
|
|
if maxPage != 2 {
|
|
t.Errorf("expected maxPage=2, got %d", maxPage)
|
|
}
|
|
}
|
|
|
|
// --- Test: Get Entry By Index ---
|
|
|
|
func TestGetEntryByIndex(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.TotalEntries = 250
|
|
|
|
// Use zero-padded names so lexicographic order matches numeric order
|
|
allEntries := make([]Entry, 250)
|
|
for i := 0; i < 250; i++ {
|
|
allEntries[i] = NewEntry(
|
|
fmt.Sprintf("/path/file%03d.txt", i),
|
|
fmt.Sprintf("file%03d.txt", i),
|
|
int64(i*100),
|
|
time.Time{},
|
|
false,
|
|
)
|
|
}
|
|
|
|
// Build position maps for sort modes
|
|
s.SortIndex = &DirectoryIndex{
|
|
Entries: allEntries,
|
|
SortOrders: map[string][]int{
|
|
"name_asc": buildPositionMap(allEntries, SortModeNameAsc),
|
|
"date_desc": buildPositionMap(allEntries, SortModeDateDesc),
|
|
},
|
|
}
|
|
|
|
// Load some pages
|
|
s.Pages[0] = loadPageFromIndex(s, 0)
|
|
s.Pages[1] = loadPageFromIndex(s, 1)
|
|
|
|
// Get entry at index 150 (page 1, offset 50)
|
|
entry, ok := getEntryByIndex(s, 150)
|
|
|
|
if !ok {
|
|
t.Fatal("expected entry to be found")
|
|
}
|
|
if entry.Name != "file150.txt" {
|
|
t.Errorf("expected entry name=file150.txt, got %s", entry.Name)
|
|
}
|
|
}
|
|
|
|
// --- Test: Get Entry By Index - Unloaded Page ---
|
|
|
|
func TestGetEntryByIndex_UnloadedPage(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.TotalEntries = 250
|
|
|
|
allEntries := make([]Entry, 250)
|
|
for i := 0; i < 250; i++ {
|
|
allEntries[i] = NewEntry(
|
|
fmt.Sprintf("/path/file%d.txt", i),
|
|
fmt.Sprintf("file%d.txt", i),
|
|
int64(i*100),
|
|
time.Time{},
|
|
false,
|
|
)
|
|
}
|
|
|
|
// Build position maps for sort modes
|
|
s.SortIndex = &DirectoryIndex{
|
|
Entries: allEntries,
|
|
SortOrders: map[string][]int{
|
|
"name_asc": buildPositionMap(allEntries, SortModeNameAsc),
|
|
"date_desc": buildPositionMap(allEntries, SortModeDateDesc),
|
|
},
|
|
}
|
|
|
|
// Only load page 0
|
|
s.Pages[0] = loadPageFromIndex(s, 0)
|
|
|
|
// Try to get entry at index 150 (page 1, not loaded)
|
|
_, ok := getEntryByIndex(s, 150)
|
|
|
|
if ok {
|
|
t.Error("expected entry not to be found for unloaded page")
|
|
}
|
|
}
|
|
|
|
// --- Test: Get Entry By Index - Out Of Bounds ---
|
|
|
|
func TestGetEntryByIndex_OutOfBounds(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.TotalEntries = 100
|
|
|
|
_, ok := getEntryByIndex(s, 200)
|
|
|
|
if ok {
|
|
t.Error("expected entry not to be found for out-of-bounds index")
|
|
}
|
|
}
|
|
|
|
// --- Test: Needs Prefetch ---
|
|
|
|
func TestNeedsPrefetch(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.EntryHeight = 48.0
|
|
s.ScrollOffset = 500 * 48 // 500 entries worth of pixels
|
|
s.VisibleCount = 20
|
|
s.TotalEntries = 2000
|
|
|
|
// Page 3 is within visible range + prefetch
|
|
needs, _ := needsPrefetch(s, 3)
|
|
if !needs {
|
|
t.Error("expected page 3 to need prefetch")
|
|
}
|
|
|
|
// Page 0 is far away, should not need prefetch
|
|
needs, _ = needsPrefetch(s, 0)
|
|
if needs {
|
|
t.Error("expected page 0 to not need prefetch")
|
|
}
|
|
|
|
// Page 25 is far away, should not need prefetch
|
|
needs, _ = needsPrefetch(s, 25)
|
|
if needs {
|
|
t.Error("expected page 25 to not need prefetch")
|
|
}
|
|
}
|
|
|
|
// --- Test: Navigate To Directory ---
|
|
|
|
func TestNavigateToDirectory(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.EntryHeight = 48.0
|
|
s.CurrentPath = "/old/path"
|
|
s.ScrollOffset = 100 * 48 // 100 entries worth of pixels
|
|
s.SelectedIndex = 5
|
|
s.Pages[0] = NewPage(0, nil)
|
|
|
|
navigateToDirectory(s, "/new/path")
|
|
|
|
if s.CurrentPath != "/new/path" {
|
|
t.Errorf("expected CurrentPath=/new/path, got %s", s.CurrentPath)
|
|
}
|
|
if s.ScrollOffset != 0 {
|
|
t.Errorf("expected ScrollOffset=0, got %.1f", s.ScrollOffset)
|
|
}
|
|
if len(s.Pages) != 0 {
|
|
t.Errorf("expected Pages to be empty, got %d", len(s.Pages))
|
|
}
|
|
}
|
|
|
|
// --- Test: Compute Total Pages ---
|
|
|
|
func TestComputeTotalPages(t *testing.T) {
|
|
s := NewBrowserState()
|
|
|
|
s.TotalEntries = 0
|
|
if pages := computeTotalPages(s); pages != 0 {
|
|
t.Errorf("expected 0 pages for 0 entries, got %d", pages)
|
|
}
|
|
|
|
s.TotalEntries = 100
|
|
if pages := computeTotalPages(s); pages != 1 {
|
|
t.Errorf("expected 1 page for 100 entries, got %d", pages)
|
|
}
|
|
|
|
s.TotalEntries = 101
|
|
if pages := computeTotalPages(s); pages != 2 {
|
|
t.Errorf("expected 2 pages for 101 entries, got %d", pages)
|
|
}
|
|
|
|
s.TotalEntries = 250
|
|
if pages := computeTotalPages(s); pages != 3 {
|
|
t.Errorf("expected 3 pages for 250 entries, got %d", pages)
|
|
}
|
|
}
|
|
|
|
// --- Test: Clamp Scroll Index ---
|
|
|
|
func TestClampScrollOffset(t *testing.T) {
|
|
s := NewBrowserState()
|
|
s.TotalEntries = 500
|
|
s.VisibleCount = 20
|
|
s.EntryHeight = 48.0
|
|
|
|
// Normal case
|
|
s.ScrollOffset = 100 * 48
|
|
clampScrollOffset(s)
|
|
expected := float64(100 * 48)
|
|
if s.ScrollOffset != expected {
|
|
t.Errorf("expected ScrollOffset=%.1f, got %.1f", expected, s.ScrollOffset)
|
|
}
|
|
|
|
// Below minimum
|
|
s.ScrollOffset = -10 * 48
|
|
clampScrollOffset(s)
|
|
if s.ScrollOffset != 0 {
|
|
t.Errorf("expected ScrollOffset=0, got %.1f", s.ScrollOffset)
|
|
}
|
|
|
|
// Above maximum
|
|
s.ScrollOffset = 1000 * 48
|
|
clampScrollOffset(s)
|
|
maxScroll := float64(500-20) * 48
|
|
if s.ScrollOffset != maxScroll {
|
|
t.Errorf("expected ScrollOffset=%.1f, got %.1f", maxScroll, s.ScrollOffset)
|
|
}
|
|
}
|
|
|
|
// --- Test: Filter Entries By Query ---
|
|
|
|
func TestFilterEntriesByQuery(t *testing.T) {
|
|
entries := []Entry{
|
|
NewEntry("/path/file0.txt", "file0.txt", 0, time.Time{}, false),
|
|
NewEntry("/path/file1.txt", "file1.txt", 100, time.Time{}, false),
|
|
NewEntry("/path/file2.txt", "file2.txt", 200, time.Time{}, false),
|
|
NewEntry("/path/file10.txt", "file10.txt", 1000, time.Time{}, false),
|
|
NewEntry("/path/file100.txt", "file100.txt", 10000, time.Time{}, false),
|
|
}
|
|
|
|
// Search for "file1" - should match file1.txt, file10.txt, file100.txt
|
|
results := filterEntriesByQuery(entries, "file1")
|
|
|
|
if len(results) != 3 {
|
|
t.Errorf("expected 3 results for 'file1', got %d", len(results))
|
|
}
|
|
|
|
expectedNames := map[string]bool{"file1.txt": true, "file10.txt": true, "file100.txt": true}
|
|
for _, idx := range results {
|
|
if !expectedNames[entries[idx].Name] {
|
|
t.Errorf("unexpected result: %s", entries[idx].Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Test: Filter Entries By Query - Empty Query ---
|
|
|
|
func TestFilterEntriesByQuery_EmptyQuery(t *testing.T) {
|
|
entries := []Entry{
|
|
NewEntry("/a", "a.txt", 100, time.Time{}, false),
|
|
NewEntry("/b", "b.txt", 200, time.Time{}, false),
|
|
}
|
|
|
|
results := filterEntriesByQuery(entries, "")
|
|
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results for empty query, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
// --- Test: Filter Entries By Query - No Match ---
|
|
|
|
func TestFilterEntriesByQuery_NoMatch(t *testing.T) {
|
|
entries := []Entry{
|
|
NewEntry("/a", "a.txt", 100, time.Time{}, false),
|
|
NewEntry("/b", "b.txt", 200, time.Time{}, false),
|
|
}
|
|
|
|
results := filterEntriesByQuery(entries, "zzz")
|
|
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results for 'zzz', got %d", len(results))
|
|
}
|
|
}
|
|
|
|
// --- Test: Filter Entries By Query - Case Insensitive ---
|
|
|
|
func TestFilterEntriesByQuery_CaseInsensitive(t *testing.T) {
|
|
entries := []Entry{
|
|
NewEntry("/a", "Apple.txt", 100, time.Time{}, false),
|
|
NewEntry("/b", "banana.txt", 200, time.Time{}, false),
|
|
}
|
|
|
|
results := filterEntriesByQuery(entries, "apple")
|
|
|
|
if len(results) != 1 {
|
|
t.Errorf("expected 1 result for 'apple', got %d", len(results))
|
|
}
|
|
if results[0] != 0 {
|
|
t.Errorf("expected result at index 0, got %d", results[0])
|
|
}
|
|
}
|