diff --git a/src/client/src/components/WorkspaceFilesPanel.test.ts b/src/client/src/components/WorkspaceFilesPanel.test.ts index 937989c..6fcbdaf 100644 --- a/src/client/src/components/WorkspaceFilesPanel.test.ts +++ b/src/client/src/components/WorkspaceFilesPanel.test.ts @@ -1,5 +1,6 @@ import type { TemplateResult } from "lit"; import { afterEach, describe, expect, it, vi } from "vitest"; +import type { FileContentResponse, FileTreeEntry } from "../api"; import { initialAppState } from "../appState"; import type { WorkspacePanelContext } from "../plugins/types"; 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(); + const onSelectFile = vi.fn(); + 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(rendered, "src")(new Event("click")); + findTemplateClickHandlerForText(rendered, "README.md")(new Event("click")); + + expect(onExpandDir).toHaveBeenCalledWith("src"); + expect(onSelectFile).toHaveBeenCalledWith("README.md"); + }); +}); + describe("workspaceUploadBatchesForScope", () => { it("filters upload batches to the selected project, workspace, and machine", () => { const matchingOlder = uploadBatch({ id: "older", startedAt: "2026-06-25T00:00:00.000Z" }); @@ -154,6 +187,50 @@ function findOptionalTemplateEventHandler(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(template: TemplateResult, text: string): TemplateEventHandler { + const handler = findOptionalTemplateClickHandlerForText(template, text); + if (handler === undefined) throw new Error(`Expected click handler near ${text}`); + return handler; +} + +function findOptionalTemplateClickHandlerForText(value: unknown, text: string): TemplateEventHandler | undefined { + if (Array.isArray(value)) { + for (const item of value) { + const nestedHandler = findOptionalTemplateClickHandlerForText(item, text); + if (nestedHandler !== undefined) return nestedHandler; + } + return undefined; + } + if (!isTemplateResult(value)) return undefined; + + for (const item of templateValues(value)) { + const nestedHandler = findOptionalTemplateClickHandlerForText(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(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[] { const strings = Reflect.get(template, "strings"); 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; } -function workspacePanelContext(patch: Partial> = {}): WorkspacePanelContext { - const workspace = { id: "workspace-1", projectId: "project-1", path: "/tmp/project", label: "main", isMain: true, isGitRepo: true, isGitWorktree: false }; +function fileEntry(path: string, size = 2): FileTreeEntry { + 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 { - 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 { + 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, - state: { ...initialAppState(), workspaceUploadBatches: {} }, - files: { + state: patch.state ?? { ...initialAppState(), workspaceUploadBatches: {} }, + files: patch.files ?? { readFile: vi.fn(() => Promise.reject(new Error("not implemented"))), writeFile: vi.fn(() => Promise.reject(new Error("not implemented"))), deleteFile: vi.fn(() => Promise.reject(new Error("not implemented"))), moveFile: vi.fn(() => Promise.reject(new Error("not implemented"))), }, - prompt: { insertText: vi.fn(), getText: vi.fn(() => ""), getSelection: vi.fn(() => null) }, - terminal: { open: vi.fn(), runCommand: vi.fn(() => Promise.reject(new Error("not implemented"))) }, - host: { requestRender: vi.fn() }, - fileTree: [], - expandedDirs: {}, - selectedFilePath: undefined, - selectedFileContent: undefined, - fileTreeStale: false, - gitStatus: undefined, - selectedDiffPath: undefined, - selectedDiff: undefined, - selectedStagedDiff: undefined, - gitStale: false, - activeTerminalCount: 0, - selectedTerminalId: undefined, - terminalAutoStart: false, + prompt: patch.prompt ?? { insertText: vi.fn(), getText: vi.fn(() => ""), getSelection: vi.fn(() => null) }, + terminal: patch.terminal ?? { open: vi.fn(), runCommand: vi.fn(() => Promise.reject(new Error("not implemented"))) }, + host: patch.host ?? { requestRender: vi.fn() }, + fileTree: patch.fileTree ?? [], + expandedDirs: patch.expandedDirs ?? {}, + selectedFilePath: patch.selectedFilePath, + selectedFileContent: patch.selectedFileContent, + fileTreeStale: patch.fileTreeStale ?? false, + gitStatus: patch.gitStatus, + selectedDiffPath: patch.selectedDiffPath, + selectedDiff: patch.selectedDiff, + selectedStagedDiff: patch.selectedStagedDiff, + gitStale: patch.gitStale ?? false, + activeTerminalCount: patch.activeTerminalCount ?? 0, + selectedTerminalId: patch.selectedTerminalId, + terminalAutoStart: patch.terminalAutoStart ?? false, workspaceUploadDefaultFolder: patch.workspaceUploadDefaultFolder ?? ".pi-web/uploads", - onRefreshFiles: vi.fn(), - onExpandDir: vi.fn(), - onSelectFile: vi.fn(), + onRefreshFiles: patch.onRefreshFiles ?? vi.fn(), + onExpandDir: patch.onExpandDir ?? vi.fn(), + onSelectFile: patch.onSelectFile ?? vi.fn(), onStartWorkspaceUpload: patch.onStartWorkspaceUpload ?? vi.fn(() => undefined), - onCancelWorkspaceUpload: vi.fn(), - onClearWorkspaceUpload: vi.fn(), - onRefreshGit: vi.fn(), - onSelectDiff: vi.fn(), - onSelectTerminal: vi.fn(), + onCancelWorkspaceUpload: patch.onCancelWorkspaceUpload ?? vi.fn(), + onClearWorkspaceUpload: patch.onClearWorkspaceUpload ?? vi.fn(), + onRefreshGit: patch.onRefreshGit ?? vi.fn(), + onSelectDiff: patch.onSelectDiff ?? vi.fn(), + onSelectTerminal: patch.onSelectTerminal ?? vi.fn(), }; }