580 lines
18 KiB
TypeScript
580 lines
18 KiB
TypeScript
import { act, renderHook } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
createWorkflowAction,
|
|
createWorkflowStepAction,
|
|
deleteWorkflowStepAction,
|
|
getWorkflowTaskCount,
|
|
getStepTaskCount,
|
|
reorderWorkflowStepsAction,
|
|
updateWorkflowAction,
|
|
updateWorkflowStepAction,
|
|
} from "@/app/actions/workspaces";
|
|
import type { Workflow, WorkflowStep } from "@/lib/types/http";
|
|
import {
|
|
useWorkflowDeleteHandlers,
|
|
useStepDeleteHandlers,
|
|
createWorkflowDraftSaveProgress,
|
|
persistWorkflowDraft,
|
|
useWorkflowStepActions,
|
|
areStepDraftsEqual,
|
|
} from "./workflow-card-actions";
|
|
|
|
vi.mock("@/app/actions/workspaces", () => ({
|
|
createWorkflowAction: vi.fn(),
|
|
createWorkflowStepAction: vi.fn(),
|
|
deleteWorkflowAction: vi.fn(),
|
|
updateWorkflowAction: vi.fn(),
|
|
updateWorkflowStepAction: vi.fn(),
|
|
deleteWorkflowStepAction: vi.fn(),
|
|
reorderWorkflowStepsAction: vi.fn(),
|
|
listWorkflowStepsAction: vi.fn(),
|
|
getStepTaskCount: vi.fn(),
|
|
getWorkflowTaskCount: vi.fn(),
|
|
exportWorkflowAction: vi.fn(),
|
|
bulkMoveTasks: vi.fn(),
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
const workflow = {
|
|
id: "wf-1",
|
|
workspace_id: "ws-1",
|
|
name: "Workflow",
|
|
created_at: "",
|
|
updated_at: "",
|
|
} as Workflow;
|
|
const CLIENT_WORKFLOW_ID = "temp-workflow-1";
|
|
const CLIENT_STEP_ONE = "temp-step-1";
|
|
const CLIENT_STEP_TWO = "temp-step-2";
|
|
const SERVER_STEP_ONE = "server-step-1";
|
|
const SERVER_STEP_TWO = "server-step-2";
|
|
|
|
function step(id: string, name: string, position: number, isStartStep: boolean): WorkflowStep {
|
|
return {
|
|
id,
|
|
workflow_id: workflow.id,
|
|
name,
|
|
position,
|
|
color: "bg-slate-500",
|
|
allow_manual_move: true,
|
|
is_start_step: isStartStep,
|
|
created_at: "",
|
|
updated_at: "",
|
|
};
|
|
}
|
|
|
|
function renderReadOnlyWorkflowStepActions(initialSteps: WorkflowStep[]) {
|
|
let steps = initialSteps;
|
|
const setWorkflowSteps = vi.fn(
|
|
(updater: ((prev: WorkflowStep[]) => WorkflowStep[]) | WorkflowStep[]) => {
|
|
steps = typeof updater === "function" ? updater(steps) : updater;
|
|
},
|
|
);
|
|
const refreshWorkflowSteps = vi.fn();
|
|
const view = renderHook(() =>
|
|
useWorkflowStepActions({
|
|
workflow,
|
|
readOnly: true,
|
|
workflowSteps: steps,
|
|
setWorkflowSteps,
|
|
refreshWorkflowSteps,
|
|
setStepToDelete: vi.fn(),
|
|
setStepTaskCount: vi.fn(),
|
|
setTargetStepForMigration: vi.fn(),
|
|
setStepDeleteOpen: vi.fn(),
|
|
toast: vi.fn(),
|
|
}),
|
|
);
|
|
return { ...view, getSteps: () => steps, refreshWorkflowSteps, setWorkflowSteps };
|
|
}
|
|
|
|
describe("useWorkflowStepActions", () => {
|
|
it("updates a step locally without calling persistence APIs", async () => {
|
|
const original = step("step-1", "Todo", 0, true);
|
|
const setWorkflowSteps = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useWorkflowStepActions({
|
|
workflow,
|
|
readOnly: false,
|
|
workflowSteps: [original],
|
|
setWorkflowSteps,
|
|
refreshWorkflowSteps: vi.fn(),
|
|
setStepToDelete: vi.fn(),
|
|
setStepTaskCount: vi.fn(),
|
|
setTargetStepForMigration: vi.fn(),
|
|
setStepDeleteOpen: vi.fn(),
|
|
toast: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.handleUpdateWorkflowStep("step-1", { name: "Renamed" });
|
|
});
|
|
|
|
const updater = setWorkflowSteps.mock.calls[0][0] as (steps: WorkflowStep[]) => WorkflowStep[];
|
|
expect(updater([original])[0].name).toBe("Renamed");
|
|
expect(updateWorkflowStepAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("adds a client-only step without calling persistence APIs", async () => {
|
|
const setWorkflowSteps = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useWorkflowStepActions({
|
|
workflow,
|
|
workflowSteps: [step("step-1", "Todo", 0, true)],
|
|
setWorkflowSteps,
|
|
refreshWorkflowSteps: vi.fn(),
|
|
setStepToDelete: vi.fn(),
|
|
setStepTaskCount: vi.fn(),
|
|
setTargetStepForMigration: vi.fn(),
|
|
setStepDeleteOpen: vi.fn(),
|
|
toast: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
await act(() => result.current.handleAddWorkflowStep());
|
|
|
|
const updater = setWorkflowSteps.mock.calls[0][0] as (steps: WorkflowStep[]) => WorkflowStep[];
|
|
expect(updater([])[0]).toMatchObject({
|
|
id: expect.stringMatching(/^temp-step-/),
|
|
name: "New Step",
|
|
});
|
|
expect(createWorkflowStepAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("adds a client-only step when crypto.randomUUID is unavailable", async () => {
|
|
vi.stubGlobal("crypto", {});
|
|
const setWorkflowSteps = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useWorkflowStepActions({
|
|
workflow,
|
|
workflowSteps: [step("step-1", "Todo", 0, true)],
|
|
setWorkflowSteps,
|
|
refreshWorkflowSteps: vi.fn(),
|
|
setStepToDelete: vi.fn(),
|
|
setStepTaskCount: vi.fn(),
|
|
setTargetStepForMigration: vi.fn(),
|
|
setStepDeleteOpen: vi.fn(),
|
|
toast: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
await act(() => result.current.handleAddWorkflowStep());
|
|
|
|
const updater = setWorkflowSteps.mock.calls[0][0] as (steps: WorkflowStep[]) => WorkflowStep[];
|
|
expect(updater([])[0]).toMatchObject({
|
|
id: expect.stringMatching(/^temp-step-[0-9a-f-]{36}$/),
|
|
name: "New Step",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("workflow step cancel completion policy", () => {
|
|
it("treats a persisted policy change as a dirty draft", () => {
|
|
const saved = step("step-1", "Todo", 0, true);
|
|
const draft = { ...saved, cancel_triggers_turn_complete: true } as WorkflowStep;
|
|
|
|
expect(areStepDraftsEqual(saved, draft)).toBe(false);
|
|
expect(areStepDraftsEqual(draft, { ...draft })).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("useWorkflowStepActions destructive and ordering paths", () => {
|
|
it("refuses to add, update, remove, or reorder steps when readOnly", async () => {
|
|
const initialSteps = [step("step-1", "Todo", 0, true), step("step-2", "Plan", 1, false)];
|
|
const { result, getSteps, refreshWorkflowSteps } =
|
|
renderReadOnlyWorkflowStepActions(initialSteps);
|
|
|
|
await act(async () => {
|
|
await result.current.handleAddWorkflowStep();
|
|
});
|
|
await act(async () => {
|
|
await result.current.handleUpdateWorkflowStep("step-2", { name: "Renamed" });
|
|
});
|
|
await act(async () => {
|
|
await result.current.handleRemoveWorkflowStep("step-1");
|
|
});
|
|
await act(async () => {
|
|
await result.current.handleReorderWorkflowSteps([initialSteps[1], initialSteps[0]]);
|
|
});
|
|
|
|
expect(getSteps()).toEqual(initialSteps);
|
|
expect(createWorkflowStepAction).not.toHaveBeenCalled();
|
|
expect(updateWorkflowStepAction).not.toHaveBeenCalled();
|
|
expect(deleteWorkflowStepAction).not.toHaveBeenCalled();
|
|
expect(reorderWorkflowStepsAction).not.toHaveBeenCalled();
|
|
expect(refreshWorkflowSteps).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("opens the task migration dialog without reporting a saved mutation", async () => {
|
|
vi.mocked(getStepTaskCount).mockResolvedValue({ task_count: 2 });
|
|
const setStepDeleteOpen = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useWorkflowStepActions({
|
|
workflow,
|
|
readOnly: false,
|
|
workflowSteps: [step("step-1", "Todo", 0, true), step("step-2", "Done", 1, false)],
|
|
setWorkflowSteps: vi.fn(),
|
|
refreshWorkflowSteps: vi.fn(),
|
|
setStepToDelete: vi.fn(),
|
|
setStepTaskCount: vi.fn(),
|
|
setTargetStepForMigration: vi.fn(),
|
|
setStepDeleteOpen,
|
|
toast: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.handleRemoveWorkflowStep("step-1");
|
|
});
|
|
|
|
expect(setStepDeleteOpen).toHaveBeenCalledWith(true);
|
|
expect(result.current.status).toBe("idle");
|
|
});
|
|
|
|
it("requires confirmation before deleting a persisted step with no tasks", async () => {
|
|
vi.mocked(getStepTaskCount).mockResolvedValue({ task_count: 0 });
|
|
const setStepDeleteOpen = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useWorkflowStepActions({
|
|
workflow,
|
|
workflowSteps: [step("step-1", "Todo", 0, true)],
|
|
setWorkflowSteps: vi.fn(),
|
|
refreshWorkflowSteps: vi.fn(),
|
|
setStepToDelete: vi.fn(),
|
|
setStepTaskCount: vi.fn(),
|
|
setTargetStepForMigration: vi.fn(),
|
|
setStepDeleteOpen,
|
|
toast: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
await act(() => result.current.handleRemoveWorkflowStep("step-1"));
|
|
|
|
expect(setStepDeleteOpen).toHaveBeenCalledWith(true);
|
|
expect(deleteWorkflowStepAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reorders locally without calling persistence APIs", async () => {
|
|
const originalSteps = [step("step-1", "Todo", 0, true), step("step-2", "Done", 1, false)];
|
|
const reorderedSteps = [originalSteps[1], originalSteps[0]];
|
|
const setWorkflowSteps = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useWorkflowStepActions({
|
|
workflow,
|
|
workflowSteps: originalSteps,
|
|
setWorkflowSteps,
|
|
refreshWorkflowSteps: vi.fn().mockResolvedValue(undefined),
|
|
setStepToDelete: vi.fn(),
|
|
setStepTaskCount: vi.fn(),
|
|
setTargetStepForMigration: vi.fn(),
|
|
setStepDeleteOpen: vi.fn(),
|
|
toast: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.handleReorderWorkflowSteps(reorderedSteps);
|
|
});
|
|
expect(setWorkflowSteps).toHaveBeenCalledWith(
|
|
reorderedSteps.map((item, position) => ({ ...item, position })),
|
|
);
|
|
expect(reorderWorkflowStepsAction).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("persistWorkflowDraft", () => {
|
|
const persistedWorkflow = { ...workflow, id: "wf-created" } as Workflow;
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(createWorkflowAction).mockResolvedValue(persistedWorkflow);
|
|
vi.mocked(updateWorkflowAction).mockResolvedValue(persistedWorkflow);
|
|
vi.mocked(updateWorkflowStepAction).mockImplementation(async (id, updates) => ({
|
|
...step(id, updates.name ?? "Step", updates.position ?? 0, updates.is_start_step ?? false),
|
|
...updates,
|
|
}));
|
|
vi.mocked(reorderWorkflowStepsAction).mockResolvedValue({ steps: [], total: 0 });
|
|
});
|
|
|
|
it("does not duplicate a workflow or successful steps when a partial create is retried", async () => {
|
|
const draftWorkflow = { ...workflow, id: CLIENT_WORKFLOW_ID } as Workflow;
|
|
const drafts = [
|
|
step(CLIENT_STEP_ONE, "Todo", 0, true),
|
|
step(CLIENT_STEP_TWO, "Done", 1, false),
|
|
].map((item) => ({ ...item, workflow_id: draftWorkflow.id }) as WorkflowStep);
|
|
vi.mocked(createWorkflowStepAction)
|
|
.mockResolvedValueOnce(step(SERVER_STEP_ONE, "Todo", 0, true))
|
|
.mockRejectedValueOnce(new Error("network down"))
|
|
.mockResolvedValueOnce(step(SERVER_STEP_TWO, "Done", 1, false));
|
|
const progress = createWorkflowDraftSaveProgress();
|
|
|
|
await expect(
|
|
persistWorkflowDraft({
|
|
workflow: draftWorkflow,
|
|
draftSteps: drafts,
|
|
savedSteps: [],
|
|
progress,
|
|
}),
|
|
).rejects.toThrow("network down");
|
|
await persistWorkflowDraft({
|
|
workflow: draftWorkflow,
|
|
draftSteps: drafts,
|
|
savedSteps: [],
|
|
progress,
|
|
});
|
|
|
|
expect(createWorkflowAction).toHaveBeenCalledOnce();
|
|
expect(createWorkflowStepAction).toHaveBeenCalledTimes(3);
|
|
expect(progress.stepIds.get(CLIENT_STEP_ONE)).toBe(SERVER_STEP_ONE);
|
|
expect(progress.stepIds.get(CLIENT_STEP_TWO)).toBe(SERVER_STEP_TWO);
|
|
});
|
|
|
|
it("remaps draft step references before updating the server", async () => {
|
|
const draftWorkflow = { ...workflow, id: CLIENT_WORKFLOW_ID } as Workflow;
|
|
const drafts = [
|
|
{
|
|
...step(CLIENT_STEP_ONE, "Todo", 0, true),
|
|
workflow_id: draftWorkflow.id,
|
|
events: {
|
|
on_turn_complete: [{ type: "move_to_step", config: { step_id: CLIENT_STEP_TWO } }],
|
|
},
|
|
},
|
|
{
|
|
...step(CLIENT_STEP_TWO, "Done", 1, false),
|
|
workflow_id: draftWorkflow.id,
|
|
pull_from_step_id: CLIENT_STEP_ONE,
|
|
},
|
|
] as WorkflowStep[];
|
|
vi.mocked(createWorkflowStepAction)
|
|
.mockResolvedValueOnce(step(SERVER_STEP_ONE, "Todo", 0, true))
|
|
.mockResolvedValueOnce(step(SERVER_STEP_TWO, "Done", 1, false));
|
|
|
|
await persistWorkflowDraft({
|
|
workflow: draftWorkflow,
|
|
draftSteps: drafts,
|
|
savedSteps: [],
|
|
progress: createWorkflowDraftSaveProgress(),
|
|
});
|
|
|
|
expect(updateWorkflowStepAction).toHaveBeenCalledWith(
|
|
SERVER_STEP_ONE,
|
|
expect.objectContaining({
|
|
events: {
|
|
on_turn_complete: [{ type: "move_to_step", config: { step_id: SERVER_STEP_TWO } }],
|
|
},
|
|
}),
|
|
);
|
|
expect(updateWorkflowStepAction).toHaveBeenCalledWith(
|
|
SERVER_STEP_TWO,
|
|
expect.objectContaining({ pull_from_step_id: SERVER_STEP_ONE }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("persistWorkflowDraft cancellation policy", () => {
|
|
it("forwards step session policy and cancellation policy when creating a missing step", async () => {
|
|
const draftWorkflow = { ...workflow, id: CLIENT_WORKFLOW_ID } as Workflow;
|
|
const drafts = [
|
|
{
|
|
...step(CLIENT_STEP_ONE, "Todo", 0, true),
|
|
workflow_id: draftWorkflow.id,
|
|
cancel_triggers_turn_complete: true,
|
|
profile_session_start_policy: "new",
|
|
profile_session_end_policy: "park",
|
|
},
|
|
] as WorkflowStep[];
|
|
vi.mocked(createWorkflowAction).mockResolvedValue({
|
|
...workflow,
|
|
id: "wf-created",
|
|
} as Workflow);
|
|
vi.mocked(updateWorkflowAction).mockResolvedValue({
|
|
...workflow,
|
|
id: "wf-created",
|
|
} as Workflow);
|
|
vi.mocked(createWorkflowStepAction).mockResolvedValueOnce(
|
|
step(SERVER_STEP_ONE, "Todo", 0, true),
|
|
);
|
|
|
|
await persistWorkflowDraft({
|
|
workflow: draftWorkflow,
|
|
draftSteps: drafts,
|
|
savedSteps: [],
|
|
progress: createWorkflowDraftSaveProgress(),
|
|
});
|
|
|
|
expect(createWorkflowStepAction).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
cancel_triggers_turn_complete: true,
|
|
profile_session_start_policy: "new",
|
|
profile_session_end_policy: "park",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("useWorkflowDeleteHandlers", () => {
|
|
it("refuses to open the delete-workflow dialog when readOnly", async () => {
|
|
const wfDel = {
|
|
setDeleteOpen: vi.fn(),
|
|
setWorkflowTaskCount: vi.fn(),
|
|
setWorkflowDeleteLoading: vi.fn(),
|
|
setTargetWorkflowId: vi.fn(),
|
|
setTargetWorkflowSteps: vi.fn(),
|
|
setTargetStepId: vi.fn(),
|
|
targetWorkflowId: "",
|
|
targetStepId: "",
|
|
setMigrateLoading: vi.fn(),
|
|
};
|
|
const { result } = renderHook(() =>
|
|
useWorkflowDeleteHandlers({
|
|
workflow,
|
|
readOnly: true,
|
|
otherWorkflows: [],
|
|
wfDel,
|
|
deleteWorkflowRun: vi.fn(),
|
|
toast: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.handleDeleteWorkflowClick();
|
|
});
|
|
|
|
expect(getWorkflowTaskCount).not.toHaveBeenCalled();
|
|
expect(wfDel.setDeleteOpen).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("useStepDeleteHandlers", () => {
|
|
it("closes a failed delete and defers loading until its retry executes", async () => {
|
|
const setStepDeleteOpen = vi.fn();
|
|
const setStepToDelete = vi.fn();
|
|
const setStepMigrateLoading = vi.fn();
|
|
const setStepDeletePending = vi.fn();
|
|
let failedOperation: (() => Promise<void>) | undefined;
|
|
const { result } = renderHook(() =>
|
|
useStepDeleteHandlers({
|
|
workflow,
|
|
stepDel: {
|
|
stepToDelete: "step-1",
|
|
targetStepForMigration: "step-2",
|
|
setStepMigrateLoading,
|
|
setStepDeletePending,
|
|
setStepDeleteOpen,
|
|
setStepToDelete,
|
|
},
|
|
refreshWorkflowSteps: vi.fn(),
|
|
runMutation: vi.fn().mockImplementation(async (operation: () => Promise<void>) => {
|
|
failedOperation = operation;
|
|
return false;
|
|
}),
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.handleDeleteStepAndTasks();
|
|
});
|
|
|
|
expect(setStepToDelete).toHaveBeenCalledWith(null);
|
|
expect(setStepDeleteOpen).toHaveBeenCalledWith(false);
|
|
expect(setStepMigrateLoading).not.toHaveBeenCalled();
|
|
expect(setStepDeletePending.mock.calls).toEqual([[true], [false]]);
|
|
setStepToDelete.mockClear();
|
|
setStepDeleteOpen.mockClear();
|
|
|
|
await act(async () => {
|
|
await failedOperation?.();
|
|
});
|
|
|
|
expect(setStepMigrateLoading.mock.calls).toEqual([[true], [false]]);
|
|
expect(setStepToDelete).toHaveBeenCalledWith(null);
|
|
expect(setStepDeleteOpen).toHaveBeenCalledWith(false);
|
|
});
|
|
});
|
|
|
|
describe("useStepDeleteHandlers retry safety", () => {
|
|
it("ignores duplicate delete submissions while the first is queued", async () => {
|
|
let finishMutation!: (saved: boolean) => void;
|
|
const runMutation = vi.fn(
|
|
() =>
|
|
new Promise<boolean>((resolve) => {
|
|
finishMutation = resolve;
|
|
}),
|
|
);
|
|
const setStepDeleteOpen = vi.fn();
|
|
const { result } = renderHook(() =>
|
|
useStepDeleteHandlers({
|
|
workflow,
|
|
stepDel: {
|
|
stepToDelete: "step-1",
|
|
targetStepForMigration: "step-2",
|
|
setStepMigrateLoading: vi.fn(),
|
|
setStepDeletePending: vi.fn(),
|
|
setStepDeleteOpen,
|
|
setStepToDelete: vi.fn(),
|
|
},
|
|
refreshWorkflowSteps: vi.fn(),
|
|
runMutation,
|
|
}),
|
|
);
|
|
|
|
let firstSubmission!: Promise<void>;
|
|
act(() => {
|
|
firstSubmission = result.current.handleDeleteStepAndTasks();
|
|
});
|
|
await act(async () => {
|
|
await result.current.handleDeleteStepAndTasks();
|
|
});
|
|
|
|
expect(runMutation).toHaveBeenCalledOnce();
|
|
|
|
await act(async () => {
|
|
finishMutation(false);
|
|
await firstSubmission;
|
|
});
|
|
expect(setStepDeleteOpen).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it("retries only refresh after a completed delete", async () => {
|
|
const refreshWorkflowSteps = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error("refresh failed"))
|
|
.mockResolvedValue(undefined);
|
|
const runMutation = vi.fn(async (operation: () => Promise<void>) => {
|
|
try {
|
|
await operation();
|
|
} catch {
|
|
await operation();
|
|
}
|
|
return true;
|
|
});
|
|
const { result } = renderHook(() =>
|
|
useStepDeleteHandlers({
|
|
workflow,
|
|
stepDel: {
|
|
stepToDelete: "step-1",
|
|
targetStepForMigration: "step-2",
|
|
setStepMigrateLoading: vi.fn(),
|
|
setStepDeletePending: vi.fn(),
|
|
setStepDeleteOpen: vi.fn(),
|
|
setStepToDelete: vi.fn(),
|
|
},
|
|
refreshWorkflowSteps,
|
|
runMutation,
|
|
}),
|
|
);
|
|
|
|
await act(async () => result.current.handleDeleteStepAndTasks());
|
|
|
|
expect(deleteWorkflowStepAction).toHaveBeenCalledOnce();
|
|
expect(refreshWorkflowSteps).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|