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(); 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(); }); });