From 4db8d30ece7782afd21ef0d2de99a663a2a1e6ee Mon Sep 17 00:00:00 2001 From: qa-agent Date: Sun, 19 Apr 2026 19:18:30 +0000 Subject: [PATCH] test(canvas): cover /orgs 5s polling on in-flight orgs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test docstring promised polling coverage but I'd only wired the describe-block header, not the actual tests. Closing that gap — vitest fake timers drive three cases: - `provisioning` org → 2nd fetch fires after 5.1s advance - all `running` → no 2nd fetch even after 10s advance - `awaiting_payment` org, unmount before timer fires → no post-unmount fetch (cleanup correctly clears the pollTimer) The unmount case is the meaningful one: without it a fast nav-away leaves the 5s interval chasing the CP forever. page.tsx L97-99 does clear the timer; the test pins the contract. Local baseline on origin/staging tip ede6597 + this branch: canvas vitest: 50 files / 781 tests, all green (+3 vs prior commit) canvas build: clean Co-Authored-By: Claude Opus 4.7 --- canvas/src/app/__tests__/orgs-page.test.tsx | 117 ++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/canvas/src/app/__tests__/orgs-page.test.tsx b/canvas/src/app/__tests__/orgs-page.test.tsx index e68fb764..af08b4bb 100644 --- a/canvas/src/app/__tests__/orgs-page.test.tsx +++ b/canvas/src/app/__tests__/orgs-page.test.tsx @@ -249,3 +249,120 @@ describe("/orgs — fetch includes credentials + timeout signal", () => { expect(callArgs![1].signal).toBeInstanceOf(AbortSignal); }); }); + +// ── Polling ────────────────────────────────────────────────────────────────── +// page.tsx line 83-88: if any org is `provisioning` OR `awaiting_payment`, +// schedule a 5s refresh so the user sees the state flip live after Stripe +// Checkout returns. Cleanup must clear the timer on unmount; otherwise a +// fast-nav-away leaves the interval firing against the CP indefinitely. + +describe("/orgs — polling of in-flight orgs", () => { + it("schedules a 5s refetch when at least one org is provisioning", async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + try { + mockFetchSession.mockResolvedValue({ userId: "u-1" }); + mockFetch.mockResolvedValueOnce( + okJson({ + orgs: [ + { + id: "o-1", + slug: "acme", + name: "Acme", + plan: "pro", + status: "provisioning", + created_at: "", + updated_at: "", + }, + ], + }) + ); + // Second fetch (the poll refresh) returns a running org so we can + // observe the state flip — and to let the test stop re-scheduling. + mockFetch.mockResolvedValueOnce( + okJson({ + orgs: [ + { + id: "o-1", + slug: "acme", + name: "Acme", + plan: "pro", + status: "running", + created_at: "", + updated_at: "", + }, + ], + }) + ); + + render(); + // First fetch resolves + await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)); + // Advance past the 5s scheduled refresh + await vi.advanceTimersByTimeAsync(5_100); + // Second fetch is the poll refresh + await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)); + } finally { + vi.useRealTimers(); + } + }); + + it("does NOT schedule a refetch when all orgs are running", async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + try { + mockFetchSession.mockResolvedValue({ userId: "u-1" }); + mockFetch.mockResolvedValueOnce( + okJson({ + orgs: [ + { + id: "o-1", + slug: "acme", + name: "Acme", + plan: "pro", + status: "running", + created_at: "", + updated_at: "", + }, + ], + }) + ); + render(); + await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)); + // Advance well past the 5s poll window — no second fetch must fire + await vi.advanceTimersByTimeAsync(10_000); + expect(mockFetch).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); + + it("clears the poll timer on unmount — no fetch after unmount", async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }); + try { + mockFetchSession.mockResolvedValue({ userId: "u-1" }); + mockFetch.mockResolvedValueOnce( + okJson({ + orgs: [ + { + id: "o-1", + slug: "acme", + name: "Acme", + plan: "pro", + status: "awaiting_payment", + created_at: "", + updated_at: "", + }, + ], + }) + ); + const { unmount } = render(); + await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)); + // Tear down BEFORE the 5s timer fires + unmount(); + await vi.advanceTimersByTimeAsync(10_000); + // Fetch count must stay at 1 — the cleanup cleared the timer + expect(mockFetch).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); +});