Pad/internal/browser/sort_test.go
Greg Pomerantz 6a43e3c0db Add e2e test harness for testing editor logic without Gio display
- internal/test/e2e/: FrameCapture, Harness, ElementAssertions, helpers
- internal/editor/logic.go: Add done channel and Done() method for graceful shutdown
- internal/editor/mock_setup.go: Mock filesystem for tests
- internal/browser/: Browser layout and search logic
- internal/io/: Worker pool for async tasks
- Update architecture docs and spec
2026-05-31 08:56:30 -04:00

304 lines
9.3 KiB
Go

package browser
import (
"fmt"
"testing"
"time"
)
// --- Test helpers ---
func newUnsortedEntries() []Entry {
return []Entry{
{Name: "Zebra.txt", Size: 1000, ModTime: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC), IsDir: false},
{Name: "apple.txt", Size: 500, ModTime: time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC), IsDir: false},
{Name: "MyDir", Size: 0, ModTime: time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC), IsDir: true},
{Name: "banana.txt", Size: 2000, ModTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), IsDir: false},
{Name: "Cherry.txt", Size: 1500, ModTime: time.Date(2024, 2, 20, 0, 0, 0, 0, time.UTC), IsDir: false},
{Name: "adir", Size: 0, ModTime: time.Date(2024, 1, 10, 0, 0, 0, 0, time.UTC), IsDir: true},
{Name: "123.txt", Size: 300, ModTime: time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC), IsDir: false},
}
}
// --- Sort Mode: Name Ascending ---
func TestSort_NameAscending(t *testing.T) {
entries := newUnsortedEntries()
sortEntries(entries, SortModeNameAsc)
// Directories and files interleaved by name
// Expected: 123.txt, adir, apple.txt, banana.txt, Cherry.txt, MyDir, Zebra.txt
wantNames := []string{"123.txt", "adir", "apple.txt", "banana.txt", "Cherry.txt", "MyDir", "Zebra.txt"}
for i, want := range wantNames {
if entries[i].Name != want {
t.Errorf("entries[%d].Name = %q, want %q", i, entries[i].Name, want)
}
}
}
func TestSort_NameAscending_DirsInterleaved(t *testing.T) {
entries := newUnsortedEntries()
sortEntries(entries, SortModeNameAsc)
// Directories should be interleaved with files at their alphabetical position
// adir should be between 123.txt and apple.txt
adirIdx := -1
for i, e := range entries {
if e.Name == "adir" {
adirIdx = i
break
}
}
if adirIdx <= 0 || adirIdx >= len(entries)-1 {
t.Errorf("adir at idx %d should be between 123.txt and apple.txt", adirIdx)
}
// Verify neighbors
if entries[adirIdx-1].Name != "123.txt" {
t.Errorf("expected 123.txt before adir, got %s", entries[adirIdx-1].Name)
}
if entries[adirIdx+1].Name != "apple.txt" {
t.Errorf("expected apple.txt after adir, got %s", entries[adirIdx+1].Name)
}
}
func TestSort_NameAscending_CaseInsensitive(t *testing.T) {
entries := newUnsortedEntries()
sortEntries(entries, SortModeNameAsc)
// "apple.txt" should come before "Cherry.txt" (case-insensitive)
appleIdx := -1
cherryIdx := -1
for i, e := range entries {
if e.Name == "apple.txt" {
appleIdx = i
}
if e.Name == "Cherry.txt" {
cherryIdx = i
}
}
if appleIdx >= cherryIdx {
t.Errorf("apple.txt (idx %d) should come before Cherry.txt (idx %d)", appleIdx, cherryIdx)
}
}
func TestSort_NameAscending_NumericFirst(t *testing.T) {
entries := newUnsortedEntries()
sortEntries(entries, SortModeNameAsc)
// "123.txt" should be the first entry (numbers sort before letters)
if entries[0].Name != "123.txt" {
t.Errorf("expected 123.txt first, got %s", entries[0].Name)
}
}
// --- Sort Mode: Name Descending ---
func TestSort_NameDescending(t *testing.T) {
entries := newUnsortedEntries()
sortEntries(entries, SortModeNameDesc)
// Directories and files interleaved by name (reverse)
// Expected: Zebra.txt, MyDir, Cherry.txt, banana.txt, apple.txt, adir, 123.txt
wantNames := []string{"Zebra.txt", "MyDir", "Cherry.txt", "banana.txt", "apple.txt", "adir", "123.txt"}
for i, want := range wantNames {
if entries[i].Name != want {
t.Errorf("entries[%d].Name = %q, want %q", i, entries[i].Name, want)
}
}
}
// --- Sort Mode: Date Ascending ---
func TestSort_DateAscending(t *testing.T) {
entries := newUnsortedEntries()
sortEntries(entries, SortModeDateAsc)
// Directories and files interleaved by date
// banana.txt: 2024-01-01, adir: 2024-01-10, Zebra.txt: 2024-01-15, MyDir: 2024-02-10, Cherry.txt: 2024-02-20, apple.txt: 2024-03-01, 123.txt: 2024-04-01
wantNames := []string{"banana.txt", "adir", "Zebra.txt", "MyDir", "Cherry.txt", "apple.txt", "123.txt"}
for i, want := range wantNames {
if entries[i].Name != want {
t.Errorf("entries[%d].Name = %q, want %q", i, entries[i].Name, want)
}
}
}
func TestSort_DateAscending_DirsInterleaved(t *testing.T) {
entries := newUnsortedEntries()
sortEntries(entries, SortModeDateAsc)
// Directories should be interleaved with files at their date position
// adir (2024-01-10) should be between banana.txt (2024-01-01) and Zebra.txt (2024-01-15)
adirIdx := -1
for i, e := range entries {
if e.Name == "adir" {
adirIdx = i
break
}
}
if adirIdx <= 0 || adirIdx >= len(entries)-1 {
t.Errorf("adir at idx %d should be between banana.txt and Zebra.txt", adirIdx)
}
if entries[adirIdx-1].Name != "banana.txt" {
t.Errorf("expected banana.txt before adir, got %s", entries[adirIdx-1].Name)
}
if entries[adirIdx+1].Name != "Zebra.txt" {
t.Errorf("expected Zebra.txt after adir, got %s", entries[adirIdx+1].Name)
}
}
// --- Sort Mode: Date Descending ---
func TestSort_DateDescending(t *testing.T) {
entries := newUnsortedEntries()
sortEntries(entries, SortModeDateDesc)
// Directories and files interleaved by date (reverse)
wantNames := []string{"123.txt", "apple.txt", "Cherry.txt", "MyDir", "Zebra.txt", "adir", "banana.txt"}
for i, want := range wantNames {
if entries[i].Name != want {
t.Errorf("entries[%d].Name = %q, want %q", i, entries[i].Name, want)
}
}
}
// --- Sort Mode: Size Ascending (REMOVED - only 4 sort modes: name +/-, date +/-) ---
// Size-based sorting has been removed from the browser to keep sort modes minimal.
// The sortEntries function still exists for potential future use.
// --- Sort Mode: Size Descending (REMOVED - only 4 sort modes: name +/-, date +/-) ---
// Size-based sorting has been removed from the browser to keep sort modes minimal.
// The sortEntries function still exists for potential future use.
// --- Edge Cases ---
func TestSort_EmptySlice(t *testing.T) {
var entries []Entry
sortEntries(entries, SortModeNameAsc)
if len(entries) != 0 {
t.Errorf("expected empty slice, got %d entries", len(entries))
}
}
func TestSort_SingleEntry(t *testing.T) {
entries := []Entry{{Name: "only.txt", Size: 100, IsDir: false}}
sortEntries(entries, SortModeNameAsc)
if len(entries) != 1 || entries[0].Name != "only.txt" {
t.Errorf("single entry sort failed: %v", entries)
}
}
func TestSort_AllDirectories(t *testing.T) {
entries := []Entry{
{Name: "Zdir", IsDir: true},
{Name: "adir", IsDir: true},
{Name: "Mdir", IsDir: true},
}
sortEntries(entries, SortModeNameAsc)
wantNames := []string{"adir", "Mdir", "Zdir"}
for i, want := range wantNames {
if entries[i].Name != want {
t.Errorf("entries[%d].Name = %q, want %q", i, entries[i].Name, want)
}
}
}
func TestSort_AllFiles(t *testing.T) {
entries := []Entry{
{Name: "Zebra.txt", IsDir: false},
{Name: "apple.txt", IsDir: false},
{Name: "Mango.txt", IsDir: false},
}
sortEntries(entries, SortModeNameAsc)
wantNames := []string{"apple.txt", "Mango.txt", "Zebra.txt"}
for i, want := range wantNames {
if entries[i].Name != want {
t.Errorf("entries[%d].Name = %q, want %q", i, entries[i].Name, want)
}
}
}
func TestSort_StableEqualKeys(t *testing.T) {
// When two entries have equal sort keys, order should be deterministic
entries := []Entry{
{Name: "bbb.txt", Size: 100, ModTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), IsDir: false},
{Name: "aaa.txt", Size: 100, ModTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), IsDir: false},
}
sortEntries(entries, SortModeNameAsc)
// Same name — should maintain stable order
if entries[0].Name != "aaa.txt" {
t.Errorf("expected aaa.txt first on equal name, got %s", entries[0].Name)
}
}
func TestSort_NoSideEffectsOnOriginalOrder(t *testing.T) {
// Sorting should not corrupt entry data
entries := newUnsortedEntries()
originalNames := make([]string, len(entries))
for i, e := range entries {
originalNames[i] = e.Name
}
sortEntries(entries, SortModeNameAsc)
// All original entries should still be present
names := make(map[string]bool)
for _, e := range entries {
names[e.Name] = true
}
for _, name := range originalNames {
if !names[name] {
t.Errorf("entry %q lost during sort", name)
}
}
}
func TestSort_InvalidMode(t *testing.T) {
entries := newUnsortedEntries()
originalLen := len(entries)
// Invalid mode should not panic and should leave entries intact
sortEntries(entries, SortMode(-99))
if len(entries) != originalLen {
t.Errorf("invalid mode changed entry count: %d -> %d", originalLen, len(entries))
}
}
func TestSort_LargeSlice(t *testing.T) {
const count = 10000
entries := make([]Entry, count)
for i := 0; i < count; i++ {
entries[i] = Entry{
Name: fmt.Sprintf("file_%05d.txt", i),
Size: int64(i * 10),
ModTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).Add(time.Duration(i) * time.Hour),
IsDir: i%100 == 0,
}
}
start := time.Now()
sortEntries(entries, SortModeNameAsc)
elapsed := time.Since(start)
if elapsed > 5*time.Second {
t.Errorf("sort took %v, expected < 5s", elapsed)
}
// Verify directories are interleaved (not separated)
// With 100 entries per 100 entries being dirs, they should be mixed
hasDirBeforeFile := false
hasFileBeforeDir := false
for i := 0; i < len(entries)-1; i++ {
if entries[i].IsDir && !entries[i+1].IsDir {
hasDirBeforeFile = true
}
if !entries[i].IsDir && entries[i+1].IsDir {
hasFileBeforeDir = true
}
}
if !hasDirBeforeFile || !hasFileBeforeDir {
t.Errorf("directories should be interleaved with files, not separated")
}
}