test(web): stop the onboarding Escape test racing the passive effect (#640)

Unit Tests went red on main after #639 with one failure: the Escape test saw a
fully rendered dialog but zero apiPut calls, so the handler never ran. Escape is
the only control in that file wired through a document listener in a passive
effect; every other control is an onClick prop attached at commit, which is why
the sibling close-button test passed in the same run.

Could not reproduce it: 50 isolated and whole-file runs stayed green, and a probe
counting document keydown registrations shows the listener is already attached
when findByText resolves locally, so the window only opens under CI contention.
Rather than bet on an unconfirmed mechanism, settle effects and retry the
dispatch until the dismissal lands. handleDismiss guards on `busy` and the
settings write is idempotent, so repeats are harmless.

Still non-vacuous: pointing the handler at a key that never fires fails the test
in about a second. 30 consecutive whole-file runs clean, full unit suite 7,474
passing.
This commit is contained in:
SnapOtter
2026-07-25 19:50:53 +08:00
committed by GitHub
parent 5cc0a850c6
commit d9a8ae7b7e
+12 -3
View File
@@ -1,7 +1,7 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom/vitest";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
@@ -265,10 +265,19 @@ describe("UsageSurveyOverlay", () => {
renderOverlay();
await screen.findByText("How are you using SnapOtter?");
await act(async () => {});
fireEvent.keyDown(document, { key: "Escape" });
// Escape is the one control here whose handler is a document listener
// registered by a passive effect; every other control is an onClick prop
// attached at commit. That makes it the only one that can miss an event
// dispatched too early, and it flaked exactly once on main: dialog fully
// rendered, handler never ran. Locally the listener is always attached by
// the time findByText resolves, so the window only opens under CI
// contention and cannot be reproduced here. Rather than bet on one
// mechanism, retry the dispatch until it lands. handleDismiss guards on
// `busy` and the settings write is idempotent, so repeats are harmless.
await waitFor(() => {
fireEvent.keyDown(document, { key: "Escape" });
expect(apiPut).toHaveBeenCalledWith("/v1/settings", {
"onboarding.usageSurvey.dismissedAt": expect.any(String),
});