diff --git a/.github/workflows/test-visual-scheduled.yml b/.github/workflows/test-visual-scheduled.yml index 51b725bd69..02953cb206 100644 --- a/.github/workflows/test-visual-scheduled.yml +++ b/.github/workflows/test-visual-scheduled.yml @@ -27,9 +27,24 @@ jobs: run: pnpm test:browser:prepare --only-shell - name: Run visual tests - # run without file parallelism, because firefox has issues with parallel visual tests + # Run without file parallelism, because firefox has issues with parallel visual tests. + # Retry the whole step because firefox intermittently *hangs* (not a snapshot diff) on a + # single visual test file in headless CI, which aborts the run with exit code 1 even though + # every test that runs passes. A hang can't be recovered by vitest's per-test `retry`, so we + # re-run the step with a fresh vitest/browser process. Successful `^build` deps are served + # from the nx cache, so retries only re-execute test:visual. run: | - pnpm nx run-many -t test:visual --parallel=1 --browser.fileParallelism=false + for attempt in 1 2 3; do + echo "::group::Visual tests (attempt $attempt/3)" + if pnpm nx run-many -t test:visual --parallel=1 --browser.fileParallelism=false; then + echo "::endgroup::" + exit 0 + fi + echo "::endgroup::" + echo "::warning::Visual tests failed on attempt $attempt/3 (likely a flaky firefox hang), retrying..." + done + echo "::error::Visual tests still failing after 3 attempts." + exit 1 - name: Send alerting message to Slack if: failure() diff --git a/packages/components/src/components/Modal/Modal.browser.test.tsx b/packages/components/src/components/Modal/Modal.browser.test.tsx index 12d1b28c6e..3eb79dc4a2 100644 --- a/packages/components/src/components/Modal/Modal.browser.test.tsx +++ b/packages/components/src/components/Modal/Modal.browser.test.tsx @@ -274,8 +274,12 @@ test("useOnClosed is called when the closing animation has finished", async () = expect(modalText).toBeInTheDocument(); expect(onClosed).not.toHaveBeenCalledOnce(); - await sleep(500); - expect(modalText).not.toBeInTheDocument(); + // Wait for the close animation to finish and the Modal to unmount instead of + // relying on a fixed delay: the animation duration varies and a hard-coded + // sleep races against it under CI load, which made this test flaky. + await vitest.waitFor(() => expect(modalText).not.toBeInTheDocument(), { + timeout: 2000, + }); // onClosed is just called, when the Modal has unmounted (after close animation) expect(onClosed).toHaveBeenCalledOnce();