opencode/packages/app/src/components/directory-picker-domain.test.ts
2026-06-24 02:51:26 +00:00

225 lines
8.9 KiB
TypeScript

import { expect, test } from "bun:test"
import {
absoluteTreePath,
activeTreeNavigation,
advanceTreePreload,
nextSuggestionIndex,
nextTreeScrollTop,
pickerTreeEntries,
pickerSearchEntries,
pickerFileSearchQuery,
pickerMode,
preloadTreeDirectories,
selectedTreePath,
treeEntries,
treePathWithin,
currentPickerSuggestions,
createDirectorySearch,
createPriorityTaskQueue,
displayPickerPath,
pickerParent,
pickerRoot,
pickerAbsoluteInput,
} from "./directory-picker-domain"
test("maps server directory entries into Pierre paths", () => {
expect(
treeEntries("src/", [
{ name: "components", type: "directory" },
{ name: "index.ts", type: "file" },
]),
).toEqual(["src/components/", "src/index.ts"])
})
test("maps Pierre paths back to the selected server root", () => {
expect(absoluteTreePath("C:/Users/luke", "src/components/")).toBe("C:/Users/luke/src/components")
expect(absoluteTreePath("C:/", "")).toBe("C:/")
expect(absoluteTreePath("C:/", "README.md")).toBe("C:/README.md")
expect(absoluteTreePath("/home/luke", "README.md")).toBe("/home/luke/README.md")
})
test("includes files only when the picker selects files", () => {
const nodes = [
{ name: "components", type: "directory" as const },
{ name: "index.ts", type: "file" as const },
]
expect(pickerTreeEntries("", nodes, "directory")).toEqual(["components/"])
expect(pickerTreeEntries("", nodes, "file")).toEqual(["components/", "index.ts"])
})
test("includes files in file autocomplete while preserving directory navigation", () => {
const nodes = [
{ name: "src", absolute: "/repo/src", type: "directory" as const },
{ name: "README.md", absolute: "/repo/README.md", type: "file" as const },
]
expect(pickerSearchEntries(nodes, "directory")).toEqual([nodes[0]])
expect(pickerSearchEntries(nodes, "file")).toEqual(nodes)
})
test("centralizes file and directory selection policy", () => {
const file = pickerMode("file", "/repo")
expect(file.includeFiles).toBeTrue()
expect(file.selection("/repo/src", "index.ts")).toBe("src/index.ts")
expect(file.selection("/repo", "src/")).toBeUndefined()
expect(file.result("/repo", "src/index.ts")).toBe("src/index.ts")
expect(file.selection("/tmp", "example.txt")).toBeUndefined()
expect(file.navigation("/repo/src")).toBe("/repo/src")
expect(file.navigation("/tmp")).toBeUndefined()
const directory = pickerMode("directory")
expect(directory.includeFiles).toBeFalse()
expect(directory.selection("/repo", "src/")).toBe("/repo/src")
expect(directory.selection("C:/Users/luke", "repos/")).toBe("C:\\Users\\luke\\repos")
expect(directory.selection("//Server/Share", "repo/")).toBe("\\\\Server\\Share\\repo")
expect(directory.navigation("/tmp")).toBe("/tmp")
expect(directory.result("/repo", "")).toBe("/repo")
expect(directory.result("C:/Users/luke", "")).toBe("C:\\Users\\luke")
expect(directory.result("//Server/Share/repo", "")).toBe("\\\\Server\\Share\\repo")
expect(directory.result("/repo", "", false)).toBeUndefined()
})
test("accepts mutations only from the active navigation", () => {
expect(activeTreeNavigation(3, 3)).toBeTrue()
expect(activeTreeNavigation(2, 3)).toBeFalse()
})
test("preserves POSIX case while matching Windows drives case-insensitively", () => {
expect(treePathWithin("/repo", "/Repo")).toBeFalse()
expect(treePathWithin("C:/Repo", "c:/repo/src")).toBeTrue()
expect(treePathWithin("//Server/Share/Repo", "//server/share/repo/src")).toBeTrue()
expect(pickerMode("file", "//Server/Share/Repo").selection("//server/share/repo/src", "file.ts")).toBe("src/file.ts")
expect(treePathWithin("/repo", "/repo/../tmp")).toBeFalse()
expect(treePathWithin("/", "/src")).toBeTrue()
expect(pickerMode("file", "C:/Repo").selection("c:/repo/src", "file.ts")).toBe("src/file.ts")
expect(pickerMode("file", "C:/").selection("C:/", "file.ts")).toBe("file.ts")
})
test("displays paths using the selected server path format", () => {
expect(displayPickerPath("C:/Users/luke/repos", "C:/Users/luke/repos", "C:/Users/luke")).toBe(
"C:\\Users\\luke\\repos",
)
expect(displayPickerPath("C:/Users/luke/repos", "C:\\Users\\luke\\repos", "C:/Users/luke")).toBe(
"C:\\Users\\luke\\repos",
)
expect(displayPickerPath("/home/luke/repos", "repos", "/home/luke")).toBe("~/repos")
expect(displayPickerPath("/home/luke/repos", "~/repos", "/home/luke")).toBe("~/repos")
})
test("treats the server share prefix as the UNC root", () => {
expect(pickerRoot("//Server/Share/repo/src")).toBe("//Server/Share")
expect(pickerRoot("\\\\Server\\Share\\repo\\src")).toBe("//Server/Share")
expect(pickerParent("//Server/Share")).toBe("//Server/Share")
expect(pickerParent("//Server/Share/repo")).toBe("//Server/Share")
})
test("resolves relative input against the current picker root", () => {
expect(pickerAbsoluteInput("src", "/home/luke", "/home/luke/repo")).toBe("/home/luke/repo/src")
expect(pickerAbsoluteInput("../other", "/home/luke", "/home/luke/repo")).toBe("/home/luke/other")
expect(pickerAbsoluteInput("~/.config", "/home/luke", "/home/luke/repo")).toBe("/home/luke/.config")
expect(pickerAbsoluteInput("src", "C:/Users/luke", "C:/Users/luke/repo")).toBe("C:/Users/luke/repo/src")
})
test("exposes autocomplete results only for their source query", () => {
const result = { query: "/repo/src", items: ["/repo/src/index.ts"] }
expect(currentPickerSuggestions(result, "/repo/src")).toEqual(result.items)
expect(currentPickerSuggestions(result, "/repo/test")).toEqual([])
})
test("scopes file autocomplete to the current browser root", () => {
expect(pickerFileSearchQuery("/home/luke/repos", "/home/luke/repos/src/in", "/home/luke")).toBe("src/in")
expect(pickerFileSearchQuery("/home/luke", "~/repos/op", "/home/luke")).toBe("repos/op")
})
test("resolves directory autocomplete from the current browser root", async () => {
const directories: string[] = []
const sdk = {
client: {
find: {
files: (input: { directory: string }) => {
directories.push(input.directory)
return Promise.resolve({ data: [] })
},
},
},
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
let base = "/repo"
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => base })
await search("components")
base = "/repo/src"
await search("components")
expect(directories).toEqual(["/repo", "/repo/src"])
})
test("identifies the next directory level to preload", () => {
expect(
preloadTreeDirectories("src/", [
{ name: "components", type: "directory" },
{ name: "index.ts", type: "file" },
{ name: "utils", type: "directory" },
]),
).toEqual(["src/components/", "src/utils/"])
})
test("advances preloading once for every expanded directory", () => {
const advanced = new Set<string>()
expect(advanceTreePreload(advanced, "")).toBeTrue()
expect(advanceTreePreload(advanced, "")).toBeFalse()
expect(advanceTreePreload(advanced, "repos/")).toBeTrue()
})
test("limits background tasks and prioritizes newly requested work", async () => {
const queue = createPriorityTaskQueue<void>(2)
const first = Promise.withResolvers<void>()
const second = Promise.withResolvers<void>()
const started: string[] = []
let active = 0
let maximum = 0
const task = (name: string, blocker?: Promise<void>) => async () => {
started.push(name)
active++
maximum = Math.max(maximum, active)
await blocker
active--
}
const running = [
queue.schedule("first", "background", task("first", first.promise)),
queue.schedule("second", "background", task("second", second.promise)),
queue.schedule("preload", "background", task("preload")),
queue.schedule("opened", "user", task("opened")),
]
await Promise.resolve()
expect(started).toEqual(["first", "second"])
first.resolve()
await running[0]
await Promise.resolve()
expect(started).toEqual(["first", "second", "opened"])
second.resolve()
await Promise.all(running)
expect(started).toEqual(["first", "second", "opened", "preload"])
expect(maximum).toBe(2)
})
test("clamps bridged tree wheel scrolling", () => {
expect(nextTreeScrollTop(100, 40, 500, 200)).toBe(140)
expect(nextTreeScrollTop(10, -40, 500, 200)).toBe(0)
expect(nextTreeScrollTop(290, 40, 500, 200)).toBe(300)
})
test("wraps autocomplete keyboard navigation", () => {
expect(nextSuggestionIndex(-1, 1, 4)).toBe(0)
expect(nextSuggestionIndex(3, 1, 4)).toBe(0)
expect(nextSuggestionIndex(0, -1, 4)).toBe(3)
expect(nextSuggestionIndex(0, 1, 0)).toBe(-1)
})
test("returns absolute directories and relative files", () => {
expect(selectedTreePath("/home/luke/repo", "src/", "directory")).toBe("/home/luke/repo/src")
expect(selectedTreePath("/home/luke/repo", "src/index.ts", "file")).toBe("src/index.ts")
expect(selectedTreePath("/home/luke/repo/src", "index.ts", "file", "/home/luke/repo")).toBe("src/index.ts")
expect(selectedTreePath("/home/luke/repo", "src/", "file")).toBeUndefined()
})