- 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
304 lines
9.3 KiB
Go
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")
|
|
}
|
|
}
|