312 lines
9.9 KiB
TypeScript
312 lines
9.9 KiB
TypeScript
|
|
import { World, Scene, Entity } from '@esengine/ecs-framework';
|
|||
|
|
import { AssetLoadingManager } from '../src/Services/AssetLoadingManager';
|
|||
|
|
import {
|
|||
|
|
LoadingState,
|
|||
|
|
TimeoutError,
|
|||
|
|
CircularDependencyError,
|
|||
|
|
EntityDestroyedError
|
|||
|
|
} from '../src/Services/AssetLoadingTypes';
|
|||
|
|
|
|||
|
|
describe('AssetLoadingManager', () => {
|
|||
|
|
let manager: AssetLoadingManager;
|
|||
|
|
let world: World;
|
|||
|
|
let scene: Scene;
|
|||
|
|
let parentEntity: Entity;
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
manager = new AssetLoadingManager();
|
|||
|
|
world = new World();
|
|||
|
|
scene = world.createScene('test');
|
|||
|
|
parentEntity = scene.createEntity('parent');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
afterEach(() => {
|
|||
|
|
manager.dispose();
|
|||
|
|
parentEntity.destroy();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('基本加载功能', () => {
|
|||
|
|
test('成功加载资产', async () => {
|
|||
|
|
const mockEntity = scene.createEntity('loaded');
|
|||
|
|
|
|||
|
|
const loader = jest.fn().mockResolvedValue(mockEntity);
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Loading);
|
|||
|
|
|
|||
|
|
const result = await handle.promise;
|
|||
|
|
|
|||
|
|
expect(result).toBe(mockEntity);
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Loaded);
|
|||
|
|
expect(loader).toHaveBeenCalledTimes(1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('加载失败', async () => {
|
|||
|
|
const mockError = new Error('Load failed');
|
|||
|
|
const loader = jest.fn().mockRejectedValue(mockError);
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader,
|
|||
|
|
{ maxRetries: 0 }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await expect(handle.promise).rejects.toThrow('Load failed');
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Failed);
|
|||
|
|
expect(handle.getError()).toBe(mockError);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('超时机制', () => {
|
|||
|
|
test('加载超时', async () => {
|
|||
|
|
const loader = jest.fn().mockImplementation(() =>
|
|||
|
|
new Promise(resolve => setTimeout(resolve, 10000))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader,
|
|||
|
|
{ timeoutMs: 100, maxRetries: 0 }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await expect(handle.promise).rejects.toThrow(TimeoutError);
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Timeout);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('超时前完成', async () => {
|
|||
|
|
const mockEntity = scene.createEntity('loaded');
|
|||
|
|
|
|||
|
|
const loader = jest.fn().mockImplementation(() =>
|
|||
|
|
new Promise(resolve => setTimeout(() => resolve(mockEntity), 50))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader,
|
|||
|
|
{ timeoutMs: 200 }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const result = await handle.promise;
|
|||
|
|
|
|||
|
|
expect(result).toBe(mockEntity);
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Loaded);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('重试机制', () => {
|
|||
|
|
test('失败后自动重试', async () => {
|
|||
|
|
const mockEntity = scene.createEntity('loaded');
|
|||
|
|
let attemptCount = 0;
|
|||
|
|
|
|||
|
|
const loader = jest.fn().mockImplementation(() => {
|
|||
|
|
attemptCount++;
|
|||
|
|
if (attemptCount < 3) {
|
|||
|
|
return Promise.reject(new Error('Temporary error'));
|
|||
|
|
}
|
|||
|
|
return Promise.resolve(mockEntity);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader,
|
|||
|
|
{ maxRetries: 3 }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const result = await handle.promise;
|
|||
|
|
|
|||
|
|
expect(result).toBe(mockEntity);
|
|||
|
|
expect(loader).toHaveBeenCalledTimes(3);
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Loaded);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('重试次数用尽后失败', async () => {
|
|||
|
|
const loader = jest.fn().mockRejectedValue(new Error('Persistent error'));
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader,
|
|||
|
|
{ maxRetries: 2 }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await expect(handle.promise).rejects.toThrow('Persistent error');
|
|||
|
|
expect(loader).toHaveBeenCalledTimes(3); // 初始 + 2次重试
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Failed);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('循环引用检测', () => {
|
|||
|
|
test('检测直接循环引用', () => {
|
|||
|
|
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
|||
|
|
|
|||
|
|
// 先加载 assetA
|
|||
|
|
const handleA = manager.startLoading(
|
|||
|
|
'assetA',
|
|||
|
|
parentEntity,
|
|||
|
|
loader,
|
|||
|
|
{ parentAssetId: undefined }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
expect(handleA.getState()).toBe(LoadingState.Loading);
|
|||
|
|
|
|||
|
|
// 尝试在 assetA 的上下文中加载 assetB
|
|||
|
|
// assetB 又尝试加载 assetA(循环)
|
|||
|
|
expect(() => {
|
|||
|
|
manager.startLoading(
|
|||
|
|
'assetB',
|
|||
|
|
parentEntity,
|
|||
|
|
loader,
|
|||
|
|
{ parentAssetId: 'assetB' } // assetB 的父是 assetB(自我循环)
|
|||
|
|
);
|
|||
|
|
}).toThrow(CircularDependencyError);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('不误报非循环引用', () => {
|
|||
|
|
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
|||
|
|
|
|||
|
|
// assetA 加载 assetB(正常)
|
|||
|
|
const handleA = manager.startLoading(
|
|||
|
|
'assetA',
|
|||
|
|
parentEntity,
|
|||
|
|
loader
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// assetB 加载 assetC(正常,不是循环)
|
|||
|
|
expect(() => {
|
|||
|
|
manager.startLoading(
|
|||
|
|
'assetC',
|
|||
|
|
parentEntity,
|
|||
|
|
loader,
|
|||
|
|
{ parentAssetId: 'assetB' }
|
|||
|
|
);
|
|||
|
|
}).not.toThrow();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('实体生命周期安全', () => {
|
|||
|
|
test('实体销毁后取消加载', async () => {
|
|||
|
|
const loader = jest.fn().mockImplementation(() =>
|
|||
|
|
new Promise(resolve => setTimeout(resolve, 100))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 销毁实体
|
|||
|
|
parentEntity.destroy();
|
|||
|
|
|
|||
|
|
// 等待一小段时间让检测生效
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|||
|
|
|
|||
|
|
await expect(handle.promise).rejects.toThrow(EntityDestroyedError);
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Cancelled);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('状态查询', () => {
|
|||
|
|
test('获取加载进度', async () => {
|
|||
|
|
const mockEntity = scene.createEntity('loaded');
|
|||
|
|
|
|||
|
|
const loader = jest.fn().mockImplementation(() =>
|
|||
|
|
new Promise(resolve => setTimeout(() => resolve(mockEntity), 100))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const progress = handle.getProgress();
|
|||
|
|
|
|||
|
|
expect(progress.state).toBe(LoadingState.Loading);
|
|||
|
|
expect(progress.elapsedMs).toBeGreaterThanOrEqual(0);
|
|||
|
|
expect(progress.retryCount).toBe(0);
|
|||
|
|
expect(progress.maxRetries).toBe(3);
|
|||
|
|
|
|||
|
|
await handle.promise;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('获取统计信息', () => {
|
|||
|
|
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
|||
|
|
|
|||
|
|
manager.startLoading('asset1', parentEntity, loader);
|
|||
|
|
manager.startLoading('asset2', parentEntity, loader);
|
|||
|
|
|
|||
|
|
const stats = manager.getStats();
|
|||
|
|
|
|||
|
|
expect(stats.totalTasks).toBe(2);
|
|||
|
|
expect(stats.loadingTasks).toBe(2);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('获取正在加载的资产列表', () => {
|
|||
|
|
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
|||
|
|
|
|||
|
|
manager.startLoading('asset1', parentEntity, loader);
|
|||
|
|
manager.startLoading('asset2', parentEntity, loader);
|
|||
|
|
|
|||
|
|
const loadingAssets = manager.getLoadingAssets();
|
|||
|
|
|
|||
|
|
expect(loadingAssets).toContain('asset1');
|
|||
|
|
expect(loadingAssets).toContain('asset2');
|
|||
|
|
expect(loadingAssets.length).toBe(2);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('任务管理', () => {
|
|||
|
|
test('取消加载任务', () => {
|
|||
|
|
const loader = jest.fn().mockImplementation(() =>
|
|||
|
|
new Promise(resolve => setTimeout(resolve, 1000))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const handle = manager.startLoading(
|
|||
|
|
'test-asset',
|
|||
|
|
parentEntity,
|
|||
|
|
loader
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Loading);
|
|||
|
|
|
|||
|
|
handle.cancel();
|
|||
|
|
|
|||
|
|
expect(handle.getState()).toBe(LoadingState.Cancelled);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('清空所有任务', async () => {
|
|||
|
|
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
|||
|
|
|
|||
|
|
manager.startLoading('asset1', parentEntity, loader);
|
|||
|
|
manager.startLoading('asset2', parentEntity, loader);
|
|||
|
|
|
|||
|
|
expect(manager.getLoadingAssets().length).toBe(2);
|
|||
|
|
|
|||
|
|
manager.clear();
|
|||
|
|
|
|||
|
|
expect(manager.getLoadingAssets().length).toBe(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('复用已存在的加载任务', () => {
|
|||
|
|
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
|||
|
|
|
|||
|
|
const handle1 = manager.startLoading('test-asset', parentEntity, loader);
|
|||
|
|
const handle2 = manager.startLoading('test-asset', parentEntity, loader);
|
|||
|
|
|
|||
|
|
// 应该返回同一个任务
|
|||
|
|
expect(handle1.assetId).toBe(handle2.assetId);
|
|||
|
|
expect(loader).toHaveBeenCalledTimes(1); // 只加载一次
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|