Files
kandev/apps/web/components/task/task-center-panel-restoration.test.ts
T
2026-09-15 12:20:54 +01:00

176 lines
5.3 KiB
TypeScript

import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenFileTab } from "@/lib/types/backend";
const mockUpdateFileContent = vi.fn();
const mockGetWebSocketClient = vi.fn();
const mockRequestFileContent = vi.fn();
const mockSaveDocument = vi.fn();
const mockToast = vi.fn();
vi.mock("@/lib/ws/workspace-files", () => ({
requestFileContent: (...args: unknown[]) => mockRequestFileContent(...args),
updateFileContent: (...args: unknown[]) => mockUpdateFileContent(...args),
deleteFile: vi.fn(),
}));
vi.mock("@/lib/ws/connection", () => ({
getWebSocketClient: () => mockGetWebSocketClient(),
}));
vi.mock("@/lib/lsp/lsp-client-manager", () => ({
lspClientManager: {
saveDocument: (...args: unknown[]) => mockSaveDocument(...args),
},
}));
vi.mock("@/components/toast-provider", () => ({
useToast: () => ({ toast: mockToast }),
}));
vi.mock("@/lib/utils/file-diff", () => ({
calculateHash: async (content: string) => `hash:${content.length}`,
generateUnifiedDiff: () => "@@ -1 +1 @@\n-before\n+after",
}));
import { loadSavedFileTabs, useFileSaveDelete } from "./task-center-panel-restoration";
const SESSION_ID = "session";
const PATH = "src/Main.kt";
const REPO = "backend";
const CLIENT = {} as ReturnType<typeof import("@/lib/ws/connection").getWebSocketClient>;
const OPEN_TAB: OpenFileTab = {
path: PATH,
name: "Main.kt",
repo: REPO,
content: "after",
originalContent: "before",
originalHash: "old-hash",
isDirty: true,
};
function renderSaveHook(initialTabs: OpenFileTab[] = [OPEN_TAB]) {
const setOpenFileTabs = vi.fn();
const hook = renderHook(
({ openFileTabs }) =>
useFileSaveDelete({
activeSessionId: SESSION_ID,
openFileTabs,
setOpenFileTabs,
setSavingFiles: vi.fn(),
handleCloseFileTab: vi.fn(),
}),
{ initialProps: { openFileTabs: initialTabs } },
);
return { ...hook, setOpenFileTabs };
}
describe("task center file saves", () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetWebSocketClient.mockReturnValue(CLIENT);
});
it("notifies the language server with the persisted snapshot after a successful save", async () => {
mockUpdateFileContent.mockResolvedValueOnce({ success: true, new_hash: "new-hash" });
const { result } = renderSaveHook();
await act(async () => {
await result.current.handleFileSave(PATH, REPO);
});
expect(mockSaveDocument).toHaveBeenCalledWith(SESSION_ID, PATH, REPO, "after", "after");
});
it("preserves edits made while the persisted save is in flight", async () => {
let resolveSave!: (response: { success: boolean; new_hash: string }) => void;
mockUpdateFileContent.mockReturnValueOnce(
new Promise((resolve) => {
resolveSave = resolve;
}),
);
const view = renderSaveHook();
let savePromise!: Promise<void>;
act(() => {
savePromise = view.result.current.handleFileSave(PATH, REPO);
});
const newerTab = { ...OPEN_TAB, content: "newer edit", isDirty: true };
view.rerender({ openFileTabs: [newerTab] });
await act(async () => {
resolveSave({ success: true, new_hash: "new-hash" });
await savePromise;
});
expect(mockSaveDocument).toHaveBeenCalledWith(SESSION_ID, PATH, REPO, "after", "newer edit");
const update = view.setOpenFileTabs.mock.calls[0]?.[0] as (
tabs: OpenFileTab[],
) => OpenFileTab[];
expect(update([newerTab])).toEqual([
expect.objectContaining({
content: "newer edit",
originalContent: "after",
originalHash: "new-hash",
isDirty: true,
}),
]);
});
it("does not notify the language server after a rejected save", async () => {
mockUpdateFileContent.mockResolvedValueOnce({ success: false, error: "write failed" });
const { result } = renderSaveHook();
await act(async () => {
await result.current.handleFileSave(PATH, REPO);
});
expect(mockSaveDocument).not.toHaveBeenCalled();
});
});
describe("task center file restoration", () => {
beforeEach(() => {
vi.clearAllMocks();
mockGetWebSocketClient.mockReturnValue(CLIENT);
});
it("restores generic rendered preview state for center-panel tabs", async () => {
mockRequestFileContent.mockResolvedValueOnce({
path: "README.md",
content: "# README",
is_binary: false,
resolved_path: "docs/README.md",
});
const tabs = await loadSavedFileTabs("session", [
{ path: "README.md", name: "README.md", renderedPreview: true, pinned: true },
]);
expect(tabs?.[0]).toEqual({
path: "README.md",
name: "README.md",
content: "# README",
originalContent: "# README",
originalHash: "hash:8",
resolvedPath: "docs/README.md",
isDirty: false,
isBinary: false,
renderedPreview: true,
});
});
it("does not restore obsolete in-place preview state for HTML tabs", async () => {
mockRequestFileContent.mockResolvedValueOnce({
path: "reports/index.html",
content: "<h1>Report</h1>",
is_binary: false,
});
const tabs = await loadSavedFileTabs("session", [
{ path: "reports/index.html", name: "index.html", renderedPreview: true, pinned: true },
]);
expect(tabs?.[0]?.renderedPreview).toBeUndefined();
});
});