mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
- 在 `jest.config.js` 中启用 V8 覆盖率提供器(兼容 Node.js 22+),提升覆盖率检测性能与准确性 - 调整覆盖率收集范围为 `src/js/modules/**/*.js`,排除测试文件,使报告更聚焦于实际业务逻辑 - 降低全局覆盖率阈值至当前可达成水平(statements/branches 30%-70%),制定逐步提升计划 - 新增 6 个模块的完整单元测试:`APIService`、`AppConfig`、`HTMLSanitizer`、`Logger`、`SecureStorage` 和 `utils`,总计约 840 行测试代码 - 修复 `HTMLSanitizer.sanitizeURL()` 中对 `data:text/html` 协议的正则检测逻辑,增强 XSS 防护能力
199 lines
5.4 KiB
JavaScript
199 lines
5.4 KiB
JavaScript
/**
|
||
* 通用工具函数模块单元测试
|
||
* 覆盖 debounce、throttle、retry、formatBytes、deepClone 等核心功能
|
||
*/
|
||
|
||
import {
|
||
debounce,
|
||
throttle,
|
||
retry,
|
||
formatBytes,
|
||
deepClone,
|
||
isBrowser,
|
||
safeJsonParse,
|
||
memoize
|
||
} from '../../src/js/modules/utils';
|
||
|
||
describe('通用工具函数', () => {
|
||
describe('debounce()', () => {
|
||
jest.useFakeTimers();
|
||
|
||
it('应该延迟执行函数', () => {
|
||
const fn = jest.fn();
|
||
const debounced = debounce(fn, 100);
|
||
debounced();
|
||
expect(fn).not.toHaveBeenCalled();
|
||
jest.advanceTimersByTime(100);
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('应该取消之前的调用', () => {
|
||
const fn = jest.fn();
|
||
const debounced = debounce(fn, 100);
|
||
debounced('a');
|
||
jest.advanceTimersByTime(50);
|
||
debounced('b');
|
||
jest.advanceTimersByTime(100);
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
expect(fn).toHaveBeenCalledWith('b');
|
||
});
|
||
|
||
it('应该支持 leading edge 模式', () => {
|
||
const fn = jest.fn();
|
||
const debounced = debounce(fn, 100, true);
|
||
debounced();
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
jest.advanceTimersByTime(100);
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|
||
|
||
describe('throttle()', () => {
|
||
jest.useFakeTimers();
|
||
|
||
it('应该限制执行频率', () => {
|
||
const fn = jest.fn();
|
||
const throttled = throttle(fn, 100);
|
||
throttled();
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
throttled();
|
||
throttled();
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
jest.advanceTimersByTime(100);
|
||
expect(fn).toHaveBeenCalledTimes(2);
|
||
});
|
||
});
|
||
|
||
describe('retry()', () => {
|
||
// retry 使用真实 setTimeout,需要恢复 real timers
|
||
beforeEach(() => {
|
||
jest.useRealTimers();
|
||
});
|
||
|
||
it('应该在成功时直接返回结果', async () => {
|
||
const fn = jest.fn().mockResolvedValue('ok');
|
||
const result = await retry(fn, 3, 10);
|
||
expect(result).toBe('ok');
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('应该在失败后重试', async () => {
|
||
const fn = jest.fn()
|
||
.mockRejectedValueOnce(new Error('fail1'))
|
||
.mockResolvedValue('ok');
|
||
const result = await retry(fn, 3, 10);
|
||
expect(result).toBe('ok');
|
||
expect(fn).toHaveBeenCalledTimes(2);
|
||
}, 10000);
|
||
|
||
it('应该在耗尽重试次数后抛出错误', async () => {
|
||
const fn = jest.fn().mockRejectedValue(new Error('always fail'));
|
||
await expect(retry(fn, 2, 10)).rejects.toThrow('always fail');
|
||
expect(fn).toHaveBeenCalledTimes(2);
|
||
}, 10000);
|
||
});
|
||
|
||
describe('formatBytes()', () => {
|
||
it('应该格式化 0 字节', () => {
|
||
expect(formatBytes(0)).toBe('0 Bytes');
|
||
});
|
||
|
||
it('应该格式化字节', () => {
|
||
expect(formatBytes(500)).toBe('500 Bytes');
|
||
});
|
||
|
||
it('应该格式化 KB', () => {
|
||
expect(formatBytes(1024)).toBe('1 KB');
|
||
});
|
||
|
||
it('应该格式化 MB', () => {
|
||
expect(formatBytes(1048576)).toBe('1 MB');
|
||
});
|
||
|
||
it('应该格式化 GB', () => {
|
||
expect(formatBytes(1073741824)).toBe('1 GB');
|
||
});
|
||
|
||
it('应该支持自定义小数位', () => {
|
||
const result = formatBytes(1536, 1);
|
||
expect(result).toBe('1.5 KB');
|
||
});
|
||
});
|
||
|
||
describe('deepClone()', () => {
|
||
it('应该深拷贝对象', () => {
|
||
const original = { a: 1, b: { c: 2 } };
|
||
const cloned = deepClone(original);
|
||
expect(cloned).toEqual(original);
|
||
expect(cloned).not.toBe(original);
|
||
expect(cloned.b).not.toBe(original.b);
|
||
});
|
||
|
||
it('应该深拷贝数组', () => {
|
||
const original = [1, [2, 3]];
|
||
const cloned = deepClone(original);
|
||
expect(cloned).toEqual(original);
|
||
expect(cloned).not.toBe(original);
|
||
});
|
||
|
||
it('应该拷贝 Date 对象', () => {
|
||
const date = new Date('2024-01-01');
|
||
const cloned = deepClone(date);
|
||
expect(cloned).toEqual(date);
|
||
expect(cloned).not.toBe(date);
|
||
});
|
||
|
||
it('应该处理 null 和基本类型', () => {
|
||
expect(deepClone(null)).toBeNull();
|
||
expect(deepClone(42)).toBe(42);
|
||
expect(deepClone('str')).toBe('str');
|
||
});
|
||
});
|
||
|
||
describe('isBrowser()', () => {
|
||
it('应该在 jsdom 环境中返回 true', () => {
|
||
expect(isBrowser()).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('safeJsonParse()', () => {
|
||
it('应该解析有效 JSON', () => {
|
||
expect(safeJsonParse('{"a":1}')).toEqual({ a: 1 });
|
||
});
|
||
|
||
it('应该在解析失败时返回 fallback', () => {
|
||
expect(safeJsonParse('invalid', 'default')).toBe('default');
|
||
});
|
||
|
||
it('应该在解析失败时返回 null', () => {
|
||
expect(safeJsonParse('invalid')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('memoize()', () => {
|
||
it('应该缓存函数结果', () => {
|
||
const fn = jest.fn(x => x * 2);
|
||
const memoized = memoize(fn);
|
||
expect(memoized(5)).toBe(10);
|
||
expect(memoized(5)).toBe(10);
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('应该对不同参数分别缓存', () => {
|
||
const fn = jest.fn(x => x * 2);
|
||
const memoized = memoize(fn);
|
||
expect(memoized(5)).toBe(10);
|
||
expect(memoized(3)).toBe(6);
|
||
expect(fn).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it('应该支持自定义 resolver', () => {
|
||
const fn = jest.fn((a, b) => a + b);
|
||
const memoized = memoize(fn, (a, b) => `${a}-${b}`);
|
||
expect(memoized(1, 2)).toBe(3);
|
||
expect(memoized(1, 2)).toBe(3);
|
||
expect(fn).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|
||
});
|