Archived
test(client): cover file explorer tree workflows
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
WorkspaceUploadBatchError,
|
WorkspaceUploadBatchError,
|
||||||
WorkspaceUploadCancelledError,
|
WorkspaceUploadCancelledError,
|
||||||
type FileContentResponse,
|
type FileContentResponse,
|
||||||
|
type FileTreeEntry,
|
||||||
type FileTreeResponse,
|
type FileTreeResponse,
|
||||||
type Machine,
|
type Machine,
|
||||||
type Project,
|
type Project,
|
||||||
@@ -13,6 +14,9 @@ import {
|
|||||||
} from "../api";
|
} from "../api";
|
||||||
import { FileExplorerController, type FileExplorerControllerDependencies } from "./fileExplorerController";
|
import { FileExplorerController, type FileExplorerControllerDependencies } from "./fileExplorerController";
|
||||||
|
|
||||||
|
type FileExplorerApi = NonNullable<FileExplorerControllerDependencies["api"]>;
|
||||||
|
type WorkspaceTree = FileExplorerApi["workspaceTree"];
|
||||||
|
type WorkspaceFile = FileExplorerApi["workspaceFile"];
|
||||||
type UploadWorkspaceFiles = NonNullable<FileExplorerControllerDependencies["uploadWorkspaceFiles"]>;
|
type UploadWorkspaceFiles = NonNullable<FileExplorerControllerDependencies["uploadWorkspaceFiles"]>;
|
||||||
type UploadWorkspaceFilesOptions = NonNullable<Parameters<UploadWorkspaceFiles>[3]>;
|
type UploadWorkspaceFilesOptions = NonNullable<Parameters<UploadWorkspaceFiles>[3]>;
|
||||||
|
|
||||||
@@ -48,6 +52,56 @@ const workspace: Workspace = {
|
|||||||
isGitWorktree: false,
|
isGitWorktree: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe("FileExplorerController file tree workflows", () => {
|
||||||
|
it("refreshes the root and already-expanded directories for the selected machine", async () => {
|
||||||
|
const rootEntries = [directoryEntry("src"), fileEntry("README.md")];
|
||||||
|
const refreshedSrcEntries = [fileEntry("src/index.ts")];
|
||||||
|
const refreshedDocsEntries = [fileEntry("docs/guide.md")];
|
||||||
|
const workspaceTree = vi.fn<WorkspaceTree>((_projectId, _workspaceId, path = "") => Promise.resolve(treeResponse(path, {
|
||||||
|
"": rootEntries,
|
||||||
|
src: refreshedSrcEntries,
|
||||||
|
docs: refreshedDocsEntries,
|
||||||
|
}[path] ?? [])));
|
||||||
|
const harness = createHarness({ api: createApi({ workspaceTree }) }, {
|
||||||
|
expandedDirs: {
|
||||||
|
src: [fileEntry("src/stale.ts")],
|
||||||
|
docs: [fileEntry("docs/stale.md")],
|
||||||
|
},
|
||||||
|
fileTreeStale: true,
|
||||||
|
error: "stale failure",
|
||||||
|
});
|
||||||
|
|
||||||
|
await harness.controller.refreshFiles();
|
||||||
|
|
||||||
|
expect(workspaceTree).toHaveBeenCalledTimes(3);
|
||||||
|
expect(workspaceTree).toHaveBeenCalledWith("project-1", "workspace-1", "", "remote-1");
|
||||||
|
expect(workspaceTree).toHaveBeenCalledWith("project-1", "workspace-1", "src", "remote-1");
|
||||||
|
expect(workspaceTree).toHaveBeenCalledWith("project-1", "workspace-1", "docs", "remote-1");
|
||||||
|
expect(harness.state.fileTree).toEqual(rootEntries);
|
||||||
|
expect(harness.state.expandedDirs).toEqual({ src: refreshedSrcEntries, docs: refreshedDocsEntries });
|
||||||
|
expect(harness.state.fileTreeStale).toBe(false);
|
||||||
|
expect(harness.state.error).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expands a directory then collapses it locally without refetching", async () => {
|
||||||
|
const srcEntries = [fileEntry("src/index.ts")];
|
||||||
|
const workspaceTree = vi.fn<WorkspaceTree>((_projectId, _workspaceId, path = "") => Promise.resolve(treeResponse(path, srcEntries)));
|
||||||
|
const harness = createHarness({ api: createApi({ workspaceTree }) });
|
||||||
|
|
||||||
|
await harness.controller.expandDir("src");
|
||||||
|
|
||||||
|
expect(workspaceTree).toHaveBeenCalledWith("project-1", "workspace-1", "src", "remote-1");
|
||||||
|
expect(harness.state.expandedDirs).toEqual({ src: srcEntries });
|
||||||
|
expect(harness.state.error).toBe("");
|
||||||
|
|
||||||
|
workspaceTree.mockClear();
|
||||||
|
await harness.controller.expandDir("src");
|
||||||
|
|
||||||
|
expect(workspaceTree).not.toHaveBeenCalled();
|
||||||
|
expect(harness.state.expandedDirs).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("FileExplorerController workspace uploads", () => {
|
describe("FileExplorerController workspace uploads", () => {
|
||||||
it("tracks upload progress, completes from final responses, refreshes files, and selects the first uploaded file", async () => {
|
it("tracks upload progress, completes from final responses, refreshes files, and selects the first uploaded file", async () => {
|
||||||
const upload = controllableUpload();
|
const upload = controllableUpload();
|
||||||
@@ -227,18 +281,16 @@ describe("FileExplorerController workspace uploads", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createHarness(deps: FileExplorerControllerDependencies = {}) {
|
function createHarness(deps: FileExplorerControllerDependencies = {}, statePatch: Partial<AppState> = {}) {
|
||||||
installWindow("http://localhost/app");
|
installWindow("http://localhost/app");
|
||||||
let state: AppState = {
|
let state: AppState = {
|
||||||
...initialAppState(),
|
...initialAppState(),
|
||||||
selectedMachine: machine,
|
selectedMachine: machine,
|
||||||
selectedProject: project,
|
selectedProject: project,
|
||||||
selectedWorkspace: workspace,
|
selectedWorkspace: workspace,
|
||||||
|
...statePatch,
|
||||||
};
|
};
|
||||||
const api: NonNullable<FileExplorerControllerDependencies["api"]> = deps.api ?? {
|
const api = deps.api ?? createApi();
|
||||||
workspaceTree: vi.fn<NonNullable<FileExplorerControllerDependencies["api"]>["workspaceTree"]>((_projectId, _workspaceId, path = "") => Promise.resolve(treeResponse(path))),
|
|
||||||
workspaceFile: vi.fn<NonNullable<FileExplorerControllerDependencies["api"]>["workspaceFile"]>((_projectId, _workspaceId, path) => Promise.resolve(fileResponse(path))),
|
|
||||||
};
|
|
||||||
const updateUrl = vi.fn();
|
const updateUrl = vi.fn();
|
||||||
let batchSequence = 0;
|
let batchSequence = 0;
|
||||||
const controller = new FileExplorerController(
|
const controller = new FileExplorerController(
|
||||||
@@ -308,8 +360,24 @@ function sequenceNow(...values: string[]): () => string {
|
|||||||
return () => values[index++] ?? values.at(-1) ?? "now";
|
return () => values[index++] ?? values.at(-1) ?? "now";
|
||||||
}
|
}
|
||||||
|
|
||||||
function treeResponse(path: string): FileTreeResponse {
|
function createApi(overrides: Partial<FileExplorerApi> = {}): FileExplorerApi {
|
||||||
return { path, entries: [], scannedAt: "2026-06-25T00:00:00.000Z", truncated: false };
|
return {
|
||||||
|
workspaceTree: vi.fn<WorkspaceTree>((_projectId, _workspaceId, path = "") => Promise.resolve(treeResponse(path))),
|
||||||
|
workspaceFile: vi.fn<WorkspaceFile>((_projectId, _workspaceId, path) => Promise.resolve(fileResponse(path))),
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function treeResponse(path: string, entries: FileTreeEntry[] = []): FileTreeResponse {
|
||||||
|
return { path, entries, scannedAt: "2026-06-25T00:00:00.000Z", truncated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function directoryEntry(path: string): FileTreeEntry {
|
||||||
|
return { name: path.split("/").at(-1) ?? path, path, type: "directory" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileEntry(path: string): FileTreeEntry {
|
||||||
|
return { name: path.split("/").at(-1) ?? path, path, type: "file", size: 2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileResponse(path: string): FileContentResponse {
|
function fileResponse(path: string): FileContentResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user