Files
eSIM-Tools/tests/modules/utils.test.js
Abner 9ed51287c0 test: 新增多个核心模块的单元测试并优化 Jest 配置
- 在 `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 防护能力
2026-05-05 10:35:14 +08:00

199 lines
5.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 通用工具函数模块单元测试
* 覆盖 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);
});
});
});