修复项目切换清单竞态测试超时
Project CI / Repository checks (pull_request) Successful in 1m1s
Project CI / Frontend tests (pull_request) Successful in 3m28s
Project CI / Backend tests (pull_request) Successful in 4m1s
Project CI / Native shell tests (pull_request) Failing after 10m14s

等待事件监听注册后再触发 manifest 失效并以有界断言确认读取启动
将迟到响应收尾纳入异步 act 避免测试时序悬挂
补充 React 异步事件测试准则
This commit is contained in:
2026-08-07 14:26:49 +08:00
parent ab5ec2c40f
commit 4911801ed6
2 changed files with 27 additions and 12 deletions
@@ -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<typeof staleFirstManifest>((resolve) => {
resolveStaleRefresh = resolve;
});
let holdFirstRefresh = false;
let signalFirstRefreshStarted!: () => void;
const firstRefreshStarted = new Promise<void>((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();
@@ -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 更新在断言前已冲刷。
## 试点调整