diff --git a/canvas/src/components/__tests__/ApprovalBanner.test.tsx b/canvas/src/components/__tests__/ApprovalBanner.test.tsx index 9d97ef5a..bf2fcf8d 100644 --- a/canvas/src/components/__tests__/ApprovalBanner.test.tsx +++ b/canvas/src/components/__tests__/ApprovalBanner.test.tsx @@ -4,9 +4,14 @@ * * Covers: renders nothing when no approvals, polls /approvals/pending, * shows approval cards, approve/deny decisions, toast notifications. + * + * Note: does NOT mock @/lib/api — uses vi.spyOn on the real module. + * vi.restoreAllMocks() is omitted from afterEach so queued mock values + * (set up via mockResolvedValueOnce in beforeEach) are preserved for the + * component's useEffect to consume. */ import React from "react"; -import { render, screen, fireEvent, cleanup, waitFor, act } from "@testing-library/react"; +import { render, screen, fireEvent, cleanup, act } from "@testing-library/react"; import { afterEach, describe, expect, it, vi, beforeEach } from "vitest"; import { ApprovalBanner } from "../ApprovalBanner"; import { showToast } from "@/components/Toaster"; @@ -45,13 +50,13 @@ describe("ApprovalBanner — empty state", () => { }); afterEach(() => { - vi.restoreAllMocks(); + cleanup(); vi.useRealTimers(); + vi.restoreAllMocks(); }); it("renders nothing when there are no pending approvals", async () => { render(); - // Wait for the initial useEffect + api.get to resolve await act(async () => { await vi.runOnlyPendingTimersAsync(); }); expect(screen.queryByRole("alert")).toBeNull(); }); @@ -74,8 +79,9 @@ describe("ApprovalBanner — renders approval cards", () => { }); afterEach(() => { - vi.restoreAllMocks(); + cleanup(); vi.useRealTimers(); + vi.restoreAllMocks(); }); it("renders an alert card for each pending approval", async () => { @@ -188,7 +194,7 @@ describe("ApprovalBanner — decisions", () => { }); it("shows an error toast when POST fails", async () => { - vi.spyOn(api, "post").mockRejectedValue(new Error("Network error")); + vi.mocked(api.post).mockRejectedValueOnce(new Error("Network error")); render(); await act(async () => { await vi.runOnlyPendingTimersAsync(); }); fireEvent.click(screen.getAllByRole("button", { name: /approve/i })[0]); @@ -200,7 +206,8 @@ describe("ApprovalBanner — decisions", () => { }); it("keeps the card visible when the POST fails", async () => { - vi.spyOn(api, "post").mockRejectedValue(new Error("Network error")); + // Use mockRejectedValueOnce on the same spy as beforeEach (don't call spyOn again) + vi.mocked(api.post).mockRejectedValueOnce(new Error("Network error")); render(); await act(async () => { await vi.runOnlyPendingTimersAsync(); }); fireEvent.click(screen.getAllByRole("button", { name: /approve/i })[0]); @@ -216,8 +223,9 @@ describe("ApprovalBanner — handles empty list from server", () => { }); afterEach(() => { - vi.restoreAllMocks(); + cleanup(); vi.useRealTimers(); + vi.restoreAllMocks(); }); it("shows nothing when the API returns an empty array on first poll", async () => { @@ -225,4 +233,4 @@ describe("ApprovalBanner — handles empty list from server", () => { await act(async () => { await vi.runOnlyPendingTimersAsync(); }); expect(screen.queryByRole("alert")).toBeNull(); }); -}); \ No newline at end of file +});