Files
kandev/apps/web/hooks/domains/settings/use-workflow-settings.test.ts
T
2026-05-17 20:16:15 +01:00

186 lines
6.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import {
workflowId as toWorkflowId,
workspaceId as toWorkspaceId,
type Workflow,
} from "@/lib/types/http";
type StoreWorkflow = {
id: string;
workspaceId: string;
name: string;
description?: string | null;
hidden?: boolean;
};
type MockState = { workflows: { items: StoreWorkflow[] } };
let mockState: MockState = { workflows: { items: [] } };
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (s: MockState) => unknown) => selector(mockState),
}));
import { useWorkflowSettings } from "./use-workflow-settings";
function setStore(items: StoreWorkflow[]) {
mockState = { workflows: { items } };
}
const wf = (id: string, wsId: string, name: string): Workflow => ({
id: toWorkflowId(id),
workspace_id: toWorkspaceId(wsId),
name,
description: "",
created_at: "",
updated_at: "",
});
const NAME_A1 = "Workflow A1";
const NAME_B1 = "Workflow B1";
const STORE_A1: StoreWorkflow = { id: "wf-a1", workspaceId: "ws-a", name: NAME_A1 };
const STORE_B1: StoreWorkflow = { id: "wf-b1", workspaceId: "ws-b", name: NAME_B1 };
beforeEach(() => {
setStore([]);
});
describe("useWorkflowSettings", () => {
it("does not include workflows from other workspaces present in the global store", () => {
// Store has a workflow from workspace A (e.g. user previously visited it)
setStore([STORE_A1]);
// We render the settings hook for workspace B with no initial workflows
const { result } = renderHook(() => useWorkflowSettings([], "ws-b"));
// The leaked workflow from workspace A must not appear in B's list
expect(result.current.workflowItems).toHaveLength(0);
expect(result.current.savedWorkflowItems).toHaveLength(0);
});
it("adds workflows from the store that belong to the current workspace", () => {
setStore([STORE_A1, STORE_B1]);
const { result } = renderHook(() => useWorkflowSettings([], "ws-b"));
expect(result.current.workflowItems.map((w) => w.id)).toEqual(["wf-b1"]);
});
it("does not remove a workspace's workflows when an unrelated workspace's entries are added/removed in the store", () => {
// Initial: workspace B has one saved workflow from SSR
const initial = [wf("wf-b1", "ws-b", NAME_B1)];
setStore([STORE_B1]);
const { result, rerender } = renderHook(
({ store }: { store: StoreWorkflow[] }) => {
setStore(store);
return useWorkflowSettings(initial, "ws-b");
},
{ initialProps: { store: [STORE_B1] } },
);
expect(result.current.workflowItems.map((w) => w.id)).toEqual(["wf-b1"]);
// Workspace A workflow is added to the store (e.g. WS event from another tab)
act(() => {
rerender({ store: [STORE_B1, STORE_A1] });
});
expect(result.current.workflowItems.map((w) => w.id)).toEqual(["wf-b1"]);
// Workspace A workflow is removed from the store — must not affect B's list
act(() => {
rerender({ store: [STORE_B1] });
});
expect(result.current.workflowItems.map((w) => w.id)).toEqual(["wf-b1"]);
});
it("falls back to the unscoped store when no workspaceId is provided", () => {
setStore([STORE_A1, STORE_B1]);
const { result } = renderHook(() => useWorkflowSettings([]));
expect(result.current.workflowItems.map((w) => w.id).sort()).toEqual(["wf-a1", "wf-b1"]);
});
it("syncs name updates from the store within the current workspace", () => {
const initial = [wf("wf-b1", "ws-b", NAME_B1)];
setStore([STORE_B1]);
const { result, rerender } = renderHook(
({ store }: { store: StoreWorkflow[] }) => {
setStore(store);
return useWorkflowSettings(initial, "ws-b");
},
{ initialProps: { store: [STORE_B1] } },
);
expect(result.current.workflowItems[0].name).toEqual(NAME_B1);
act(() => {
rerender({ store: [{ id: "wf-b1", workspaceId: "ws-b", name: "Renamed B1" }] });
});
expect(result.current.workflowItems[0].name).toEqual("Renamed B1");
});
it("excludes hidden system workflows from the settings list", () => {
// System workflows like "Improve Kandev" live in the global store with
// hidden=true so the kanban can resolve task references, but they must
// never appear in the management UI.
const HIDDEN_SYSTEM: StoreWorkflow = {
id: "wf-improve-kandev",
workspaceId: "ws-b",
name: "Improve Kandev",
hidden: true,
};
setStore([STORE_B1, HIDDEN_SYSTEM]);
const { result } = renderHook(() => useWorkflowSettings([], "ws-b"));
expect(result.current.workflowItems.map((w) => w.id)).toEqual(["wf-b1"]);
expect(result.current.savedWorkflowItems.map((w) => w.id)).toEqual(["wf-b1"]);
});
it("drops a workflow from the settings list once it becomes hidden", () => {
const initial = [wf("wf-b1", "ws-b", NAME_B1)];
setStore([STORE_B1]);
const { result, rerender } = renderHook(
({ store }: { store: StoreWorkflow[] }) => {
setStore(store);
return useWorkflowSettings(initial, "ws-b");
},
{ initialProps: { store: [STORE_B1] } },
);
expect(result.current.workflowItems.map((w) => w.id)).toEqual(["wf-b1"]);
// Backend flips hidden=true (e.g. healing the improve-kandev record).
act(() => {
rerender({ store: [{ ...STORE_B1, hidden: true }] });
});
expect(result.current.workflowItems.map((w) => w.id)).toEqual([]);
});
it("starts scoping store entries once a workspaceId becomes defined", () => {
setStore([STORE_A1, STORE_B1]);
const { result, rerender } = renderHook(
({ workspaceId }: { workspaceId?: string }) => useWorkflowSettings([], workspaceId),
{ initialProps: { workspaceId: undefined as string | undefined } },
);
// No workspaceId → unscoped fallback shows both
expect(result.current.workflowItems.map((w) => w.id).sort()).toEqual(["wf-a1", "wf-b1"]);
act(() => {
rerender({ workspaceId: "ws-b" });
});
// Once scoped to B, A's workflow is dropped
expect(result.current.workflowItems.map((w) => w.id)).toEqual(["wf-b1"]);
});
});