修复前端测试偶发超时与异步清理 #391

Merged
lhk229 merged 3 commits from fix/fte into master 2026-09-16 21:53:19 +08:00
6 changed files with 77 additions and 13 deletions
@@ -38,6 +38,7 @@ vi.mock('../api/adminApiClient', () => ({
interface MockIntersectionObserverController {
enter: (target: Element) => void;
enterAll: (targets: Element[]) => void;
isObserved: (target: Element) => boolean;
}
@@ -106,6 +107,25 @@ function installIntersectionObserverMock(): MockIntersectionObserverController {
);
});
},
enterAll(targets) {
act(() => {
for (const target of targets) {
const record = observed.get(target);
if (!record) {
throw new Error('目标缩略图尚未进入 IntersectionObserver');
}
record.callback(
[
{
isIntersecting: true,
target,
} as IntersectionObserverEntry,
],
record.observer,
);
}
});
},
isObserved(target) {
return observed.has(target);
},
@@ -753,10 +773,10 @@ test('后台素材查询为大量同时可见的缩略图持续错峰换签', as
const thumbnails = entries.map((entry) =>
thumbnailElementForLabel(entry.label),
);
thumbnails.forEach((thumbnail) => {
for (const thumbnail of thumbnails) {
expect(observer.isObserved(thumbnail)).toBe(true);
observer.enter(thumbnail);
});
}
observer.enterAll(thumbnails);
await act(async () => {
await Promise.resolve();
});
@@ -776,7 +796,7 @@ test('后台素材查询为大量同时可见的缩略图持续错峰换签', as
await vi.advanceTimersByTimeAsync(200);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(105);
});
}, 10_000);
test('后台素材查询读取更多后为新进入可视区域的素材换签', async () => {
const observer = installIntersectionObserverMock();
@@ -38,11 +38,16 @@ export function useDirectActiveTurns({
const [snapshotReadFailed, setSnapshotReadFailed] = useState(false);
const mountedRef = useRef(true);
const inFlightRef = useRef<Promise<void> | null>(null);
const retryTimerRef = useRef<number | null>(null);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
if (retryTimerRef.current !== null) {
window.clearTimeout(retryTimerRef.current);
retryTimerRef.current = null;
}
};
}, []);
@@ -73,12 +78,12 @@ export function useDirectActiveTurns({
return;
} catch {
if (attempt < DIRECT_ACTIVE_TURNS_READ_ATTEMPTS) {
await new Promise((resolve) =>
window.setTimeout(
resolve,
DIRECT_ACTIVE_TURNS_READ_RETRY_DELAY_MS * attempt,
),
);
await new Promise<void>((resolve) => {
retryTimerRef.current = window.setTimeout(() => {
retryTimerRef.current = null;
resolve();
}, DIRECT_ACTIVE_TURNS_READ_RETRY_DELAY_MS * attempt);
});
}
}
}
@@ -3350,7 +3350,7 @@ export function registerProjectWorkbenchFoundationTests() {
expect(URL.createObjectURL).toHaveBeenCalledTimes(
objectUrlCountBeforeLateResult,
);
});
}, 20_000);
it('renders immutable manifest versions, their parent graph, and bound asset highlights', async () => {
const manifest = createGameCreationAppManifest(
@@ -1,12 +1,49 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import {
act,
cleanup,
fireEvent,
render,
renderHook,
screen,
} from '@testing-library/react';
import { afterEach, expect, it, vi } from 'vitest';
import { useDirectActiveTurns } from '../src/features/agent-runtime/directActiveTurns';
import { ActiveProjectRunsPanel } from '../src/features/app-shell/ActiveProjectRunsPanel';
afterEach(() => cleanup());
it('读取失败后的重试定时器会在卸载后清理', async () => {
vi.useFakeTimers();
const invoke = vi.fn(async () => {
throw new Error('temporarily unavailable');
});
const clearTimeoutSpy = vi.spyOn(window, 'clearTimeout');
try {
const { unmount } = renderHook(() =>
useDirectActiveTurns({ invoke, enabled: true }),
);
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
expect(invoke).toHaveBeenCalledTimes(1);
unmount();
expect(clearTimeoutSpy).toHaveBeenCalled();
await act(async () => {
await vi.advanceTimersByTimeAsync(1_000);
});
expect(invoke).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
clearTimeoutSpy.mockRestore();
}
});
it('按开始时间展示正在运行的项目并支持进入项目', () => {
const onOpenProject = vi.fn();
render(
@@ -106,6 +106,8 @@ function installInvokeMock() {
return { revision: HELD_REVISION };
case 'get_design_agent_runtime_mode':
return null;
case 'list_game_creator_direct_active_turns':
return [];
case 'read_project_permission_policy':
return {
projectPath: PROJECT_PATH,
@@ -1351,7 +1351,7 @@ describe('CreationLandingView', () => {
`/api/assets/read-url?objectKey=${encodeURIComponent(campaignObjectKey)}`,
expect.objectContaining({ method: 'GET' }),
);
});
}, 10_000);
it('loads more featured resources when the list reaches the end', async () => {
const observers = installIntersectionObserverMock();