- 文件上传验证: ModelUploader组件增强、上传API错误处理 - GLTF解析器: 增强节点提取和元数据 - 板材编辑: PanelEditor客户端验证 + panels API服务端验证 - 装配步骤编辑: 修复未使用代码,添加预览按钮 - 装配动画播放: AssemblyPlayer步骤导航边界检查优化 - 分享链接: 完整CRUD API(创建、获取、删除),含验证和过期检查 跳过可选测试任务以加快MVP开发
404 lines
10 KiB
TypeScript
404 lines
10 KiB
TypeScript
/**
|
|
* 性能优化工具函数单元测试
|
|
* 测试懒加载、防抖、节流等性能优化功能
|
|
*/
|
|
|
|
import {
|
|
debounce,
|
|
throttle,
|
|
preloadImage,
|
|
preloadImages,
|
|
isSlowNetwork,
|
|
isDataSaverEnabled,
|
|
measurePerformance,
|
|
measureAsyncPerformance,
|
|
requestIdleCallback,
|
|
cancelIdleCallback,
|
|
} from '@/lib/performance';
|
|
|
|
// Mock 浏览器 API
|
|
global.Image = class {
|
|
onload: (() => void) | null = null;
|
|
onerror: ((error: Error) => void) | null = null;
|
|
src = '';
|
|
|
|
constructor() {
|
|
// 模拟图片加载成功
|
|
setTimeout(() => {
|
|
if (this.onload) {
|
|
this.onload();
|
|
}
|
|
}, 10);
|
|
}
|
|
} as any;
|
|
|
|
describe('性能优化工具函数测试', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('debounce - 防抖函数', () => {
|
|
beforeEach(() => {
|
|
jest.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('应该延迟执行函数', () => {
|
|
const mockFn = jest.fn();
|
|
const debouncedFn = debounce(mockFn, 300);
|
|
|
|
debouncedFn('test');
|
|
expect(mockFn).not.toHaveBeenCalled();
|
|
|
|
jest.advanceTimersByTime(300);
|
|
expect(mockFn).toHaveBeenCalledWith('test');
|
|
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('应该在延迟期间重新触发时重置计时器', () => {
|
|
const mockFn = jest.fn();
|
|
const debouncedFn = debounce(mockFn, 300);
|
|
|
|
debouncedFn('first');
|
|
jest.advanceTimersByTime(200);
|
|
|
|
debouncedFn('second');
|
|
jest.advanceTimersByTime(200);
|
|
|
|
expect(mockFn).not.toHaveBeenCalled();
|
|
|
|
jest.advanceTimersByTime(100);
|
|
expect(mockFn).toHaveBeenCalledWith('second');
|
|
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('应该支持多个参数', () => {
|
|
const mockFn = jest.fn();
|
|
const debouncedFn = debounce(mockFn, 300);
|
|
|
|
debouncedFn('arg1', 'arg2', 'arg3');
|
|
jest.advanceTimersByTime(300);
|
|
|
|
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2', 'arg3');
|
|
});
|
|
});
|
|
|
|
describe('throttle - 节流函数', () => {
|
|
beforeEach(() => {
|
|
jest.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('应该立即执行第一次调用', () => {
|
|
const mockFn = jest.fn();
|
|
const throttledFn = throttle(mockFn, 300);
|
|
|
|
throttledFn('test');
|
|
expect(mockFn).toHaveBeenCalledWith('test');
|
|
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('应该在限制时间内忽略后续调用', () => {
|
|
const mockFn = jest.fn();
|
|
const throttledFn = throttle(mockFn, 300);
|
|
|
|
throttledFn('first');
|
|
throttledFn('second');
|
|
throttledFn('third');
|
|
|
|
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
expect(mockFn).toHaveBeenCalledWith('first');
|
|
});
|
|
|
|
it('应该在限制时间后允许再次执行', () => {
|
|
const mockFn = jest.fn();
|
|
const throttledFn = throttle(mockFn, 300);
|
|
|
|
throttledFn('first');
|
|
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
|
|
jest.advanceTimersByTime(300);
|
|
|
|
throttledFn('second');
|
|
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
expect(mockFn).toHaveBeenCalledWith('second');
|
|
});
|
|
});
|
|
|
|
describe('preloadImage - 图片预加载', () => {
|
|
beforeEach(() => {
|
|
// Mock 图片加载成功
|
|
global.Image = class {
|
|
onload: (() => void) | null = null;
|
|
onerror: ((error: Error) => void) | null = null;
|
|
src = '';
|
|
|
|
constructor() {
|
|
setTimeout(() => {
|
|
if (this.onload) {
|
|
this.onload();
|
|
}
|
|
}, 10);
|
|
}
|
|
} as any;
|
|
});
|
|
|
|
it('应该成功预加载图片', async () => {
|
|
const src = '/test-image.jpg';
|
|
await expect(preloadImage(src)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('应该处理图片加载失败', async () => {
|
|
// Mock 图片加载失败
|
|
global.Image = class {
|
|
onload: (() => void) | null = null;
|
|
onerror: ((error: Error) => void) | null = null;
|
|
src = '';
|
|
|
|
constructor() {
|
|
setTimeout(() => {
|
|
if (this.onerror) {
|
|
this.onerror(new Error('加载失败'));
|
|
}
|
|
}, 10);
|
|
}
|
|
} as any;
|
|
|
|
const src = '/invalid-image.jpg';
|
|
await expect(preloadImage(src)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('preloadImages - 批量预加载图片', () => {
|
|
beforeEach(() => {
|
|
// Mock 图片加载成功
|
|
global.Image = class {
|
|
onload: (() => void) | null = null;
|
|
onerror: ((error: Error) => void) | null = null;
|
|
src = '';
|
|
|
|
constructor() {
|
|
setTimeout(() => {
|
|
if (this.onload) {
|
|
this.onload();
|
|
}
|
|
}, 10);
|
|
}
|
|
} as any;
|
|
});
|
|
|
|
it('应该成功预加载多张图片', async () => {
|
|
const srcs = ['/image1.jpg', '/image2.jpg', '/image3.jpg'];
|
|
await expect(preloadImages(srcs)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('应该处理空数组', async () => {
|
|
await expect(preloadImages([])).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('isSlowNetwork - 检测慢速网络', () => {
|
|
it('应该在没有 connection API 时返回 false', () => {
|
|
// @ts-ignore
|
|
delete navigator.connection;
|
|
expect(isSlowNetwork()).toBe(false);
|
|
});
|
|
|
|
it('应该检测慢速连接类型', () => {
|
|
Object.defineProperty(navigator, 'connection', {
|
|
writable: true,
|
|
value: {
|
|
effectiveType: '2g',
|
|
},
|
|
});
|
|
|
|
expect(isSlowNetwork()).toBe(true);
|
|
});
|
|
|
|
it('应该检测低下载速度', () => {
|
|
Object.defineProperty(navigator, 'connection', {
|
|
writable: true,
|
|
value: {
|
|
effectiveType: '4g',
|
|
downlink: 0.5,
|
|
},
|
|
});
|
|
|
|
expect(isSlowNetwork()).toBe(true);
|
|
});
|
|
|
|
it('应该在快速网络时返回 false', () => {
|
|
Object.defineProperty(navigator, 'connection', {
|
|
writable: true,
|
|
value: {
|
|
effectiveType: '4g',
|
|
downlink: 10,
|
|
},
|
|
});
|
|
|
|
expect(isSlowNetwork()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isDataSaverEnabled - 检测数据节省模式', () => {
|
|
it('应该在没有 connection API 时返回 false', () => {
|
|
Object.defineProperty(navigator, 'connection', {
|
|
writable: true,
|
|
value: undefined,
|
|
});
|
|
expect(isDataSaverEnabled()).toBe(false);
|
|
});
|
|
|
|
it('应该检测数据节省模式已启用', () => {
|
|
Object.defineProperty(navigator, 'connection', {
|
|
writable: true,
|
|
value: {
|
|
saveData: true,
|
|
},
|
|
});
|
|
|
|
expect(isDataSaverEnabled()).toBe(true);
|
|
});
|
|
|
|
it('应该检测数据节省模式未启用', () => {
|
|
Object.defineProperty(navigator, 'connection', {
|
|
writable: true,
|
|
value: {
|
|
saveData: false,
|
|
},
|
|
});
|
|
|
|
expect(isDataSaverEnabled()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('measurePerformance - 性能监控', () => {
|
|
it('应该测量同步函数执行时间', () => {
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
|
|
const testFn = (x: number, y: number) => x + y;
|
|
const measuredFn = measurePerformance(testFn, '加法运算');
|
|
|
|
const result = measuredFn(1, 2);
|
|
|
|
expect(result).toBe(3);
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('[性能] 加法运算:')
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('应该保持函数的返回值', () => {
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
|
|
const testFn = () => 'test result';
|
|
const measuredFn = measurePerformance(testFn, '测试函数');
|
|
|
|
expect(measuredFn()).toBe('test result');
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('measureAsyncPerformance - 异步性能监控', () => {
|
|
it('应该测量异步函数执行时间', async () => {
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
|
|
const asyncFn = async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
return 'async result';
|
|
};
|
|
|
|
const result = await measureAsyncPerformance(asyncFn, '异步操作');
|
|
|
|
expect(result).toBe('async result');
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('[性能] 异步操作:')
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('应该处理异步函数错误', async () => {
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
|
|
const asyncFn = async () => {
|
|
throw new Error('测试错误');
|
|
};
|
|
|
|
await expect(
|
|
measureAsyncPerformance(asyncFn, '错误操作')
|
|
).rejects.toThrow('测试错误');
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('requestIdleCallback - 空闲回调', () => {
|
|
it('应该使用原生 requestIdleCallback', () => {
|
|
const mockRequestIdleCallback = jest.fn().mockReturnValue(123);
|
|
window.requestIdleCallback = mockRequestIdleCallback;
|
|
|
|
const callback = jest.fn();
|
|
const id = requestIdleCallback(callback);
|
|
|
|
expect(mockRequestIdleCallback).toHaveBeenCalledWith(callback, undefined);
|
|
expect(id).toBe(123);
|
|
});
|
|
|
|
it('应该支持 timeout 选项', () => {
|
|
const mockRequestIdleCallback = jest.fn().mockReturnValue(123);
|
|
window.requestIdleCallback = mockRequestIdleCallback;
|
|
|
|
const callback = jest.fn();
|
|
const options = { timeout: 1000 };
|
|
requestIdleCallback(callback, options);
|
|
|
|
expect(mockRequestIdleCallback).toHaveBeenCalledWith(callback, options);
|
|
});
|
|
|
|
it('应该在不支持时降级到 setTimeout', () => {
|
|
// @ts-ignore
|
|
delete window.requestIdleCallback;
|
|
const mockSetTimeout = jest.spyOn(window, 'setTimeout');
|
|
|
|
const callback = jest.fn();
|
|
requestIdleCallback(callback);
|
|
|
|
expect(mockSetTimeout).toHaveBeenCalledWith(callback, 1);
|
|
|
|
mockSetTimeout.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('cancelIdleCallback - 取消空闲回调', () => {
|
|
it('应该使用原生 cancelIdleCallback', () => {
|
|
const mockCancelIdleCallback = jest.fn();
|
|
window.cancelIdleCallback = mockCancelIdleCallback;
|
|
|
|
cancelIdleCallback(123);
|
|
|
|
expect(mockCancelIdleCallback).toHaveBeenCalledWith(123);
|
|
});
|
|
|
|
it('应该在不支持时降级到 clearTimeout', () => {
|
|
// @ts-ignore
|
|
delete window.cancelIdleCallback;
|
|
const mockClearTimeout = jest.spyOn(window, 'clearTimeout');
|
|
|
|
cancelIdleCallback(123);
|
|
|
|
expect(mockClearTimeout).toHaveBeenCalledWith(123);
|
|
|
|
mockClearTimeout.mockRestore();
|
|
});
|
|
});
|
|
});
|