From 4911801ed67e782f37f63145fcd5d5c36de8943a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 7 Aug 2026 14:26:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A1=B9=E7=9B=AE=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E6=B8=85=E5=8D=95=E7=AB=9E=E6=80=81=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 等待事件监听注册后再触发 manifest 失效并以有界断言确认读取启动 将迟到响应收尾纳入异步 act 避免测试时序悬挂 补充 React 异步事件测试准则 --- .../tests/appSurface/home.suite.ts | 36 +++++++++++++------ ...前端测试】React组件测试准则-2026-06-26.md | 3 +- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index 1ff1f578f..d5c9bf98c 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -359,17 +359,11 @@ export function registerClientHomeTests() { source: { kind: 'generated' }, }, ]; - let resolveStaleRefresh!: ( - manifest: typeof staleFirstManifest, - ) => void; + let resolveStaleRefresh!: (manifest: typeof staleFirstManifest) => void; const staleRefresh = new Promise((resolve) => { resolveStaleRefresh = resolve; }); let holdFirstRefresh = false; - let signalFirstRefreshStarted!: () => void; - const firstRefreshStarted = new Promise((resolve) => { - signalFirstRefreshStarted = resolve; - }); const runtimeHarness = createProjectSupervisorRuntimeHarness({ projectPath: firstProjectPath, }); @@ -393,7 +387,6 @@ export function registerClientHomeTests() { return secondManifest; } if (holdFirstRefresh) { - signalFirstRefreshStarted(); return staleRefresh; } return firstManifest; @@ -461,11 +454,30 @@ export function registerClientHomeTests() { }); fireEvent.click(screen.getByRole('button', { name: '打开' })); await screen.findByLabelText('项目开发工作台'); + await waitFor(() => { + expect(runtimeHarness.listen).toHaveBeenCalledWith( + 'game-creator-manifest-invalidated', + expect.any(Function), + ); + }); + const firstProjectManifestReadsBeforeRefresh = invoke.mock.calls.filter( + ([command, args]) => + command === 'get_local_game_manifest' && + args?.projectPath === firstProjectPath, + ).length; holdFirstRefresh = true; act(() => { runtimeHarness.emitManifestInvalidated('code-prototype'); }); - await firstRefreshStarted; + await waitFor(() => { + expect( + invoke.mock.calls.filter( + ([command, args]) => + command === 'get_local_game_manifest' && + args?.projectPath === firstProjectPath, + ).length, + ).toBeGreaterThan(firstProjectManifestReadsBeforeRefresh); + }); fireEvent.click(screen.getByRole('button', { name: '项目组' })); fireEvent.change(screen.getByLabelText('项目目录'), { @@ -481,8 +493,10 @@ export function registerClientHomeTests() { await screen.findByRole('button', { name: /second\.png/ }), ).not.toBeNull(); - act(() => resolveStaleRefresh(staleFirstManifest)); - await Promise.resolve(); + await act(async () => { + resolveStaleRefresh(staleFirstManifest); + await staleRefresh; + }); expect( screen.queryByRole('button', { name: /stale-first\.png/ }), ).toBeNull(); diff --git a/docs/technical/【前端测试】React组件测试准则-2026-06-26.md b/docs/technical/【前端测试】React组件测试准则-2026-06-26.md index a2a7667a8..cada040a1 100644 --- a/docs/technical/【前端测试】React组件测试准则-2026-06-26.md +++ b/docs/technical/【前端测试】React组件测试准则-2026-06-26.md @@ -1,6 +1,6 @@ # React 组件测试准则 -更新时间:`2026-06-26` +更新时间:`2026-08-07` ## 背景 @@ -36,6 +36,7 @@ - `data-testid` 名称必须描述用户或稳定渲染边界,例如 `image-canvas-editor-snap-guide-vertical`;不要描述 React 私有 state 名称。 - 测试用 fixture 只包含本行为需要的字段。演化中的 payload 使用 `expect.objectContaining(...)` 或 helper 生成默认对象,避免一处契约加字段导致大量无关用例碎裂。 - 当测试是为防止历史回归,应在测试名或邻近注释中说明防的是什么行为,而不是记录实现步骤。 +- 测试 React effect 中注册的事件监听时,触发事件前先用可观测的 listener 调用确认注册已完成;异步请求已开始应用有界 `waitFor` 断言确认,不要用无界手工 Promise 等待一次性信号。解除挂起请求时将 Promise 收尾纳入异步 `act`,确保后续 React 更新在断言前已冲刷。 ## 试点调整