172 lines
6.5 KiB
TypeScript
172 lines
6.5 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type { FileTreeNode } from "@/lib/types/backend";
|
|
import {
|
|
moveNodesInTree,
|
|
computeMoveTargets,
|
|
findUnloadedAncestor,
|
|
findNodeByPath,
|
|
getAncestorPaths,
|
|
getVisiblePaths,
|
|
sortRootChildren,
|
|
} from "./file-tree-utils";
|
|
|
|
function file(name: string, parentPath = ""): FileTreeNode {
|
|
const path = parentPath ? `${parentPath}/${name}` : name;
|
|
return { name, path, is_dir: false, size: 0 };
|
|
}
|
|
|
|
function dir(name: string, children: FileTreeNode[], parentPath = ""): FileTreeNode {
|
|
const path = parentPath ? `${parentPath}/${name}` : name;
|
|
return { name, path, is_dir: true, children };
|
|
}
|
|
|
|
function root(children: FileTreeNode[]): FileTreeNode {
|
|
return { name: "", path: "", is_dir: true, children };
|
|
}
|
|
|
|
const UTILS_TS = "utils.ts";
|
|
|
|
describe("computeMoveTargets", () => {
|
|
it("computes simple move without collision", () => {
|
|
const tree = root([file("a.ts"), dir("lib", [file("b.ts", "lib")])]);
|
|
const result = computeMoveTargets(tree, ["a.ts"], "lib");
|
|
expect(result).toEqual([{ oldPath: "a.ts", newPath: "lib/a.ts" }]);
|
|
});
|
|
|
|
it("deduplicates when target already has a file with the same name", () => {
|
|
const tree = root([dir("src", [file(UTILS_TS, "src")]), dir("lib", [file(UTILS_TS, "lib")])]);
|
|
const result = computeMoveTargets(tree, [`src/${UTILS_TS}`], "lib");
|
|
expect(result).toEqual([{ oldPath: `src/${UTILS_TS}`, newPath: "lib/utils (1).ts" }]);
|
|
});
|
|
|
|
it("deduplicates when two source files have the same name", () => {
|
|
const tree = root([
|
|
dir("src", [file(UTILS_TS, "src")]),
|
|
dir("lib", [file(UTILS_TS, "lib")]),
|
|
dir("target", []),
|
|
]);
|
|
const result = computeMoveTargets(tree, [`src/${UTILS_TS}`, `lib/${UTILS_TS}`], "target");
|
|
expect(result).toEqual([
|
|
{ oldPath: `src/${UTILS_TS}`, newPath: `target/${UTILS_TS}` },
|
|
{ oldPath: `lib/${UTILS_TS}`, newPath: "target/utils (1).ts" },
|
|
]);
|
|
});
|
|
|
|
it("deduplicates with multiple existing collisions", () => {
|
|
const tree = root([
|
|
file("readme.md"),
|
|
dir("dest", [file("readme.md", "dest"), file("readme (1).md", "dest")]),
|
|
]);
|
|
const result = computeMoveTargets(tree, ["readme.md"], "dest");
|
|
expect(result).toEqual([{ oldPath: "readme.md", newPath: "dest/readme (2).md" }]);
|
|
});
|
|
|
|
it("handles files without extensions", () => {
|
|
const tree = root([file("Makefile"), dir("dest", [file("Makefile", "dest")])]);
|
|
const result = computeMoveTargets(tree, ["Makefile"], "dest");
|
|
expect(result).toEqual([{ oldPath: "Makefile", newPath: "dest/Makefile (1)" }]);
|
|
});
|
|
});
|
|
|
|
describe("moveNodesInTree", () => {
|
|
it("moves a file into a directory", () => {
|
|
const tree = root([file("a.ts"), dir("lib", [])]);
|
|
const result = moveNodesInTree(tree, ["a.ts"], "lib");
|
|
expect(findNodeByPath(result, "a.ts")).toBeNull();
|
|
expect(findNodeByPath(result, "lib/a.ts")).not.toBeNull();
|
|
});
|
|
|
|
it("deduplicates on collision during move", () => {
|
|
const tree = root([
|
|
dir("src", [file("x.ts", "src")]),
|
|
dir("lib", [file("x.ts", "lib")]),
|
|
dir("dest", []),
|
|
]);
|
|
const result = moveNodesInTree(tree, ["src/x.ts", "lib/x.ts"], "dest");
|
|
expect(findNodeByPath(result, "dest/x.ts")).not.toBeNull();
|
|
expect(findNodeByPath(result, "dest/x (1).ts")).not.toBeNull();
|
|
expect(findNodeByPath(result, "src/x.ts")).toBeNull();
|
|
expect(findNodeByPath(result, "lib/x.ts")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("sortRootChildren", () => {
|
|
it("orders directories before files at the root, alpha within each group", () => {
|
|
// Backend can return root children in any order — interleaved dirs/files
|
|
// and not necessarily alphabetical. The file browser feeds this list
|
|
// straight into useTree, which never re-sorts its top-level `nodes`, so
|
|
// this helper is the only thing keeping dirs on top at depth 0.
|
|
const tree = root([
|
|
file("README.md"),
|
|
dir("zeta", []),
|
|
file(".babelrc"),
|
|
dir("alpha", []),
|
|
file("package.json"),
|
|
dir("src", []),
|
|
]);
|
|
// compareTreeNodes uses localeCompare, which is case-insensitive by
|
|
// default — "package" (p) sorts before "README" (R/r).
|
|
expect(sortRootChildren(tree).map((n) => n.name)).toEqual([
|
|
"alpha",
|
|
"src",
|
|
"zeta",
|
|
".babelrc",
|
|
"package.json",
|
|
"README.md",
|
|
]);
|
|
});
|
|
|
|
it("returns an empty array for a null tree or a tree with no children", () => {
|
|
expect(sortRootChildren(null)).toEqual([]);
|
|
expect(sortRootChildren({ name: "", path: "", is_dir: true })).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("getVisiblePaths", () => {
|
|
it("returns all nodes in DFS order including directories", () => {
|
|
const tree = root([dir("lib", [file("config.ts", "lib")], ""), file("readme.md")]);
|
|
const expanded = new Set(["lib"]);
|
|
const paths = getVisiblePaths(tree, expanded);
|
|
// dirs first then files, alphabetical within each group
|
|
expect(paths).toEqual(["lib", "lib/config.ts", "readme.md"]);
|
|
});
|
|
|
|
it("excludes collapsed directory children", () => {
|
|
const tree = root([dir("lib", [file("config.ts", "lib")], ""), file("readme.md")]);
|
|
const expanded = new Set<string>();
|
|
const paths = getVisiblePaths(tree, expanded);
|
|
expect(paths).toEqual(["lib", "readme.md"]);
|
|
});
|
|
});
|
|
|
|
describe("file tree reveal", () => {
|
|
const targetPath = "src/main/kotlin/Greeting.kt";
|
|
const ancestors = ["src", "src/main", "src/main/kotlin"];
|
|
|
|
it("returns every directory path above the target file", () => {
|
|
expect(getAncestorPaths(targetPath)).toEqual(ancestors);
|
|
expect(getAncestorPaths("Greeting.kt")).toEqual([]);
|
|
});
|
|
|
|
it("loads one missing ancestor level at a time", () => {
|
|
const initialTree = root([dir("src", [])]);
|
|
expect(findUnloadedAncestor(initialTree, targetPath, ancestors)?.path).toBe("src");
|
|
|
|
const srcLoaded = root([dir("src", [dir("main", [], "src")])]);
|
|
expect(findUnloadedAncestor(srcLoaded, targetPath, ancestors)?.path).toBe("src/main");
|
|
|
|
const mainLoaded = root([dir("src", [dir("main", [dir("kotlin", [], "src/main")], "src")])]);
|
|
expect(findUnloadedAncestor(mainLoaded, targetPath, ancestors)?.path).toBe("src/main/kotlin");
|
|
});
|
|
|
|
it("stops when the target is loaded or its first ancestor is absent", () => {
|
|
const loadedTree = root([
|
|
dir("src", [
|
|
dir("main", [dir("kotlin", [file("Greeting.kt", "src/main/kotlin")], "src/main")], "src"),
|
|
]),
|
|
]);
|
|
expect(findUnloadedAncestor(loadedTree, targetPath, ancestors)).toBeNull();
|
|
expect(findUnloadedAncestor(root([dir("other", [])]), targetPath, ancestors)).toBeNull();
|
|
});
|
|
});
|