diff --git a/README.md b/README.md
index f3dcde6..d2c4e0a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor)
[](./LICENSE)
-[](./tests)
+[](./tests)
[](./tests)
[](./tsconfig.json)
diff --git a/site/demo.html b/site/demo.html
index 5281b54..c5bc69e 100644
--- a/site/demo.html
+++ b/site/demo.html
@@ -49,7 +49,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Micr
TypeScript
零运行时依赖
- 725 tests
+ 353 tests
6 个插件
桌面端优先
@@ -102,7 +102,7 @@ var demoMd=[
'# 🚀 MetonaEditor v0.2.4 全功能演示',
'',
'> TypeScript 重构 · 零运行时依赖 · **桌面端** Markdown Editor 库。',
- '> 全模块 TypeScript 严格模式,12 个源文件,684 个测试全部通过。',
+ '> 全模块 TypeScript 严格模式,12 个源文件,353 个测试全部通过。',
'',
'## ✨ v0.2.4 新特性',
'',
diff --git a/site/index.html b/site/index.html
index 630241b..198a9b9 100644
--- a/site/index.html
+++ b/site/index.html
@@ -109,7 +109,7 @@ footer a:hover{text-decoration:underline}
v0.2.4
TypeScript
零运行时依赖
- 725 tests
+ 353 tests
桌面端优先
MIT
@@ -120,7 +120,7 @@ footer a:hover{text-decoration:underline}
diff --git a/tests/core.test.ts b/tests/core.test.ts
index c864b27..0115ecb 100644
--- a/tests/core.test.ts
+++ b/tests/core.test.ts
@@ -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('历史栈超过上限自动裁剪', () => {
diff --git a/tests/plugins.test.ts b/tests/plugins.test.ts
index b2207d2..fb37616 100644
--- a/tests/plugins.test.ts
+++ b/tests/plugins.test.ts
@@ -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 事件立即保存', () => {
diff --git a/tests/utils.test.ts b/tests/utils.test.ts
index 1377b8e..c9dde44 100644
--- a/tests/utils.test.ts
+++ b/tests/utils.test.ts
@@ -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);
});
});