From cebd9ab9168791b6e209f0bc273b4557ebe51cac Mon Sep 17 00:00:00 2001 From: Molecule AI App-FE Date: Mon, 11 May 2026 10:25:10 +0000 Subject: [PATCH 1/2] fix(canvas/test): replace fixed-delay dialog wait with waitFor polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PurchaseSuccessModal tests used a fixed 50ms setTimeout to wait for the dialog to appear after React useEffect batch + createPortal. This was flaky because React's rendering timing varies. Replace waitForDialog() fixed-delay with waitFor() polling — the test waits exactly as long as React needs, no more. Update all dismiss tests to use act(() => setTimeout(...)) after vi.useRealTimers() for reliable real-timer behavior. Result: 18/18 tests pass (was 14/18 with 4 timing-related failures). Co-Authored-By: Claude Opus 4.7 --- .../__tests__/PurchaseSuccessModal.test.tsx | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/canvas/src/components/__tests__/PurchaseSuccessModal.test.tsx b/canvas/src/components/__tests__/PurchaseSuccessModal.test.tsx index 4abdb36c..30e774c3 100644 --- a/canvas/src/components/__tests__/PurchaseSuccessModal.test.tsx +++ b/canvas/src/components/__tests__/PurchaseSuccessModal.test.tsx @@ -12,7 +12,7 @@ * window.location.search in the jsdom environment. */ import React from "react"; -import { render, screen, fireEvent, cleanup, act } from "@testing-library/react"; +import { render, screen, fireEvent, cleanup, act, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { PurchaseSuccessModal } from "../PurchaseSuccessModal"; @@ -30,9 +30,13 @@ function clearSearch() { setSearch(""); } -// Helper: wait for dialog to appear (real timers) +// Helper: wait for the dialog to appear after React useEffect batch. +// Uses waitFor (polling) rather than a fixed timer so the test waits +// exactly as long as React needs — more reliable than a fixed 50ms delay. async function waitForDialog() { - await act(async () => { await new Promise((r) => setTimeout(r, 50)); }); + await waitFor(() => { + expect(screen.queryByRole("dialog")).toBeTruthy(); + }, { timeout: 2000 }); } // ─── Tests ──────────────────────────────────────────────────────────────────── @@ -104,6 +108,7 @@ describe("PurchaseSuccessModal — render conditions", () => { describe("PurchaseSuccessModal — dismiss", () => { beforeEach(() => { setSearch("?purchase_success=1&item=TestItem"); + vi.useRealTimers(); // use real timers throughout so waitFor + setTimeout are synchronous-friendly }); afterEach(() => { @@ -116,52 +121,45 @@ describe("PurchaseSuccessModal — dismiss", () => { it("closes the dialog when the close button is clicked", async () => { render(); await waitForDialog(); - expect(screen.getByRole("dialog")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Close" })); - await waitForDialog(); + await act(async () => { await new Promise((r) => setTimeout(r, 100)); }); expect(screen.queryByRole("dialog")).toBeNull(); }); it("closes the dialog when the backdrop is clicked", async () => { render(); await waitForDialog(); - expect(screen.getByRole("dialog")).toBeTruthy(); const backdrop = document.body.querySelector('[aria-hidden="true"]'); if (backdrop) fireEvent.click(backdrop); - await waitForDialog(); + await act(async () => { await new Promise((r) => setTimeout(r, 100)); }); expect(screen.queryByRole("dialog")).toBeNull(); }); it("closes on Escape key", async () => { render(); await waitForDialog(); - expect(screen.getByRole("dialog")).toBeTruthy(); fireEvent.keyDown(window, { key: "Escape" }); - await waitForDialog(); + await act(async () => { await new Promise((r) => setTimeout(r, 100)); }); expect(screen.queryByRole("dialog")).toBeNull(); }); // Auto-dismiss tests use real timers — the component's setTimeout fires - // naturally after 5s in the test environment. vi.useFakeTimers() is not used - // here because React 18 + fake timers require careful microtask/macrotask - // interleaving that is fragile in jsdom; real timers are reliable. + // naturally after 5s in the test environment. it("auto-dismisses after 5 seconds", async () => { render(); await waitForDialog(); - expect(screen.getByRole("dialog")).toBeTruthy(); - // The component's AUTO_DISMISS_MS = 5000ms. In jsdom, setTimeout fires - // reliably. Wait long enough for 2 dismiss cycles to ensure the first fires. - await act(async () => { await new Promise((r) => setTimeout(r, 11000)); }); + // AUTO_DISMISS_MS = 5000ms. Wait 6s to ensure dismiss has fired + React updated. + await act(async () => { await new Promise((r) => setTimeout(r, 6000)); }); expect(screen.queryByRole("dialog")).toBeNull(); - }, 15000); // extended timeout for real-timer wait + }, 10000); it("does not auto-dismiss before 5 seconds", async () => { render(); await waitForDialog(); - expect(screen.getByRole("dialog")).toBeTruthy(); + const dialog = screen.getByRole("dialog"); // Wait 4s — just under the 5s auto-dismiss threshold await act(async () => { await new Promise((r) => setTimeout(r, 4000)); }); - expect(screen.getByRole("dialog")).toBeTruthy(); + expect(screen.queryByRole("dialog")).toBeTruthy(); }); }); @@ -210,27 +208,28 @@ describe("PurchaseSuccessModal — accessibility", () => { it("has aria-modal=true on the dialog", async () => { render(); - await waitForDialog(); - const dialog = screen.getByRole("dialog"); - expect(dialog.getAttribute("aria-modal")).toBe("true"); + await waitFor(() => { + expect(screen.getByRole("dialog").getAttribute("aria-modal")).toBe("true"); + }); }); it("has aria-labelledby pointing to the title", async () => { render(); - await waitForDialog(); - const dialog = screen.getByRole("dialog"); - const labelledby = dialog.getAttribute("aria-labelledby"); - expect(labelledby).toBeTruthy(); - expect(document.getElementById(labelledby!)).toBeTruthy(); - expect(document.getElementById(labelledby!)?.textContent).toMatch(/purchase successful/i); + await waitFor(() => { + const dialog = screen.getByRole("dialog"); + const labelledby = dialog.getAttribute("aria-labelledby"); + expect(labelledby).toBeTruthy(); + expect(document.getElementById(labelledby!)).toBeTruthy(); + expect(document.getElementById(labelledby!)?.textContent).toMatch(/purchase successful/i); + }); }); // Focus test: verify close button exists after dialog renders. // We test presence (not focus) since rAF focus is tricky in jsdom. it("moves focus to the close button on open", async () => { render(); - await act(async () => { await new Promise((r) => setTimeout(r, 100)); }); - // Use getByRole which is more reliable than querySelector - expect(screen.getByRole("button", { name: "Close" })).toBeTruthy(); + await waitFor(() => { + expect(screen.getByRole("button", { name: "Close" })).toBeTruthy(); + }); }); }); From b62b18b523e9fae23f38289acbecdebbdb17e9e3 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Mon, 11 May 2026 12:50:57 +0000 Subject: [PATCH 2/2] [core-be-agent] ci: retrigger Canvas tests for env validation Retry CI run to confirm Canvas test suite passes on current head.