mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-11 10:57:07 +00:00
* fix(app): show current contents of collapsed folders after Files refresh Refresh only re-listed expanded folders, and expanding a folder skips the request when a listing is cached, so a folder collapsed during Refresh kept showing its old contents. Drop the cached listings Refresh does not re-list. The refresh helper moves to file-explorer/tree.ts so it can be tested. * test(app): cover refreshing collapsed folders through the browser Exercise the reported file creation, refresh and reopen sequence through a real daemon, then check refresh while expanded. Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn> * fix(app): ignore directory replies invalidated by Refresh A directory request started before Refresh could finish afterwards and restore a stale collapsed listing. Track the refresh generation in explorer state and accept completion only from the current generation. Keep directory loading and invalidation in the file-explorer model. * fix(app): preserve expansion when Refresh cancels restoration Keep an invalidated directory operation distinct from a failed listing. Propagate cancellation through saved-folder restoration so it preserves the current expansion state instead of committing an incomplete restored path set. --------- Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
272 lines
8.9 KiB
TypeScript
272 lines
8.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { ExplorerDirectory, ExplorerEntry } from "@/stores/session-store";
|
|
import {
|
|
MAX_AUTO_EXPANDED_DIRECTORY_DEPTH,
|
|
flattenExplorerTree,
|
|
reconcileRestoredExpandedPaths,
|
|
refreshExplorerDirectories,
|
|
restoreExpandedDirectories,
|
|
setExpandedDirectoryPath,
|
|
showHiddenFilesAndRestoreExpandedDirectories,
|
|
} from "./tree";
|
|
|
|
function makeDirectoryEntry(name: string, path: string): ExplorerEntry {
|
|
return {
|
|
name,
|
|
path,
|
|
kind: "directory",
|
|
size: 0,
|
|
modifiedAt: "2026-01-01T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
describe("file explorer tree", () => {
|
|
it("flattens a deeply expanded tree without consuming the call stack", () => {
|
|
const depth = 10_000;
|
|
const directories = new Map<string, { path: string; entries: ExplorerEntry[] }>();
|
|
const expandedPaths = new Set<string>(["."]);
|
|
let parentPath = ".";
|
|
|
|
for (let index = 1; index <= depth; index += 1) {
|
|
const childPath = `directory-${index}`;
|
|
directories.set(parentPath, {
|
|
path: parentPath,
|
|
entries: [makeDirectoryEntry(childPath, childPath)],
|
|
});
|
|
expandedPaths.add(childPath);
|
|
parentPath = childPath;
|
|
}
|
|
directories.set(parentPath, { path: parentPath, entries: [] });
|
|
|
|
const rows = flattenExplorerTree({
|
|
directories,
|
|
expandedPaths,
|
|
sortOption: "name",
|
|
showHiddenFiles: true,
|
|
});
|
|
|
|
expect(rows).toHaveLength(depth);
|
|
expect(rows[0]).toEqual({
|
|
entry: makeDirectoryEntry("directory-1", "directory-1"),
|
|
depth: 0,
|
|
});
|
|
expect(rows.at(-1)).toEqual({
|
|
entry: makeDirectoryEntry(`directory-${depth}`, `directory-${depth}`),
|
|
depth: depth - 1,
|
|
});
|
|
});
|
|
|
|
it("flattens a large expanded directory without spreading its rows into the parent", () => {
|
|
const fileCount = 150_000;
|
|
const files = Array.from(
|
|
{ length: fileCount },
|
|
(_, index): ExplorerEntry => ({
|
|
name: `file-${index.toString().padStart(6, "0")}`,
|
|
path: `generated/file-${index}`,
|
|
kind: "file",
|
|
size: index,
|
|
modifiedAt: "2026-01-01T00:00:00.000Z",
|
|
}),
|
|
);
|
|
const child = makeDirectoryEntry("generated", "generated");
|
|
const directories = new Map([
|
|
[".", { path: ".", entries: [child] }],
|
|
["generated", { path: "generated", entries: files }],
|
|
]);
|
|
|
|
const rows = flattenExplorerTree({
|
|
directories,
|
|
expandedPaths: new Set([".", "generated"]),
|
|
sortOption: "name",
|
|
showHiddenFiles: true,
|
|
});
|
|
|
|
expect(rows).toHaveLength(fileCount + 1);
|
|
expect(rows[0]).toEqual({ entry: child, depth: 0 });
|
|
expect(rows.at(-1)).toEqual({ entry: files[fileCount - 1], depth: 1 });
|
|
});
|
|
|
|
it("restores five rendered directory levels rather than counting path segments", async () => {
|
|
const paths = [
|
|
"generated/cache/level-1",
|
|
"generated/cache/level-1/level-2",
|
|
"generated/cache/level-1/level-2/level-3",
|
|
"generated/cache/level-1/level-2/level-3/level-4",
|
|
"generated/cache/level-1/level-2/level-3/level-4/level-5",
|
|
"generated/cache/level-1/level-2/level-3/level-4/level-5/level-6",
|
|
];
|
|
const directories = new Map<string, { path: string; entries: ExplorerEntry[] }>();
|
|
const rootDirectory = {
|
|
path: ".",
|
|
entries: [makeDirectoryEntry("level-1", paths[0])],
|
|
};
|
|
directories.set(".", rootDirectory);
|
|
for (let index = 0; index < paths.length - 1; index += 1) {
|
|
directories.set(paths[index], {
|
|
path: paths[index],
|
|
entries: [makeDirectoryEntry(`level-${index + 2}`, paths[index + 1])],
|
|
});
|
|
}
|
|
|
|
const requestedPaths: string[] = [];
|
|
const expandedPaths = await restoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths: new Set(paths),
|
|
showHiddenFiles: true,
|
|
requestDirectoryListing: async (path) => {
|
|
requestedPaths.push(path);
|
|
return directories.get(path) ?? null;
|
|
},
|
|
});
|
|
|
|
expect(MAX_AUTO_EXPANDED_DIRECTORY_DEPTH).toBe(5);
|
|
expect(requestedPaths).toEqual(paths.slice(0, 5));
|
|
expect(expandedPaths).toEqual([".", ...paths.slice(0, 5)]);
|
|
});
|
|
|
|
it("does not restore persisted descendants beneath a collapsed rendered directory", async () => {
|
|
const rootDirectory = {
|
|
path: ".",
|
|
entries: [makeDirectoryEntry("parent", "parent")],
|
|
};
|
|
const requestedPaths: string[] = [];
|
|
|
|
const expandedPaths = await restoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths: new Set(["parent/child", "parent/child/grandchild"]),
|
|
showHiddenFiles: true,
|
|
requestDirectoryListing: async (path) => {
|
|
requestedPaths.push(path);
|
|
return null;
|
|
},
|
|
});
|
|
|
|
expect(requestedPaths).toEqual([]);
|
|
expect(expandedPaths).toEqual(["."]);
|
|
});
|
|
|
|
it("preserves expansion changes made while persisted directories are restoring", () => {
|
|
const paths = reconcileRestoredExpandedPaths({
|
|
persistedExpandedPaths: new Set([".", "parent", "parent/child"]),
|
|
currentExpandedPaths: new Set([".", "parent/child", "manual"]),
|
|
restoredExpandedPaths: [".", "parent"],
|
|
});
|
|
|
|
expect(paths).toEqual([".", "manual"]);
|
|
});
|
|
|
|
it("applies a directory click to the latest restored expansion paths", () => {
|
|
const expanded = setExpandedDirectoryPath({
|
|
currentExpandedPaths: [".", "restored"],
|
|
directoryPath: "manual",
|
|
expanded: true,
|
|
});
|
|
const collapsed = setExpandedDirectoryPath({
|
|
currentExpandedPaths: expanded,
|
|
directoryPath: "manual",
|
|
expanded: false,
|
|
});
|
|
|
|
expect(expanded).toEqual([".", "restored", "manual"]);
|
|
expect(collapsed).toEqual([".", "restored"]);
|
|
});
|
|
|
|
it("shows hidden files before waiting for expanded directories to restore", async () => {
|
|
const rootDirectory = {
|
|
path: ".",
|
|
entries: [makeDirectoryEntry(".hidden", ".hidden")],
|
|
};
|
|
let resolveDirectory!: (directory: { path: string; entries: ExplorerEntry[] }) => void;
|
|
const directoryListing = new Promise<{ path: string; entries: ExplorerEntry[] }>((resolve) => {
|
|
resolveDirectory = resolve;
|
|
});
|
|
let hiddenFilesAreShown = false;
|
|
|
|
const restoration = showHiddenFilesAndRestoreExpandedDirectories({
|
|
rootDirectory,
|
|
persistedExpandedPaths: new Set([".hidden"]),
|
|
showHiddenFiles: () => {
|
|
hiddenFilesAreShown = true;
|
|
},
|
|
requestDirectoryListing: () => directoryListing,
|
|
});
|
|
|
|
expect(hiddenFilesAreShown).toBe(true);
|
|
resolveDirectory({ path: ".hidden", entries: [] });
|
|
await expect(restoration).resolves.toEqual([".", ".hidden"]);
|
|
});
|
|
});
|
|
|
|
describe("refreshExplorerDirectories", () => {
|
|
function makeListing(path: string, names: string[]): ExplorerDirectory {
|
|
return {
|
|
path,
|
|
entries: names.map((name) => ({
|
|
name,
|
|
path: path === "." ? name : `${path}/${name}`,
|
|
kind: "file" as const,
|
|
size: 0,
|
|
modifiedAt: "2026-01-01T00:00:00.000Z",
|
|
})),
|
|
};
|
|
}
|
|
|
|
function createExplorer(disk: Record<string, string[]>, cached: Record<string, string[]>) {
|
|
const directories = new Map(
|
|
Object.entries(cached).map(([path, names]) => [path, makeListing(path, names)]),
|
|
);
|
|
return {
|
|
directories,
|
|
requestDirectoryListing: async (path: string) => {
|
|
const listing = makeListing(path, disk[path] ?? []);
|
|
directories.set(path, listing);
|
|
return listing;
|
|
},
|
|
invalidateDirectoryListings: (retainedPaths: readonly string[]) => {
|
|
const retained = new Set(retainedPaths);
|
|
for (const path of directories.keys()) if (!retained.has(path)) directories.delete(path);
|
|
},
|
|
};
|
|
}
|
|
|
|
it("does not keep a stale listing for a collapsed folder", async () => {
|
|
// docs/ was expanded and collapsed earlier, then NEW.md was created in it.
|
|
const explorer = createExplorer(
|
|
{ ".": ["docs"], docs: ["README.md", "NEW.md"] },
|
|
{ ".": ["docs"], docs: ["README.md"] },
|
|
);
|
|
|
|
await refreshExplorerDirectories({
|
|
hasWorkspaceScope: true,
|
|
expandedPaths: new Set(["."]),
|
|
showHiddenFiles: false,
|
|
requestDirectoryListing: explorer.requestDirectoryListing,
|
|
invalidateDirectoryListings: explorer.invalidateDirectoryListings,
|
|
});
|
|
|
|
// Expanding docs/ only fetches when it has no cached listing.
|
|
expect(explorer.directories.has("docs")).toBe(false);
|
|
expect(explorer.directories.get(".")?.entries.map((entry) => entry.name)).toEqual(["docs"]);
|
|
});
|
|
|
|
it("refreshes expanded folders and keeps their listings", async () => {
|
|
const explorer = createExplorer(
|
|
{ ".": ["src"], src: ["a.ts", "b.ts"] },
|
|
{ ".": ["src"], src: ["a.ts"] },
|
|
);
|
|
|
|
await refreshExplorerDirectories({
|
|
hasWorkspaceScope: true,
|
|
expandedPaths: new Set([".", "src"]),
|
|
showHiddenFiles: false,
|
|
requestDirectoryListing: explorer.requestDirectoryListing,
|
|
invalidateDirectoryListings: explorer.invalidateDirectoryListings,
|
|
});
|
|
|
|
expect(explorer.directories.get("src")?.entries.map((entry) => entry.name)).toEqual([
|
|
"a.ts",
|
|
"b.ts",
|
|
]);
|
|
});
|
|
});
|