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") } }