From b6566784928d0cfa8310dd3429bd7ba5a3289396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=8E=E4=BC=9F=E6=9D=B0?= <674416404@qq.com> Date: Tue, 29 Sep 2026 22:13:44 +0800 Subject: [PATCH] test: fix React act(...) warnings in unit tests --- .../__tests__/ActionSheetMethod.test.tsx | 90 +++++++++++-------- src/collapse/__tests__/collapse.test.tsx | 10 ++- src/dialog/__tests__/dialog.test.tsx | 22 ++--- src/form/__tests__/form.test.tsx | 10 ++- src/guide/__tests__/guide.test.tsx | 42 ++++++--- src/message/__tests__/message.test.tsx | 8 +- .../__tests__/pull-down-refresh.test.tsx | 6 +- src/qrcode/__tests__/qrcode.test.tsx | 6 +- 8 files changed, 124 insertions(+), 70 deletions(-) diff --git a/src/action-sheet/__tests__/ActionSheetMethod.test.tsx b/src/action-sheet/__tests__/ActionSheetMethod.test.tsx index ac8804cea..bd5440ac3 100644 --- a/src/action-sheet/__tests__/ActionSheetMethod.test.tsx +++ b/src/action-sheet/__tests__/ActionSheetMethod.test.tsx @@ -1,10 +1,12 @@ -import { describe, it, expect, afterEach } from '@test/utils'; +import { describe, it, expect, afterEach, act } from '@test/utils'; import { show, close } from '../ActionSheetMethod'; describe('ActionSheetMethod', () => { - afterEach(() => { + afterEach(async () => { // 清理每次测试后可能残留的 ActionSheet - close(); + await act(async () => { + close(); + }); }); describe('module exports', () => { @@ -13,51 +15,63 @@ describe('ActionSheetMethod', () => { expect(typeof close).toBe('function'); }); - it('should be able to call show function', () => { - expect(() => { - show({ - items: ['Item 1', 'Item 2'], - }); - }).not.toThrow(); + it('should be able to call show function', async () => { + await act(async () => { + expect(() => { + show({ + items: ['Item 1', 'Item 2'], + }); + }).not.toThrow(); + }); }); - it('should be able to call close function', () => { - expect(() => { - close(); - }).not.toThrow(); + it('should be able to call close function', async () => { + await act(async () => { + expect(() => { + close(); + }).not.toThrow(); + }); }); - it('should handle show with empty config', () => { - expect(() => { - show({}); - }).not.toThrow(); + it('should handle show with empty config', async () => { + await act(async () => { + expect(() => { + show({}); + }).not.toThrow(); + }); }); - it('should handle show with various config options', () => { - expect(() => { - show({ - items: ['Test Item'], - theme: 'grid', - description: 'Test Description', - visible: true, - }); - }).not.toThrow(); + it('should handle show with various config options', async () => { + await act(async () => { + expect(() => { + show({ + items: ['Test Item'], + theme: 'grid', + description: 'Test Description', + visible: true, + }); + }).not.toThrow(); + }); }); - it('should handle multiple show calls', () => { - expect(() => { - show({ items: ['Item 1'] }); - show({ items: ['Item 2'] }); - }).not.toThrow(); + it('should handle multiple show calls', async () => { + await act(async () => { + expect(() => { + show({ items: ['Item 1'] }); + show({ items: ['Item 2'] }); + }).not.toThrow(); + }); }); - it('should handle show and close sequence', () => { - expect(() => { - show({ items: ['Item 1'] }); - close(); - show({ items: ['Item 2'] }); - close(); - }).not.toThrow(); + it('should handle show and close sequence', async () => { + await act(async () => { + expect(() => { + show({ items: ['Item 1'] }); + close(); + show({ items: ['Item 2'] }); + close(); + }).not.toThrow(); + }); }); }); }); diff --git a/src/collapse/__tests__/collapse.test.tsx b/src/collapse/__tests__/collapse.test.tsx index 3affc7df6..b18a47ee9 100644 --- a/src/collapse/__tests__/collapse.test.tsx +++ b/src/collapse/__tests__/collapse.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { describe, expect, it, render, fireEvent, vi, beforeEach, afterEach, waitFor } from '@test/utils'; +import { describe, expect, it, render, fireEvent, vi, beforeEach, afterEach, waitFor, act } from '@test/utils'; import { Collapse, CollapsePanel } from 'tdesign-mobile-react'; describe('Collapse', () => { @@ -426,7 +426,9 @@ describe('Collapse', () => { fireEvent.click(panels[1]); // 此时应该有一次debounced调用 - vi.advanceTimersByTime(150); + act(() => { + vi.advanceTimersByTime(150); + }); // 验证最终状态 expect(onChange).toHaveBeenCalled(); @@ -452,7 +454,9 @@ describe('Collapse', () => { fireEvent.click(panels[0]); // 等待debounce - vi.advanceTimersByTime(150); + act(() => { + vi.advanceTimersByTime(150); + }); // 验证行为 expect(onChange).toHaveBeenCalled(); diff --git a/src/dialog/__tests__/dialog.test.tsx b/src/dialog/__tests__/dialog.test.tsx index 1d6d5225f..509ad7aa0 100644 --- a/src/dialog/__tests__/dialog.test.tsx +++ b/src/dialog/__tests__/dialog.test.tsx @@ -201,12 +201,14 @@ describe('Dialog', () => { onClosed, }); }); - instance.hide(); - - setTimeout(() => { - expect(document.querySelector(name)).toBeFalsy(); - expect(onClosed).toHaveBeenCalled(); - }, 1000); + await act(async () => { + instance.hide(); + }); + await act(async () => { + vi.advanceTimersByTime(1000); + }); + expect(document.querySelector(name)).toBeFalsy(); + expect(onClosed).toHaveBeenCalled(); }); it(': update', async () => { @@ -218,10 +220,10 @@ describe('Dialog', () => { onCancel: vi.fn(), }); }); - instance.update({ title: '新标题' }); - setInterval(() => { - expect(document.querySelector(`${name}__header`).textContent).toBe('新标题'); - }, 1000); + await act(async () => { + instance.update({ title: '新标题' }); + }); + expect(document.querySelector(`${name}__header`).textContent).toBe('新标题'); }); it(': destroy', async () => { diff --git a/src/form/__tests__/form.test.tsx b/src/form/__tests__/form.test.tsx index 52c17ac63..a89d7f1df 100644 --- a/src/form/__tests__/form.test.tsx +++ b/src/form/__tests__/form.test.tsx @@ -1690,7 +1690,7 @@ describe('Form 组件测试', () => { expect(() => requestSubmit({} as any)).toThrow('target must be HTMLFormElement'); }); - it('HTMLFormElement 正常工作', () => { + it('HTMLFormElement 正常工作', async () => { const { container } = render(
@@ -1698,7 +1698,9 @@ describe('Form 组件测试', () => {
, ); - expect(() => requestSubmit(container.querySelector('form') as HTMLFormElement)).not.toThrow(); + await act(async () => { + expect(() => requestSubmit(container.querySelector('form') as HTMLFormElement)).not.toThrow(); + }); }); }); @@ -1804,7 +1806,7 @@ describe('Form 组件测试', () => { expect((getByPlaceholderText('name') as HTMLInputElement).value).toBe('delayed'); }); - it('taskQueue 延迟执行 - setFields/submit/reset/clearValidate/setValidateMessage', () => { + it('taskQueue 延迟执行 - setFields/submit/reset/clearValidate/setValidateMessage', async () => { const submitFn = vi.fn(); const TestForm = () => { const [form] = Form.useForm(); @@ -1832,6 +1834,7 @@ describe('Form 组件测试', () => { }; render(); // taskQueue 方法在 flashQueue 时执行,不崩溃即可 + await mockDelay(); }); }); @@ -2490,6 +2493,7 @@ describe('Form 组件测试', () => { ); // 无 onSubmit 时不崩溃 expect(() => fireEvent.click(getByText('submit'))).not.toThrow(); + await mockDelay(); }); it('onReset 不传时 reset 正常工作', async () => { diff --git a/src/guide/__tests__/guide.test.tsx b/src/guide/__tests__/guide.test.tsx index a457b6daf..d5e026df7 100644 --- a/src/guide/__tests__/guide.test.tsx +++ b/src/guide/__tests__/guide.test.tsx @@ -1,5 +1,5 @@ import '@testing-library/jest-dom'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import Guide from '../Guide'; @@ -850,7 +850,9 @@ describe('Guide Component', () => { render(); // 快进定时器 - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); const portal = document.querySelector('.t-portal-wrapper'); expect(portal).toBeInTheDocument(); @@ -863,11 +865,15 @@ describe('Guide Component', () => { // 超时后应可见 vi.useFakeTimers(); - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); // 更改 current 触发可见性变化 rerender(); - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); const portal = document.querySelector('.t-portal-wrapper'); expect(portal).toBeInTheDocument(); @@ -889,7 +895,9 @@ describe('Guide Component', () => { render(); // 快进定时器以触发 showDialogGuide(内部调用 scrollToParentVisibleArea) - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); // 应调用 scrollToParentVisibleArea const portal = document.querySelector('.t-portal-wrapper'); @@ -907,7 +915,9 @@ describe('Guide Component', () => { expect(portal).toBeInTheDocument(); // 快进定时器触发 showGuide 内的 setTimeout - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); // 超时后设置为可见 portal = document.querySelector('.t-portal-wrapper'); @@ -930,7 +940,9 @@ describe('Guide Component', () => { render(); // 快进定时器以触发 setReferenceFullW(可能存在空元素) - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); // setReferenceFullW 应优雅处理空元素 const portal = document.querySelector('.t-portal-wrapper'); @@ -1152,7 +1164,9 @@ describe('Guide Component', () => { render(); // 快进定时器以触发 showPopoverGuide(其以 isReference=true 调用 setHighlightLayerPosition) - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); // 组件应渲染自定义高亮 expect(screen.getByTestId('custom-highlight-ref')).toBeInTheDocument(); @@ -1204,7 +1218,9 @@ describe('Guide Component', () => { render(); // 快进定时器以触发 showPopoverGuide - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); // 气泡模式下居中应调用 setReferenceFullW const portal = document.querySelector('.t-portal-wrapper'); @@ -1227,7 +1243,9 @@ describe('Guide Component', () => { render(); // 快进定时器以触发 showDialogGuide - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); // showDialogGuide 应无错执行 const portal = document.querySelector('.t-portal-wrapper'); @@ -1245,7 +1263,9 @@ describe('Guide Component', () => { expect(portal).toBeInTheDocument(); // 快进定时器触发 setPopoverVisible(true) - vi.advanceTimersByTime(100); + act(() => { + vi.advanceTimersByTime(100); + }); // 现在气泡应可见 portal = document.querySelector('.t-portal-wrapper'); diff --git a/src/message/__tests__/message.test.tsx b/src/message/__tests__/message.test.tsx index 3fb792b82..37593755b 100644 --- a/src/message/__tests__/message.test.tsx +++ b/src/message/__tests__/message.test.tsx @@ -264,7 +264,9 @@ describe('Message', () => { expect(textEl).toMatchSnapshot(); // Fire transitionend -> snapshot reset state - textEl.dispatchEvent(new Event('transitionend')); + act(() => { + textEl.dispatchEvent(new Event('transitionend')); + }); expect(textEl).toMatchSnapshot(); // Next play after delay (playedCount > 0): snapshot styles applied @@ -290,7 +292,9 @@ describe('Message', () => { expect(textEl).toMatchSnapshot(); // Fire transitionend -> snapshot reset state - textEl.dispatchEvent(new Event('transitionend')); + act(() => { + textEl.dispatchEvent(new Event('transitionend')); + }); expect(textEl).toMatchSnapshot(); // Next play after delay (playedCount > 0): snapshot styles applied diff --git a/src/pull-down-refresh/__tests__/pull-down-refresh.test.tsx b/src/pull-down-refresh/__tests__/pull-down-refresh.test.tsx index c57d8f8b4..4e71aa715 100644 --- a/src/pull-down-refresh/__tests__/pull-down-refresh.test.tsx +++ b/src/pull-down-refresh/__tests__/pull-down-refresh.test.tsx @@ -167,7 +167,7 @@ describe('PullDownRefresh', () => { expect(container.querySelector(name)).toBeTruthy(); }); - it(': maxBarHeight with string', () => { + it(': maxBarHeight with string', async () => { const { container } = render(
content
@@ -177,6 +177,8 @@ describe('PullDownRefresh', () => { const track = container.querySelector(`${name}__track`) as HTMLElement; mockPullDown(track!, 60, 2, 0); + // 等待 doRefresh 内的异步状态更新在 act 中完成 + await act(async () => {}); expect(track).toHaveStyle({ transform: 'translate3d(0, 50px, 0)' }); }); @@ -366,6 +368,8 @@ describe('PullDownRefresh', () => { // absX = 0, absY = 60 mockPullDown(track!, 80, 4, 0); + // 等待 doRefresh 内的异步状态更新在 act 中完成 + await act(async () => {}); expect(mockRefresh).toHaveBeenCalled(); }); diff --git a/src/qrcode/__tests__/qrcode.test.tsx b/src/qrcode/__tests__/qrcode.test.tsx index a81c4fd3a..93273249a 100644 --- a/src/qrcode/__tests__/qrcode.test.tsx +++ b/src/qrcode/__tests__/qrcode.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { describe, it, expect, render, fireEvent, waitFor, vi, cleanup, afterEach } from '@test/utils'; +import { describe, it, expect, render, fireEvent, waitFor, vi, cleanup, afterEach, act } from '@test/utils'; import QRCode from '../QRCode'; import { QRCodeSVG } from '../QRCodeSVG'; @@ -250,7 +250,9 @@ describe('QRCodeCanvas - image settings, excavation and crossOrigin (merged)', ( Object.defineProperty(img, 'naturalWidth', { configurable: true, get: () => 10 }); Object.defineProperty(img, 'naturalHeight', { configurable: true, get: () => 10 }); - img.dispatchEvent(new Event('load')); + act(() => { + img.dispatchEvent(new Event('load')); + }); await waitFor(() => { const alphaSets = calls.globalAlpha || [];