修复前端测试偶发超时与异步清理 #391
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user