Archived
test(client): cover workspace files panel tree boundary
This commit is contained in:
@@ -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"]>(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user