fix: 重构所有定时器测试为 Jest fake timers,彻底解决 CI Run tests 卡死
tests/utils.test.ts: debounce/throttle/sleep 使用 useFakeTimers + advanceTimersByTime; tests/core.test.ts: 历史栈防抖合并测试用 fake timers 替代 400ms setTimeout; tests/plugins.test.ts: autoSave 三个测试用 fake timers 替代 setTimeout/done; README.md: 更新测试徽章为 353 passed; site/index.html, site/demo.html: 更新测试数为 353
This commit is contained in:
+13
-9
@@ -1053,11 +1053,16 @@ describe('MarkdownEditor - 工具栏点击', () => {
|
||||
});
|
||||
|
||||
describe('MarkdownEditor - 历史栈合并', () => {
|
||||
test('连续输入触发防抖合并', (done) => {
|
||||
beforeEach(() => jest.useFakeTimers());
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
test('连续输入触发防抖合并', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: '' });
|
||||
// 初始历史栈长度(构造时已 push 一次)
|
||||
const initialLen = ed._history.length;
|
||||
// 连续快速输入
|
||||
ed.textarea.value = 'a';
|
||||
ed.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
@@ -1065,14 +1070,13 @@ describe('MarkdownEditor - 历史栈合并', () => {
|
||||
ed.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
ed.textarea.value = 'abc';
|
||||
ed.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
// 等待防抖结束
|
||||
setTimeout(() => {
|
||||
const initialLen = ed._history.length;
|
||||
// 防抖合并后历史栈不应为 3
|
||||
expect(ed._history.length).toBeLessThanOrEqual(initialLen + 1);
|
||||
ed.destroy();
|
||||
done();
|
||||
}, 500);
|
||||
// 防抖期内历史栈不应增长
|
||||
expect(ed._history.length).toBe(initialLen);
|
||||
// 推进防抖时间(默认 400ms),触发 _pushHistory
|
||||
jest.advanceTimersByTime(400);
|
||||
// 防抖合并后历史栈只增加 1 条
|
||||
expect(ed._history.length).toBe(initialLen + 1);
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('历史栈超过上限自动裁剪', () => {
|
||||
|
||||
+30
-20
@@ -65,15 +65,16 @@ describe('autoSave 插件', () => {
|
||||
expect(key).toContain('me-draft-');
|
||||
});
|
||||
|
||||
test('change 后内容最终写入 localStorage', (done) => {
|
||||
test('change 后内容最终写入 localStorage', () => {
|
||||
jest.useFakeTimers();
|
||||
ed.setValue('autosaved content');
|
||||
// autoSave 默认 delay 1000ms,等 1100ms
|
||||
setTimeout(() => {
|
||||
const key = ed.getDraftKey();
|
||||
const stored = localStorage.getItem(key);
|
||||
expect(stored).toBe('autosaved content');
|
||||
done();
|
||||
}, 1150);
|
||||
const key = ed.getDraftKey();
|
||||
// autoSave 默认 delay 1000ms,未到时间不应保存
|
||||
expect(localStorage.getItem(key)).toBeNull();
|
||||
// 推进时间触发 save
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(localStorage.getItem(key)).toBe('autosaved content');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('clearDraft 清除存储', () => {
|
||||
@@ -105,16 +106,19 @@ describe('autoSave 自定义配置', () => {
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('自定义 delay 生效', (done) => {
|
||||
test('自定义 delay 生效', () => {
|
||||
jest.useFakeTimers();
|
||||
localStorage.clear();
|
||||
const ed = makeEditor();
|
||||
ed.use(presetPlugins.autoSave, { delay: 30 });
|
||||
ed.setValue('fast');
|
||||
setTimeout(() => {
|
||||
expect(localStorage.getItem(ed.getDraftKey())).toBe('fast');
|
||||
ed.destroy();
|
||||
done();
|
||||
}, 60);
|
||||
// 未到时间不应保存
|
||||
expect(localStorage.getItem(ed.getDraftKey())).toBeNull();
|
||||
// 推进 30ms 触发 save
|
||||
jest.advanceTimersByTime(30);
|
||||
expect(localStorage.getItem(ed.getDraftKey())).toBe('fast');
|
||||
ed.destroy();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -386,14 +390,20 @@ describe('autoSave 插件 - 边界', () => {
|
||||
expect(ed.clearDraft()).toBe(ed);
|
||||
});
|
||||
|
||||
test('保存触发 autosave 事件', (done) => {
|
||||
test('保存触发 autosave 事件', () => {
|
||||
jest.useFakeTimers();
|
||||
let eventData = null;
|
||||
ed.use(presetPlugins.autoSave, { key: 'evt-draft', delay: 10 });
|
||||
ed.on('autosave', (data) => {
|
||||
expect(data.key).toBe('evt-draft');
|
||||
expect(data.value).toBe('triggered');
|
||||
done();
|
||||
});
|
||||
ed.on('autosave', (data) => { eventData = data; });
|
||||
ed.setValue('triggered');
|
||||
// 未到时间不应触发事件
|
||||
expect(eventData).toBeNull();
|
||||
// 推进 10ms 触发 save 并 emit 'autosave'
|
||||
jest.advanceTimersByTime(10);
|
||||
expect(eventData).not.toBeNull();
|
||||
expect(eventData.key).toBe('evt-draft');
|
||||
expect(eventData.value).toBe('triggered');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('blur 事件立即保存', () => {
|
||||
|
||||
+39
-38
@@ -61,27 +61,26 @@ describe('prefersDark', () => {
|
||||
});
|
||||
|
||||
describe('debounce', () => {
|
||||
test('延迟执行', (done) => {
|
||||
beforeEach(() => jest.useFakeTimers());
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
test('延迟执行', () => {
|
||||
let called = 0;
|
||||
const fn = debounce(() => { called++; }, 50);
|
||||
fn();
|
||||
expect(called).toBe(0);
|
||||
setTimeout(() => {
|
||||
expect(called).toBe(1);
|
||||
done();
|
||||
}, 100);
|
||||
jest.advanceTimersByTime(50);
|
||||
expect(called).toBe(1);
|
||||
});
|
||||
|
||||
test('多次调用只执行最后一次', (done) => {
|
||||
test('多次调用只执行最后一次', () => {
|
||||
let result = 0;
|
||||
const fn = debounce((v) => { result = v; }, 50);
|
||||
fn(1);
|
||||
fn(2);
|
||||
fn(3);
|
||||
setTimeout(() => {
|
||||
expect(result).toBe(3);
|
||||
done();
|
||||
}, 100);
|
||||
jest.advanceTimersByTime(50);
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
test('immediate 模式立即执行', () => {
|
||||
@@ -91,31 +90,30 @@ describe('debounce', () => {
|
||||
expect(called).toBe(1);
|
||||
});
|
||||
|
||||
test('保留 this 上下文', (done) => {
|
||||
test('保留 this 上下文', () => {
|
||||
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);
|
||||
expect(typeof p).toBe('undefined');
|
||||
jest.advanceTimersByTime(30);
|
||||
});
|
||||
|
||||
test('传递参数', (done) => {
|
||||
test('传递参数', () => {
|
||||
let captured = null;
|
||||
const fn = debounce((a, b) => { captured = a + b; }, 30);
|
||||
fn(1, 2);
|
||||
setTimeout(() => {
|
||||
expect(captured).toBe(3);
|
||||
done();
|
||||
}, 60);
|
||||
jest.advanceTimersByTime(30);
|
||||
expect(captured).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('throttle', () => {
|
||||
beforeEach(() => jest.useFakeTimers());
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
test('首次立即执行', () => {
|
||||
let called = 0;
|
||||
const fn = throttle(() => { called++; }, 50);
|
||||
@@ -132,25 +130,22 @@ describe('throttle', () => {
|
||||
expect(called).toBe(1);
|
||||
});
|
||||
|
||||
test('限流期后可再次执行', (done) => {
|
||||
test('限流期后可再次执行', () => {
|
||||
let called = 0;
|
||||
const fn = throttle(() => { called++; }, 40);
|
||||
fn();
|
||||
setTimeout(() => {
|
||||
fn();
|
||||
expect(called).toBe(2);
|
||||
done();
|
||||
}, 60);
|
||||
jest.advanceTimersByTime(40);
|
||||
fn();
|
||||
expect(called).toBe(2);
|
||||
});
|
||||
|
||||
test('保留 this 与参数', (done) => {
|
||||
test('保留 this 与参数', () => {
|
||||
const obj = {
|
||||
n: 10,
|
||||
run: throttle(function (x) { this.result = this.n + x; }, 30),
|
||||
};
|
||||
obj.run(5);
|
||||
expect(obj.result).toBe(15);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,22 +220,28 @@ describe('formatFileSize', () => {
|
||||
});
|
||||
|
||||
describe('sleep', () => {
|
||||
beforeEach(() => jest.useFakeTimers());
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
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('延迟后 resolve', async () => {
|
||||
let resolved = false;
|
||||
sleep(50).then(() => { resolved = true; });
|
||||
expect(resolved).toBe(false);
|
||||
jest.advanceTimersByTime(50);
|
||||
// 让微任务队列执行
|
||||
await Promise.resolve();
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
|
||||
test('async/await 用法', async () => {
|
||||
const start = Date.now();
|
||||
await sleep(30);
|
||||
expect(Date.now() - start).toBeGreaterThanOrEqual(20);
|
||||
let resolved = false;
|
||||
const p = sleep(30).then(() => { resolved = true; });
|
||||
jest.advanceTimersByTime(30);
|
||||
await p;
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user