From acacf6fdc603388fcd0b73e832cc878f7b0d5050 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-UIUX Date: Mon, 11 May 2026 11:34:05 +0000 Subject: [PATCH] fix(canvas/test): mockReset before rejection in ApprovalBanner POST error tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes cross-test mock pollution from ActivityTab.test.tsx which uses vi.hoisted() to mock api.post. In the jsdom shared environment, vi.spyOn(api, "post") in ApprovalBanner wraps that bare spy. Calling mockRejectedValueOnce() queues the rejection AFTER ActivityTab's mockResolvedValue({}) — reliable in isolation, order- dependent in the full suite. The fix: add a module-level mockPost reference, assign the spy to it in beforeEach, and use mockPost.mockReset().mockRejectedValue() in the two error-path tests. mockReset() atomically clears the spy implementation before setting a permanent rejection, bypassing the mock queue entirely. Fixes: shows an error toast when POST fails Fixes: keeps the card visible when the POST fails Co-Authored-By: Claude Opus 4.7 --- .../components/__tests__/ApprovalBanner.test.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/canvas/src/components/__tests__/ApprovalBanner.test.tsx b/canvas/src/components/__tests__/ApprovalBanner.test.tsx index 09817ef9..2a3fc758 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,7 @@ describe("ApprovalBanner — decisions", () => { }); it("shows an error toast when POST fails", async () => { - vi.mocked(api.post).mockRejectedValueOnce(new Error("Network error")); + mockPost.mockReset().mockRejectedValue(new Error("Network error")); render(); await act(async () => { await vi.runOnlyPendingTimersAsync(); }); fireEvent.click(screen.getAllByRole("button", { name: /approve/i })[0]); @@ -208,8 +209,9 @@ 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().mockRejectedValue(new Error("Network error")); render(); await act(async () => { await vi.runOnlyPendingTimersAsync(); }); fireEvent.click(screen.getAllByRole("button", { name: /approve/i })[0]); -- 2.45.2