From daecab6d6dac0f456b8a0bd4e2297a3ec79f8384 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-FE Date: Mon, 11 May 2026 10:50:16 +0000 Subject: [PATCH] fix(canvas/test): fix ApprovalBanner POST error test isolation The "keeps the card visible when the POST fails" test was failing in the full suite due to cross-test mock pollution from ActivityTab.test.tsx. Root cause: ActivityTab.test.tsx mocks @/lib/api with vi.fn() mocks (mockGet, mockPost) that persist across test files. When ApprovalBanner's decisions beforeEach creates a vi.spyOn on api.post, it wraps ActivityTab's mockPost. Calling mockRejectedValueOnce() on the spy queues after ActivityTab's mockResolvedValue({}) implementation, but in certain test orderings the queue ordering was unreliable. Fix: use mockPost.mockReset().mockImplementation(() => Promise.reject(...)) to atomically clear the beforeEach mock setup and set a permanent rejection, rather than relying on mockRejectedValueOnce which queues behind the existing implementation. Also: store mockGet and mockPost in module-level variables so error tests can manipulate them without creating duplicate spies. Co-Authored-By: Molecule AI Core-FE --- .../__tests__/ApprovalBanner.test.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/canvas/src/components/__tests__/ApprovalBanner.test.tsx b/canvas/src/components/__tests__/ApprovalBanner.test.tsx index 09817ef9..f2b2bfc5 100644 --- a/canvas/src/components/__tests__/ApprovalBanner.test.tsx +++ b/canvas/src/components/__tests__/ApprovalBanner.test.tsx @@ -41,9 +41,10 @@ const pendingApproval = (id = "a1", workspaceId = "ws-1"): { created_at: "2026-05-10T10:00:00Z", }); -// Shared spy reference so individual tests can call mockGet.mockRestore() -// without needing to pass it through beforeEach → it scope chain. +// Shared spy references so individual tests can reset or reject the POST mock +// without needing to call spyOn again (which would create a duplicate spy). let mockGet: ReturnType; +let mockPost: ReturnType; // ─── Tests ──────────────────────────────────────────────────────────────────── @@ -139,8 +140,8 @@ describe("ApprovalBanner — renders approval cards", () => { describe("ApprovalBanner — decisions", () => { beforeEach(() => { vi.useFakeTimers(); - vi.spyOn(api, "get").mockResolvedValueOnce([pendingApproval("a1")]); - vi.spyOn(api, "post").mockResolvedValue({}); + mockGet = vi.spyOn(api, "get").mockResolvedValueOnce([pendingApproval("a1")]); + mockPost = vi.spyOn(api, "post").mockResolvedValue({}); }); afterEach(() => { @@ -196,7 +197,8 @@ describe("ApprovalBanner — decisions", () => { }); it("shows an error toast when POST fails", async () => { - vi.mocked(api.post).mockRejectedValueOnce(new Error("Network error")); + mockPost.mockReset(); + mockPost.mockImplementation(() => Promise.reject(new Error("Network error"))); render(); await act(async () => { await vi.runOnlyPendingTimersAsync(); }); fireEvent.click(screen.getAllByRole("button", { name: /approve/i })[0]); @@ -208,8 +210,10 @@ describe("ApprovalBanner — decisions", () => { }); it("keeps the card visible when the POST fails", async () => { - // Use mockRejectedValueOnce on the same spy as beforeEach (don't call spyOn again) - vi.mocked(api.post).mockRejectedValueOnce(new Error("Network error")); + // Reset the post mock before rejecting so the beforeEach's resolved value + // is gone and we get a clean rejection instead of a resolved→rejected queue. + mockPost.mockReset(); + mockPost.mockImplementation(() => Promise.reject(new Error("Network error"))); render(); await act(async () => { await vi.runOnlyPendingTimersAsync(); }); fireEvent.click(screen.getAllByRole("button", { name: /approve/i })[0]);