/** * utils.js 单元测试 * 覆盖所有工具函数 */ import { generateId, escapeHTML, prefersDark, debounce, throttle, deepMerge, isBrowser, formatFileSize, sleep, } from '../src/utils.js'; describe('generateId', () => { test('返回字符串', () => { expect(typeof generateId()).toBe('string'); }); test('以 me- 前缀开头', () => { expect(generateId()).toMatch(/^me-/); }); test('多次调用产生不同值', () => { const ids = new Set(); for (let i = 0; i < 100; i++) ids.add(generateId()); expect(ids.size).toBe(100); }); }); describe('escapeHTML', () => { test('转义 < > &', () => { const out = escapeHTML('&b'); expect(out).toContain('<'); expect(out).toContain('>'); expect(out).toContain('&'); expect(out).not.toContain(''); }); test('null/undefined 转空字符串', () => { expect(escapeHTML(null)).toBe(''); expect(escapeHTML(undefined)).toBe(''); }); test('数字被转为字符串', () => { expect(escapeHTML(123)).toBe('123'); }); test('无特殊字符原样返回', () => { expect(escapeHTML('hello world')).toBe('hello world'); }); }); describe('prefersDark', () => { test('返回布尔值', () => { expect(typeof prefersDark()).toBe('boolean'); }); }); describe('debounce', () => { test('延迟执行', (done) => { let called = 0; const fn = debounce(() => { called++; }, 50); fn(); expect(called).toBe(0); setTimeout(() => { expect(called).toBe(1); done(); }, 100); }); test('多次调用只执行最后一次', (done) => { let result = 0; const fn = debounce((v) => { result = v; }, 50); fn(1); fn(2); fn(3); setTimeout(() => { expect(result).toBe(3); done(); }, 100); }); test('immediate 模式立即执行', () => { let called = 0; const fn = debounce(() => { called++; }, 50, true); fn(); expect(called).toBe(1); }); test('保留 this 上下文', (done) => { const obj = { val: 42, getVal: debounce(function () { return this.val; }, 30), }; const p = obj.getVal(); // immediate=false 时首次不返回,但 this 应正确 setTimeout(() => { expect(typeof p).toBe('undefined'); // debounce 返回的是 undefined(异步执行) done(); }, 60); }); test('传递参数', (done) => { let captured = null; const fn = debounce((a, b) => { captured = a + b; }, 30); fn(1, 2); setTimeout(() => { expect(captured).toBe(3); done(); }, 60); }); }); describe('throttle', () => { test('首次立即执行', () => { let called = 0; const fn = throttle(() => { called++; }, 50); fn(); expect(called).toBe(1); }); test('限流期内不重复执行', () => { let called = 0; const fn = throttle(() => { called++; }, 50); fn(); fn(); fn(); expect(called).toBe(1); }); test('限流期后可再次执行', (done) => { let called = 0; const fn = throttle(() => { called++; }, 40); fn(); setTimeout(() => { fn(); expect(called).toBe(2); done(); }, 60); }); test('保留 this 与参数', (done) => { const obj = { n: 10, run: throttle(function (x) { this.result = this.n + x; }, 30), }; obj.run(5); expect(obj.result).toBe(15); done(); }); }); describe('deepMerge', () => { test('浅层合并', () => { const out = deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 }); expect(out).toEqual({ a: 1, b: 3, c: 4 }); }); test('深层对象合并', () => { const out = deepMerge({ obj: { x: 1, y: 2 } }, { obj: { y: 3, z: 4 } }); expect(out).toEqual({ obj: { x: 1, y: 3, z: 4 } }); }); test('source 覆盖 target 非对象值', () => { const out = deepMerge({ a: { x: 1 } }, { a: 'str' }); expect(out.a).toBe('str'); }); test('不修改原始 target', () => { const target = { a: { x: 1 } }; const source = { a: { y: 2 } }; deepMerge(target, source); expect(target.a).toEqual({ x: 1 }); }); test('多层嵌套合并', () => { const out = deepMerge( { a: { b: { c: 1, d: 2 } } }, { a: { b: { d: 3, e: 4 } } } ); expect(out).toEqual({ a: { b: { c: 1, d: 3, e: 4 } } }); }); }); describe('isBrowser', () => { test('jsdom 环境下返回 true', () => { expect(isBrowser()).toBe(true); }); }); describe('formatFileSize', () => { test('0 返回 0 Bytes', () => { expect(formatFileSize(0)).toBe('0 Bytes'); }); test('Bytes 档位', () => { expect(formatFileSize(512)).toBe('512 Bytes'); }); test('KB 档位', () => { expect(formatFileSize(1024)).toBe('1 KB'); expect(formatFileSize(1536)).toBe('1.5 KB'); }); test('MB 档位', () => { expect(formatFileSize(1048576)).toBe('1 MB'); }); test('GB 档位', () => { expect(formatFileSize(1073741824)).toBe('1 GB'); }); test('小数位数控制', () => { expect(formatFileSize(1536, 0)).toBe('2 KB'); expect(formatFileSize(1536, 3)).toBe('1.5 KB'); }); test('负 decimals 当作 0', () => { expect(formatFileSize(1536, -1)).toBe('2 KB'); }); }); describe('sleep', () => { test('返回 Promise', () => { expect(sleep(10)).toBeInstanceOf(Promise); }); test('延迟后 resolve', (done) => { const start = Date.now(); sleep(50).then(() => { const elapsed = Date.now() - start; expect(elapsed).toBeGreaterThanOrEqual(40); done(); }); }); test('async/await 用法', async () => { const start = Date.now(); await sleep(30); expect(Date.now() - start).toBeGreaterThanOrEqual(20); }); });