Files
kandev/apps/web/app/threads/threads-page-client.test.tsx
T
2026-09-12 16:54:56 +02:00

114 lines
3.8 KiB
TypeScript

import { cleanup, render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTaskRemovalState } from "@/lib/state/task-removal";
const bootstrapMock = vi.hoisted(() => vi.fn());
const searchMock = vi.hoisted(() => ({ value: "" }));
const snapshotsMock = vi.hoisted(() => vi.fn());
const displaySettingsMock = vi.hoisted(() => ({
activeWorkspaceId: "stored-workspace" as string | null,
activeWorkflowId: null as string | null,
workspaces: [] as Array<{ id: string }>,
workflows: [] as Array<{ id: string; workspaceId: string }>,
}));
vi.mock("@/lib/routing/client-router", () => ({
useRouter: () => ({ push: vi.fn() }),
useSearchParams: () => new URLSearchParams(searchMock.value),
}));
vi.mock("@/src/kanban-route", () => ({ useKanbanRouteBootstrap: bootstrapMock }));
vi.mock("@/components/kanban/kanban-header", () => ({ KanbanHeader: () => null }));
vi.mock("@/components/threads/threads-board", () => ({ ThreadsBoard: () => null }));
vi.mock("@/hooks/domains/kanban/use-all-workflow-snapshots", () => ({
useAllWorkflowSnapshots: snapshotsMock,
}));
vi.mock("@/hooks/use-kanban-display-settings", () => ({
useKanbanDisplaySettings: () => displaySettingsMock,
}));
vi.mock("@/hooks/use-task-listing-view", () => ({
useTaskListingView: () => ({ setView: vi.fn() }),
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: unknown) => unknown) =>
selector({
kanbanMulti: { snapshots: {}, isLoading: false },
taskRemoval: createTaskRemovalState(),
}),
}));
import { scopeSnapshotsToWorkspace, ThreadsPageClient } from "./threads-page-client";
afterEach(() => {
cleanup();
bootstrapMock.mockReset();
snapshotsMock.mockReset();
searchMock.value = "";
});
beforeEach(() => {
snapshotsMock.mockImplementation(() => ({ refresh: vi.fn() }));
displaySettingsMock.workspaces = [];
displaySettingsMock.workflows = [];
});
describe("ThreadsPageClient — workspace deep links", () => {
it("bootstraps the workspace the link asked for, not the stored one", () => {
searchMock.value = "workspace=requested-workspace";
render(<ThreadsPageClient />);
expect(bootstrapMock).toHaveBeenCalledWith(
expect.objectContaining({ workspaceId: "requested-workspace" }),
false,
);
});
it("uses the stored workspace when the link names an unknown workspace", () => {
displaySettingsMock.workspaces = [{ id: "ws-a" }];
searchMock.value = "workspace=UNKNOWN";
render(<ThreadsPageClient />);
expect(snapshotsMock).toHaveBeenCalledWith("stored-workspace");
});
it("scopes snapshots to the requested workspace before effects clear stale data", () => {
const snapshots = {
"wf-old": { workflowId: "wf-old", workflowName: "Old", steps: [], tasks: [] },
"wf-new": { workflowId: "wf-new", workflowName: "New", steps: [], tasks: [] },
};
expect(
Object.keys(
scopeSnapshotsToWorkspace(
snapshots,
[
{ id: "wf-old", workspaceId: "workspace-old" },
{ id: "wf-new", workspaceId: "workspace-new" },
],
"workspace-new",
),
),
).toEqual(["wf-new"]);
});
it("leaves the workspace unset when the link names none", () => {
render(<ThreadsPageClient />);
expect(bootstrapMock).toHaveBeenCalledWith(
expect.objectContaining({ workspaceId: undefined }),
false,
);
});
it("keeps one bootstrap route identity across re-renders of the same link", () => {
searchMock.value = "workspace=requested-workspace";
const { rerender } = render(<ThreadsPageClient />);
rerender(<ThreadsPageClient />);
const [first] = bootstrapMock.mock.calls[0];
const [second] = bootstrapMock.mock.calls[bootstrapMock.mock.calls.length - 1];
expect(first).toBe(second);
});
});