From cf0bc450554350748b20bc15a292330836f9c909 Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Tue, 28 Jul 2026 21:18:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=87=8D=E6=9E=84=E6=89=80=E6=9C=89?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E5=99=A8=E6=B5=8B=E8=AF=95=E4=B8=BA=20Jest?= =?UTF-8?q?=20fake=20timers=EF=BC=8C=E5=BD=BB=E5=BA=95=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=20CI=20Run=20tests=20=E5=8D=A1=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 2 +- site/demo.html | 4 +-- site/index.html | 4 +-- tests/core.test.ts | 22 ++++++++----- tests/plugins.test.ts | 50 +++++++++++++++++----------- tests/utils.test.ts | 77 ++++++++++++++++++++++--------------------- 6 files changed, 87 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index f3dcde6..d2c4e0a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![version](https://img.shields.io/badge/version-0.2.4-blue)](https://git.metona.cn/MetonaTeam/-/packages/npm/@metona-team%2Fmetona-editor) [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE) -[![tests](https://img.shields.io/badge/tests-412%20passed-brightgreen)](./tests) +[![tests](https://img.shields.io/badge/tests-353%20passed-brightgreen)](./tests) [![coverage](https://img.shields.io/badge/coverage-parser%2099%25-brightgreen)](./tests) [![types](https://img.shields.io/badge/types-TypeScript%20strict-blue)](./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}
0
运行时依赖
99%
解析器行覆盖
-
725
单元测试
+
353
单元测试
~30KB
gzip 体积
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); }); });