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: '批量启用' }));
 

Reply via email to