This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 368683f92 test(web): wait for the table to leave antd's spin blur
before clicking into it (#4200)
368683f92 is described below
commit 368683f9220ab4db16ae1b4e045ec632a3ef9479
Author: lizhimins <[email protected]>
AuthorDate: Tue Sep 15 21:05:44 2026 +0800
test(web): wait for the table to leave antd's spin blur before clicking
into it (#4200)
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 2ce9f90fe2bc0e8b48ac672241a9ad2c7d2eb065)
---
.../pages/instance/__tests__/ConsumerPage.test.tsx | 29 ++++++++++++++++++----
web/src/pages/ops/__tests__/AlertsPage.test.tsx | 15 +++++++++++
2 files changed, 39 insertions(+), 5 deletions(-)
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 45743d7e0..f4b4e92fc 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -127,6 +127,24 @@ const renderWithProviders = (ui: React.ReactElement,
initialEntry = '/instance/c
beforeAll(installBrowserMocks);
+// antd's Spin keys its `.ant-spin-blur` class on internal state that follows
the Table's
+// `loading` prop one commit behind, and that class sets `pointer-events:
none` over the whole
+// table body — including the pagination, which antd renders inside the same
spin container.
+// Rows are kept rendered while loading, so `findByText` on a row resolves
during that window and
+// a click straight afterwards is rejected. Wait for the target to accept
pointer events first.
+async function expectInteractive(element: Element) {
+ await waitFor(() =>
expect(getComputedStyle(element).pointerEvents).not.toBe('none'));
+}
+
+async function findInteractivePageItem(selector: string) {
+ return waitFor(() => {
+ const element = document.querySelector<HTMLElement>(selector);
+ expect(element).not.toBeNull();
+ expect(getComputedStyle(element as
HTMLElement).pointerEvents).not.toBe('none');
+ return element as HTMLElement;
+ });
+}
+
describe('Consumer page', () => {
beforeEach(() => {
vi.restoreAllMocks();
@@ -318,9 +336,8 @@ describe('Consumer page', () => {
expect(await screen.findByText('remote-cg-01')).toBeInTheDocument();
- const secondPage = document.querySelector('.ant-pagination-item-2');
- expect(secondPage).not.toBeNull();
- await user.click(secondPage as HTMLElement);
+ const secondPage = await findInteractivePageItem('.ant-pagination-item-2');
+ await user.click(secondPage);
// The empty out-of-range page is corrected: the list reloads page 1.
await waitFor(() =>
@@ -385,9 +402,11 @@ describe('Consumer page', () => {
renderWithProviders(<ConsumerPage />);
expect(await screen.findByText('cg-01')).toBeInTheDocument();
- await user.click(document.querySelector('.ant-pagination-item-2') as
HTMLElement);
+ await user.click(await findInteractivePageItem('.ant-pagination-item-2'));
expect(await screen.findByText('cg-21')).toBeInTheDocument();
- await user.click(screen.getAllByRole('checkbox')[0]);
+ const firstCheckbox = screen.getAllByRole('checkbox')[0];
+ await expectInteractive(firstCheckbox);
+ await user.click(firstCheckbox);
await user.click(screen.getByRole('button', { name: /删除 \(1\)$/ }));
await waitFor(() =>
diff --git a/web/src/pages/ops/__tests__/AlertsPage.test.tsx
b/web/src/pages/ops/__tests__/AlertsPage.test.tsx
index 77abe6db6..afe7360ce 100644
--- a/web/src/pages/ops/__tests__/AlertsPage.test.tsx
+++ b/web/src/pages/ops/__tests__/AlertsPage.test.tsx
@@ -137,6 +137,18 @@ function getRuleRow(ruleName: string) {
return row;
}
+// antd's Spin keys its `.ant-spin-blur` class on an internal `spinning` state
that follows the
+// Table's `loading` prop one commit behind (see the effect in
antd/es/spin/index.js), and that
+// class sets `pointer-events: none` over the whole table body. Because antd
keeps the previous
+// rows rendered while loading, `findByText` on a rule name resolves during
that window, so a
+// click straight afterwards is rejected. Tests that poll anything else first
absorb the extra
+// commit and never see it; these waits make the same guarantee explicit.
+async function expectRuleRowInteractive(ruleName: string) {
+ await waitFor(() =>
+
expect(getComputedStyle(getRuleRow(ruleName)).pointerEvents).not.toBe('none'),
+ );
+}
+
function getSelectOption(label: string) {
const option = screen
.getAllByText(label)
@@ -437,6 +449,7 @@ describe('AlertsPage', () => {
renderPage();
await screen.findByText('Broker disk usage');
+ await expectRuleRowInteractive('Broker disk usage');
await user.click(within(getRuleRow('Broker disk
usage')).getByRole('button', { name: '编辑' }));
await waitFor(() =>
expect(listNativeAlertMetrics).toHaveBeenCalledWith('local', 'CLUSTER'));
@@ -474,6 +487,7 @@ describe('AlertsPage', () => {
renderPage('BUSINESS');
await screen.findByText('Legacy disk usage');
+ await expectRuleRowInteractive('Legacy disk usage');
await user.click(within(getRuleRow('Legacy disk
usage')).getByRole('button', { name: '编辑' }));
expect(await screen.findByRole('combobox', { name: '监控指标'
})).toBeEnabled();
@@ -733,6 +747,7 @@ describe('AlertsPage', () => {
renderPage();
await screen.findByText('Broker disk usage');
+ await expectRuleRowInteractive('Broker disk usage');
await user.click(within(getRuleRow('Broker disk
usage')).getByRole('checkbox'));
await user.click(screen.getByRole('button', { name: '批量启用' }));