test(client): cover workspace files panel tree boundary

This commit is contained in:
Federico Jaramillo Martinez
2026-07-05 00:26:34 +02:00
parent fad83772b7
commit a6e0f1db18
@@ -1,5 +1,6 @@
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { FileContentResponse, FileTreeEntry } from "../api";
import { initialAppState } from "../appState"; import { initialAppState } from "../appState";
import type { WorkspacePanelContext } from "../plugins/types"; import type { WorkspacePanelContext } from "../plugins/types";
import type { WorkspaceUploadBatchState } from "../workspaceUploadState"; import type { WorkspaceUploadBatchState } from "../workspaceUploadState";
@@ -39,6 +40,38 @@ describe("workspace-files-panel upload review", () => {
}); });
}); });
describe("workspace-files-panel file tree boundary", () => {
it("renders expanded tree and selected-file state while wiring row clicks", () => {
const onExpandDir = vi.fn<WorkspacePanelContext["onExpandDir"]>();
const onSelectFile = vi.fn<WorkspacePanelContext["onSelectFile"]>();
const panel = new WorkspaceFilesPanel();
panel.context = workspacePanelContext({
fileTree: [directoryEntry("src"), fileEntry("README.md", 4096)],
expandedDirs: { src: [fileEntry("src/main.ts")] },
selectedFilePath: "README.md",
selectedFileContent: binaryFileContent("README.md", 4096),
onExpandDir,
onSelectFile,
});
const rendered = panel.render();
const text = collectTemplateText(rendered);
expect(text).toContain("▾");
expect(text).toContain("src");
expect(text).toContain("main.ts");
expect(text).toContain("README.md");
expect(text).toContain("Binary file: README.md · 4.0 KB");
expect(text).not.toContain("Select a file.");
findTemplateClickHandlerForText<Event>(rendered, "src")(new Event("click"));
findTemplateClickHandlerForText<Event>(rendered, "README.md")(new Event("click"));
expect(onExpandDir).toHaveBeenCalledWith("src");
expect(onSelectFile).toHaveBeenCalledWith("README.md");
});
});
describe("workspaceUploadBatchesForScope", () => { describe("workspaceUploadBatchesForScope", () => {
it("filters upload batches to the selected project, workspace, and machine", () => { it("filters upload batches to the selected project, workspace, and machine", () => {
const matchingOlder = uploadBatch({ id: "older", startedAt: "2026-06-25T00:00:00.000Z" }); const matchingOlder = uploadBatch({ id: "older", startedAt: "2026-06-25T00:00:00.000Z" });
@@ -154,6 +187,50 @@ function findOptionalTemplateEventHandler<E extends Event>(template: TemplateRes
} }
} }
// Node-based Lit tests cannot click shadow DOM here; keep direct handler extraction
// anchored to rendered file labels and assert the observable context callbacks.
function findTemplateClickHandlerForText<E extends Event>(template: TemplateResult, text: string): TemplateEventHandler<E> {
const handler = findOptionalTemplateClickHandlerForText<E>(template, text);
if (handler === undefined) throw new Error(`Expected click handler near ${text}`);
return handler;
}
function findOptionalTemplateClickHandlerForText<E extends Event>(value: unknown, text: string): TemplateEventHandler<E> | undefined {
if (Array.isArray(value)) {
for (const item of value) {
const nestedHandler = findOptionalTemplateClickHandlerForText<E>(item, text);
if (nestedHandler !== undefined) return nestedHandler;
}
return undefined;
}
if (!isTemplateResult(value)) return undefined;
for (const item of templateValues(value)) {
const nestedHandler = findOptionalTemplateClickHandlerForText<E>(item, text);
if (nestedHandler !== undefined) return nestedHandler;
}
if (!collectTemplateText(value).includes(text)) return undefined;
const strings = templateStrings(value);
const values = templateValues(value);
for (let index = 0; index < values.length; index += 1) {
const staticChunk = strings[index];
const candidate = values[index];
if (staticChunk !== undefined && staticChunk.includes("@click") && isTemplateEventHandler<E>(candidate)) return candidate;
}
return undefined;
}
function collectTemplateText(value: unknown): string {
if (Array.isArray(value)) return value.map((item) => collectTemplateText(item)).join("");
if (isTemplateResult(value)) {
const strings = templateStrings(value);
const values = templateValues(value);
return strings.map((part, index) => `${part}${index < values.length ? collectTemplateText(values[index]) : ""}`).join("");
}
return typeof value === "string" || typeof value === "number" ? String(value) : "";
}
function templateStrings(template: TemplateResult): readonly string[] { function templateStrings(template: TemplateResult): readonly string[] {
const strings = Reflect.get(template, "strings"); const strings = Reflect.get(template, "strings");
if (!isStringArray(strings)) throw new Error("TemplateResult strings were unavailable"); if (!isStringArray(strings)) throw new Error("TemplateResult strings were unavailable");
@@ -222,44 +299,64 @@ class FakeSubmitEvent extends Event implements SubmitEvent {
readonly submitter: HTMLElement | null = null; readonly submitter: HTMLElement | null = null;
} }
function workspacePanelContext(patch: Partial<Pick<WorkspacePanelContext, "onStartWorkspaceUpload" | "workspaceUploadDefaultFolder">> = {}): WorkspacePanelContext { function fileEntry(path: string, size = 2): FileTreeEntry {
const workspace = { id: "workspace-1", projectId: "project-1", path: "/tmp/project", label: "main", isMain: true, isGitRepo: true, isGitWorktree: false }; return { name: path.split("/").at(-1) ?? path, path, type: "file", size };
}
function directoryEntry(path: string): FileTreeEntry {
return { name: path.split("/").at(-1) ?? path, path, type: "directory" };
}
function binaryFileContent(path: string, size: number): FileContentResponse {
return { return {
machine: { id: "local", name: "Local", kind: "local" }, path,
encoding: "utf8",
size,
modifiedAt: "2026-06-25T00:00:00.000Z",
content: "",
truncated: false,
binary: true,
};
}
function workspacePanelContext(patch: Partial<WorkspacePanelContext> = {}): WorkspacePanelContext {
const workspace = patch.workspace ?? { id: "workspace-1", projectId: "project-1", path: "/tmp/project", label: "main", isMain: true, isGitRepo: true, isGitWorktree: false };
return {
machine: patch.machine ?? { id: "local", name: "Local", kind: "local" },
workspace, workspace,
state: { ...initialAppState(), workspaceUploadBatches: {} }, state: patch.state ?? { ...initialAppState(), workspaceUploadBatches: {} },
files: { files: patch.files ?? {
readFile: vi.fn<WorkspacePanelContext["files"]["readFile"]>(() => Promise.reject(new Error("not implemented"))), readFile: vi.fn<WorkspacePanelContext["files"]["readFile"]>(() => Promise.reject(new Error("not implemented"))),
writeFile: vi.fn<WorkspacePanelContext["files"]["writeFile"]>(() => Promise.reject(new Error("not implemented"))), writeFile: vi.fn<WorkspacePanelContext["files"]["writeFile"]>(() => Promise.reject(new Error("not implemented"))),
deleteFile: vi.fn<WorkspacePanelContext["files"]["deleteFile"]>(() => Promise.reject(new Error("not implemented"))), deleteFile: vi.fn<WorkspacePanelContext["files"]["deleteFile"]>(() => Promise.reject(new Error("not implemented"))),
moveFile: vi.fn<WorkspacePanelContext["files"]["moveFile"]>(() => Promise.reject(new Error("not implemented"))), moveFile: vi.fn<WorkspacePanelContext["files"]["moveFile"]>(() => Promise.reject(new Error("not implemented"))),
}, },
prompt: { insertText: vi.fn<WorkspacePanelContext["prompt"]["insertText"]>(), getText: vi.fn<WorkspacePanelContext["prompt"]["getText"]>(() => ""), getSelection: vi.fn<WorkspacePanelContext["prompt"]["getSelection"]>(() => null) }, prompt: patch.prompt ?? { insertText: vi.fn<WorkspacePanelContext["prompt"]["insertText"]>(), getText: vi.fn<WorkspacePanelContext["prompt"]["getText"]>(() => ""), getSelection: vi.fn<WorkspacePanelContext["prompt"]["getSelection"]>(() => null) },
terminal: { open: vi.fn<WorkspacePanelContext["terminal"]["open"]>(), runCommand: vi.fn<WorkspacePanelContext["terminal"]["runCommand"]>(() => Promise.reject(new Error("not implemented"))) }, terminal: patch.terminal ?? { open: vi.fn<WorkspacePanelContext["terminal"]["open"]>(), runCommand: vi.fn<WorkspacePanelContext["terminal"]["runCommand"]>(() => Promise.reject(new Error("not implemented"))) },
host: { requestRender: vi.fn<WorkspacePanelContext["host"]["requestRender"]>() }, host: patch.host ?? { requestRender: vi.fn<WorkspacePanelContext["host"]["requestRender"]>() },
fileTree: [], fileTree: patch.fileTree ?? [],
expandedDirs: {}, expandedDirs: patch.expandedDirs ?? {},
selectedFilePath: undefined, selectedFilePath: patch.selectedFilePath,
selectedFileContent: undefined, selectedFileContent: patch.selectedFileContent,
fileTreeStale: false, fileTreeStale: patch.fileTreeStale ?? false,
gitStatus: undefined, gitStatus: patch.gitStatus,
selectedDiffPath: undefined, selectedDiffPath: patch.selectedDiffPath,
selectedDiff: undefined, selectedDiff: patch.selectedDiff,
selectedStagedDiff: undefined, selectedStagedDiff: patch.selectedStagedDiff,
gitStale: false, gitStale: patch.gitStale ?? false,
activeTerminalCount: 0, activeTerminalCount: patch.activeTerminalCount ?? 0,
selectedTerminalId: undefined, selectedTerminalId: patch.selectedTerminalId,
terminalAutoStart: false, terminalAutoStart: patch.terminalAutoStart ?? false,
workspaceUploadDefaultFolder: patch.workspaceUploadDefaultFolder ?? ".pi-web/uploads", workspaceUploadDefaultFolder: patch.workspaceUploadDefaultFolder ?? ".pi-web/uploads",
onRefreshFiles: vi.fn<WorkspacePanelContext["onRefreshFiles"]>(), onRefreshFiles: patch.onRefreshFiles ?? vi.fn<WorkspacePanelContext["onRefreshFiles"]>(),
onExpandDir: vi.fn<WorkspacePanelContext["onExpandDir"]>(), onExpandDir: patch.onExpandDir ?? vi.fn<WorkspacePanelContext["onExpandDir"]>(),
onSelectFile: vi.fn<WorkspacePanelContext["onSelectFile"]>(), onSelectFile: patch.onSelectFile ?? vi.fn<WorkspacePanelContext["onSelectFile"]>(),
onStartWorkspaceUpload: patch.onStartWorkspaceUpload ?? vi.fn<WorkspacePanelContext["onStartWorkspaceUpload"]>(() => undefined), onStartWorkspaceUpload: patch.onStartWorkspaceUpload ?? vi.fn<WorkspacePanelContext["onStartWorkspaceUpload"]>(() => undefined),
onCancelWorkspaceUpload: vi.fn<WorkspacePanelContext["onCancelWorkspaceUpload"]>(), onCancelWorkspaceUpload: patch.onCancelWorkspaceUpload ?? vi.fn<WorkspacePanelContext["onCancelWorkspaceUpload"]>(),
onClearWorkspaceUpload: vi.fn<WorkspacePanelContext["onClearWorkspaceUpload"]>(), onClearWorkspaceUpload: patch.onClearWorkspaceUpload ?? vi.fn<WorkspacePanelContext["onClearWorkspaceUpload"]>(),
onRefreshGit: vi.fn<WorkspacePanelContext["onRefreshGit"]>(), onRefreshGit: patch.onRefreshGit ?? vi.fn<WorkspacePanelContext["onRefreshGit"]>(),
onSelectDiff: vi.fn<WorkspacePanelContext["onSelectDiff"]>(), onSelectDiff: patch.onSelectDiff ?? vi.fn<WorkspacePanelContext["onSelectDiff"]>(),
onSelectTerminal: vi.fn<WorkspacePanelContext["onSelectTerminal"]>(), onSelectTerminal: patch.onSelectTerminal ?? vi.fn<WorkspacePanelContext["onSelectTerminal"]>(),
}; };
} }