Files
MetonaEditor/tests/utils.test.js
T

247 lines
5.6 KiB
JavaScript

/**
* 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('<a>&b</a>');
expect(out).toContain('&lt;');
expect(out).toContain('&gt;');
expect(out).toContain('&amp;');
expect(out).not.toContain('<a>');
});
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);
});
});