Repository navigation
test(web): wait for the table to leave antd's spin blur before clicking into it - #4200
Merged
lizhimins merged 1 commit intoSep 15, 2026
Conversation
RockteMQ-AI
approved these changes
Sep 9, 2026
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
Fix flaky frontend tests caused by antd's Spin component retaining pointer-events: none (.ant-spin-blur) for one extra commit after loading flips to false. The fix adds a small waitFor helper in each test file to wait until the target element accepts pointer events before clicking — applied only where a test clicks a table-internal control immediately after a findBy* query.
Thorough root-cause analysis in the PR description confirms this is purely a test-side race, not a page-side bug. Changes are minimal, well-scoped, and correctly placed.
LGTM.
Automated review by github-manager-bot
…ng into it Two frontend tests failed intermittently with `Unable to perform pointer interaction as the element has pointer-events: none` — on an INPUT (a row selection checkbox) in AlertsPage and on an LI (a pagination item) in ConsumerPage. Both are the same race, and it is in the tests rather than in the pages. antd's Table keeps the previously rendered rows on screen while `loading` is true and wraps the body — pagination included — in a Spin. Spin tracks its own `spinning` state (`antd/es/spin/index.js:52`) and only syncs it from the `spinning` prop inside an effect (`:54-66`), while the `.ant-spin-blur` class is keyed on that internal state (`:80`). So when the Table's `loading` prop flips to false, the blur class survives one further commit. `.ant-spin-blur` sets `pointer-events: none` (`antd/es/spin/style/index.js:133-137`), which covers every row control and the pagination items antd renders inside the same spin container (`antd/es/table/InternalTable.js:396`). `findByText` on a row name resolves during that window, because the rows are already in the DOM, and the click that follows is then rejected by userEvent's pointer-events check. Instrumenting the failing runs confirms it: the row checkbox reports `disabled=false` with computed `pointer-events: none` on both the input and its row, one `.ant-spin-blur` and one `.ant-spin-spinning` present, and no bulk action ever invoked — passing runs report `blur=0 spinning=0 pe=auto` at the same point. Each file gains a small wait for the target to accept pointer events, applied only where a test clicks a table-internal control immediately after a `findBy*` with nothing polled in between. Sites that already wait on something else (for example `waitFor(() => expect(checkbox).toBeEnabled())`) are left alone: that poll absorbs the extra commit, which is why those tests never failed. Batching `setLoading(false)` into the same `.then` as the row updates was tried first and reverted. It removes our own intermediate commit but not Spin's, so it reduced the failure rate without eliminating it; and since both microtasks drain before the browser paints, the intermediate state is invisible to users and only observable from tests. There was nothing to fix on the page side. (cherry picked from commit 2ce9f90)
lizhimins
force-pushed
the
fix/table-spin-blur-test-race
branch
from
September 15, 2026 13:05
2ce9f90 to
c183bf2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two frontend tests were failing intermittently with
Unable to perform pointer interaction as the element has pointer-events: none— on anINPUT(a row selection checkbox) inAlertsPage.test.tsxand on anLI(a pagination item) inConsumerPage.test.tsx. Both are the same race, and it is in the tests rather than in the pages.Root cause
antd's
Tablekeeps the previously rendered rows on screen whileloadingis true, and wraps the body — pagination included — in aSpin.Spintracks its ownspinningstate (antd/es/spin/index.js:52) and only syncs it from thespinningprop inside an effect (:54-66), while the.ant-spin-blurclass is keyed on that internal state (:80). So when the Table'sloadingprop flips to false, the blur class survives one further commit..ant-spin-blursetspointer-events: none(antd/es/spin/style/index.js:133-137), covering every row control plus the pagination items that antd renders inside the same spin container (antd/es/table/InternalTable.js:396).findByTexton a row name resolves during that window because the rows are already in the DOM, and the click that follows is rejected by userEvent's pointer-events check.Instrumenting the failing runs confirms the mechanism and rules out the alternatives:
.ant-spin-blurpresent.ant-spin-spinningpresentdisabledpointer-events(input / row)none/noneauto/autobulkToggleAlertRulescallsThe checkbox is not disabled (
getCheckboxPropsonly disables whileisActionRunning, and no action had run), there is no stale DOM from a previous test, and the row text matches exactly once — so the only thing blocking the click is the spin blur.Why the fix is test-side
Batching
setLoading(false)into the same.thenas the row updates inpages/ops/alerts.tsxwas tried first and reverted. It removes our own intermediate commit but not Spin's, so it lowered the failure rate without eliminating it (1 failure in 10 runs, versus 1 in 6 before). And since both microtasks drain before the browser paints, that intermediate state is invisible to users — it is only observable from tests. There was nothing to fix on the page side.What changed
Each of the two test files gains a small wait for the target to accept pointer events, applied only where a test clicks a table-internal control immediately after a
findBy*with nothing polled in between:AlertsPage.test.tsx—expectRuleRowInteractive(ruleName), used at 3 sites (two 编辑 button clicks and the row checkbox before 批量启用).ConsumerPage.test.tsx—findInteractivePageItem(selector)at the two pagination clicks, andexpectInteractive(element)for the row checkbox that follows a page change.Sites that already wait on something else — e.g.
await waitFor(() => expect(checkbox).toBeEnabled())atAlertsPage.test.tsx:247and:696— are deliberately left untouched. That poll absorbs the extra commit, which is exactly why those tests never failed. TheConsumerPagepagination waits are preventive: that file produced one observed failure with the identical signature and contains the identical code pattern.No production code is changed.
Validation
npx tsc --noEmit -p tsconfig.app.json— clean.npx eslinton both files — clean, 0 errors.npm run build— succeeds.AlertsPage.test.tsxunder 3-way concurrent load: 30/30 passed (baseline before the fix: 1 failure in 6 sequential runs, and 1 in 12 on a second measurement).ConsumerPage.test.tsxunder 3-way concurrent load: 15/15 passed.vitest run, 120 files / 980 tests): 6 consecutive clean runs, 0 failures, 0 unhandled errors. Before this change the suite produced one failure per run, alternating between these two files.Note that
tsc --noEmit -p tsconfig.jsonis not a usable check in this repo —tsconfig.jsonis a solution file with"files": []and only project references, so it type-checks nothing and exits 0 regardless. Usetsconfig.app.json, ornpm run build, whose script istsc -b && vite build.Follow-up (not in this PR)
The same latent pattern —
findBy*on table content followed immediately by a click on a control inside the table — may exist in other test files. If more instances surface it is worth promoting one shared helper intosrc/test/rather than keeping a copy per file.Separately, 18 call sites across 12 frontend files set data in
.then()and clear a loading flag in.finally(). Those are two microtasks, so they always commit an intermediate state where the data is present and the spinner is still on. Browsers drain both before painting, so this is not user-visible and is not touched here; it is noted only because it is what makes the intermediate state reachable from a test in the first place.