import { describe, test, expect, beforeEach, afterEach } from "bun:test"; import { mkdirSync, rmSync, existsSync } from "fs"; import * as fs from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { readDirectory, validateDirectoryPath, getCompletions, formatDisplayPath } from "./DirectoriesStep.tsx"; // Import the functions we want to test // Note: We'll need to export these from DirectoriesStep.tsx first describe("Directory Browser Utility Functions", () => { let testDir: string; function createTestDir() { testDir = join(tmpdir(), `gitforest-utils-test-${Date.now()}`); mkdirSync(testDir, { recursive: true }); // Create test structure mkdirSync(join(testDir, "projects")); mkdirSync(join(testDir, "code")); mkdirSync(join(testDir, "documents")); mkdirSync(join(testDir, "test-folder")); mkdirSync(join(testDir, ".hidden")); // Create files (should not appear in directory listing) Bun.write(join(testDir, "file.txt"), "test"); } function cleanupTestDir() { if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } } beforeEach(() => { createTestDir(); }); afterEach(() => { cleanupTestDir(); }); describe("readDirectory", () => { test("should return only directories, excluding files", () => { const dirs = readDirectory(testDir); const dirNames = dirs.map(d => d.name); // Should contain directories expect(dirNames).toContain("projects"); expect(dirNames).toContain("code"); expect(dirNames).toContain("documents"); expect(dirNames).toContain("test-folder"); // Should NOT contain files expect(dirNames).not.toContain("file.txt"); }); test("should exclude hidden directories starting with .", () => { const dirs = readDirectory(testDir); const dirNames = dirs.map(d => d.name); // Should not include .hidden expect(dirNames).not.toContain(".hidden"); }); test("should sort results alphabetically", () => { const dirs = readDirectory(testDir); const dirNames = dirs.map(d => d.name); // Check if sorted (code, documents, projects, test-folder) expect(dirNames[0]).toBe("code"); expect(dirNames[1]).toBe("documents"); expect(dirNames[2]).toBe("projects"); expect(dirNames[3]).toBe("test-folder"); }); test("should return empty array for non-existent directory", () => { const dirs = readDirectory("/non-existent-path-12345"); expect(dirs).toEqual([]); }); }); describe("validateDirectoryPath", () => { test("should return valid: true for existing directory", () => { const result = validateDirectoryPath(testDir); expect(result.valid).toBe(true); expect(result.error).toBeUndefined(); }); test("should return valid: false for non-existent path", () => { const result = validateDirectoryPath("/fake-path-12345"); expect(result.valid).toBe(false); expect(result.error).toBe("Path does not exist"); }); test("should return valid: false for file instead of directory", () => { const result = validateDirectoryPath(join(testDir, "file.txt")); expect(result.valid).toBe(false); expect(result.error).toBe("Not a directory"); }); test("should return valid: false for empty path", () => { const result = validateDirectoryPath(""); expect(result.valid).toBe(false); expect(result.error).toBe("Path is required"); }); test("should expand tilde before validating", () => { // After expansion, ~/ becomes the user's home dir (which always exists). const result = validateDirectoryPath("~/"); expect(result).toBeDefined(); }); }); describe("getCompletions", () => { test("should complete absolute paths starting with /", () => { const completions = getCompletions("/proj", testDir); // Should return nothing since we're not in root expect(Array.isArray(completions)).toBe(true); }); test("should complete relative paths from current directory", () => { const completions = getCompletions("proj", testDir); expect(completions.length).toBeGreaterThan(0); expect(completions[0]).toContain("projects"); }); test("should be case-insensitive for matching", () => { const completions = getCompletions("PROJ", testDir); expect(completions.length).toBeGreaterThan(0); expect(completions[0]).toContain("projects"); }); test("should strip trailing slash before processing", () => { // Test that /proj and /proj/ give same results const c1 = getCompletions("/proj", testDir); const c2 = getCompletions("/proj/", testDir); expect(c1).toEqual(c2); }); test("should return empty array for invalid base directory", () => { const completions = getCompletions("test", "/non-existent-12345"); expect(completions).toEqual([]); }); }); describe("formatDisplayPath", () => { test("should replace home directory prefix with ~", () => { const homeDir = require("os").homedir(); const testPath = join(homeDir, "projects"); const formatted = formatDisplayPath(testPath); expect(formatted).toBe("~/projects"); }); test("should not modify paths not in home directory", () => { const formatted = formatDisplayPath("/Volumes/Storage"); expect(formatted).toBe("/Volumes/Storage"); }); test("should handle paths that exactly equal home directory", () => { const homeDir = require("os").homedir(); const formatted = formatDisplayPath(homeDir); expect(formatted).toBe("~"); }); }); describe("Edge Cases", () => { test("should handle paths with trailing slash", () => { const result1 = validateDirectoryPath(join(testDir, "projects")); const result2 = validateDirectoryPath(join(testDir, "projects/")); // Both should be valid expect(result1.valid).toBe(true); expect(result2.valid).toBe(true); }); test("should handle case-insensitive folder filtering", () => { const completions = getCompletions("PROJ", testDir); expect(completions.length).toBeGreaterThan(0); expect(completions.some(c => c.includes("projects"))).toBe(true); }); test("should handle empty input for completions", () => { // Empty input from testDir should list all entries const completions = getCompletions("", testDir); expect(Array.isArray(completions)).toBe(true); }); test("should handle paths with spaces", () => { // Create a directory with spaces in the name const spacedDir = join(testDir, "folder with spaces"); mkdirSync(spacedDir, { recursive: true }); const dirs = readDirectory(testDir); const dirNames = dirs.map(d => d.name); expect(dirNames).toContain("folder with spaces"); // Validate the path with spaces const result = validateDirectoryPath(spacedDir); expect(result.valid).toBe(true); // Format display path should preserve spaces const formatted = formatDisplayPath(spacedDir); expect(formatted).toContain("folder with spaces"); }); test("should handle non-ascii characters in folder names", () => { // Create directories with non-ASCII characters const unicodeDir1 = join(testDir, "café"); const unicodeDir2 = join(testDir, "проект"); mkdirSync(unicodeDir1, { recursive: true }); mkdirSync(unicodeDir2, { recursive: true }); const dirs = readDirectory(testDir); const dirNames = dirs.map(d => d.name); expect(dirNames).toContain("café"); expect(dirNames).toContain("проект"); }); test("should handle symlinks to directories", () => { // Skip on Windows if symlinks require admin privileges if (process.platform === "win32") { // Symlink behavior varies on Windows expect(true).toBe(true); return; } // Create a target directory const targetDir = join(testDir, "target"); mkdirSync(targetDir, { recursive: true }); // Create a symlink to the directory const symlinkPath = join(testDir, "symlink-to-target"); try { fs.symlinkSync(targetDir, symlinkPath, "dir"); } catch (e) { // Symlink creation might fail due to permissions // Skip test if we can't create symlinks expect(true).toBe(true); return; } const dirs = readDirectory(testDir); const dirNames = dirs.map(d => d.name); // Symlink should appear in listing (it's a directory entry) expect(dirNames).toContain("symlink-to-target"); }); // These tests require specific platform or permission setups // Mark them as todo since they're environment-dependent test.todo("should handle very long paths gracefully", () => {}); test.todo("should handle paths with special characters", () => {}); test.todo("should handle permission denied errors gracefully", () => {}); }); }); describe("Browser State Management", () => { describe("input buffer changes", () => { test.todo("should reset selection when input changes", () => {}); test.todo("should reset scroll offset when input changes", () => {}); test.todo("should not reset when typing same character", () => {}); }); describe("currentPath changes", () => { test.todo("should reset scroll offset when navigating to new directory", () => {}); test.todo("should reset selection when navigating to new directory", () => {}); test.todo("should reload directory entries for new path", () => {}); }); describe("tab completion", () => { test.todo("should cycle through completions when pressing Tab repeatedly", () => {}); test.todo("should add trailing slash after completing directory", () => {}); test.todo("should update currentPath to completed directory", () => {}); test.todo("should update folder list to show completed directory's contents", () => {}); }); describe("backspace handling", () => { test.todo("should allow backspacing initial tilde", () => {}); test.todo("should allow backspacing to empty input", () => {}); test.todo("should reset selection when backspacing", () => {}); test.todo("should reset scroll offset when backspacing", () => {}); }); describe("escape key handling", () => { test.todo("should go to parent directory when input ends with /", () => {}); test.todo("should clear input when input has text beyond ~", () => {}); test.todo("should cancel when input is just ~", () => {}); test.todo("should reset all state when going back", () => {}); }); describe("Ctrl+U handling", () => { test.todo("should reset input to ~", () => {}); test.todo("should reset selection", () => {}); test.todo("should reset scroll offset", () => {}); test.todo("should clear errors", () => {}); }); describe("folder filtering logic", () => { test.todo("should filter by name when input has no slashes", () => {}); test.todo("should NOT filter when input starts with /", () => {}); test.todo("should NOT filter when input contains /", () => {}); test.todo("should show all folders when typing /Volumes", () => {}); test.todo("should show all folders when typing ~/code/proj", () => {}); }); describe("dynamic currentPath updates", () => { test.todo("should update currentPath when input is valid directory", () => {}); test.todo("should expand ~ before validating", () => {}); test.todo("should reload entries when currentPath changes", () => {}); test.todo("should default to startingPath when input is empty", () => {}); }); }); describe("Integration Scenarios", () => { test.todo("scenario: User types /Volumes/Storage/code and selects it", () => { // Full workflow test }); test.todo("scenario: User filters by typing 'proj' and navigates", () => { // Filter workflow }); test.todo("scenario: User starts with ~, backspaces, types absolute path", () => { // Home to absolute path workflow }); test.todo("scenario: User navigates deep into hierarchy, uses Esc to go back", () => { // Navigation and back workflow }); test.todo("scenario: User tabs through completions, cycles correctly", () => { // Tab completion workflow }); test.todo("scenario: Many folders, user scrolls through list", () => { // Scrolling workflow }); });