feat: v0.4.2 — 审查清偿:8 项修复 + setOutline API + 871 测试

修复:插件面板实例级 i18n / undo-redo 滚动保持 / shortcutHelp 卸载残留 / CSS.escape 环境崩溃 / 大纲光标按源码行号定位 / setLocale 即时刷新 / 浮动工具栏 CJK 宽度 / ko 语言包括号

新增:setOutline(isOutline) 运行时大纲 API、getRenderCacheSize 观测接口

测试:841 → 871(补 7 个事件断言、5 个配置项、修 3 处恒真断言)

工程:CI lint 阻断、build.sh npm ci、rollup 死代码清理、覆盖率阈值门禁
This commit is contained in:
2026-08-19 22:30:52 +08:00
parent 5966ac7500
commit 5f4915222e
22 changed files with 704 additions and 107 deletions
+112 -4
View File
@@ -570,7 +570,9 @@ describe('searchReplace 插件 - 完整流程', () => {
input.value = 'foo';
input.dispatchEvent(new Event('input', { bubbles: true }));
ed.el.querySelector('.me-search-next').click();
expect(ed.textarea.selectionStart).toBeGreaterThanOrEqual(0);
// 首个匹配位于 0,且选中内容即搜索词
expect(ed.textarea.selectionStart).toBe(0);
expect(ed.getSelectedText()).toBe('foo');
});
test('查找上一个高亮匹配', () => {
@@ -580,7 +582,9 @@ describe('searchReplace 插件 - 完整流程', () => {
input.dispatchEvent(new Event('input', { bubbles: true }));
ed.el.querySelector('.me-search-next').click();
ed.el.querySelector('.me-search-prev').click();
expect(ed.textarea.selectionStart).toBeGreaterThanOrEqual(0);
// next 选中第一处(0),prev 回绕到最后一处('foo bar foo baz foo' 中 16
expect(ed.textarea.selectionStart).toBe(16);
expect(ed.getSelectedText()).toBe('foo');
});
test('空查询不抛错', () => {
@@ -600,7 +604,8 @@ describe('searchReplace 插件 - 完整流程', () => {
input.value = 'foo';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(ed.textarea.selectionStart).toBeGreaterThanOrEqual(0);
expect(ed.textarea.selectionStart).toBe(0);
expect(ed.getSelectedText()).toBe('foo');
});
test('Shift+Enter 在 findInput 上查找上一个', () => {
@@ -609,7 +614,9 @@ describe('searchReplace 插件 - 完整流程', () => {
input.value = 'foo';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true, bubbles: true }));
expect(ed.textarea.selectionStart).toBeGreaterThanOrEqual(0);
// 初始光标在 0,prev 无更早匹配时回绕到最后一处(16)
expect(ed.textarea.selectionStart).toBe(16);
expect(ed.getSelectedText()).toBe('foo');
});
test('替换行 display 切换(none ↔ flex', () => {
@@ -1301,3 +1308,104 @@ describe('v0.2.5 imagePaste 尺寸限制', () => {
ed.destroy();
});
});
// ============ v0.4.2 fileSystem 事件补齐 ============
describe('fileSystem 插件 - fileOpened / fileSaved 事件', () => {
afterEach(() => {
delete (window as any).showOpenFilePicker;
delete (window as any).showSaveFilePicker;
});
test('openFile 触发 fileOpened 事件并载入内容', async () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: '' });
const onOpened = jest.fn();
ed.on('fileOpened', onOpened);
const fakeFile = { name: 'doc.md', text: async () => '# opened content' };
const handle = { getFile: async () => fakeFile };
(window as any).showOpenFilePicker = async () => [handle];
ed.use('fileSystem');
const result = await (ed as any).openFile();
expect(result).toEqual({ name: 'doc.md', content: '# opened content', handle });
expect(ed.getValue()).toBe('# opened content');
expect(onOpened).toHaveBeenCalledWith({ name: 'doc.md', handle }, ed);
ed.destroy();
});
test('saveFile 触发 fileSaved 事件', async () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: '# save me' });
const onSaved = jest.fn();
ed.on('fileSaved', onSaved);
const writable = { write: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined) };
const handle = { createWritable: async () => writable };
(window as any).showOpenFilePicker = async () => [];
(window as any).showSaveFilePicker = async () => handle;
ed.use('fileSystem');
const ok = await (ed as any).saveFile();
expect(ok).toBe(true);
expect(writable.write).toHaveBeenCalledWith('# save me');
expect(onSaved).toHaveBeenCalledWith({ handle }, ed);
ed.destroy();
});
});
// ============ v0.4.2 插件面板实例级 i18n ============
describe('v0.4.2 插件面板实例级 i18n', () => {
let container: HTMLElement;
beforeEach(() => {
document.body.innerHTML = '';
container = document.createElement('div');
document.body.appendChild(container);
});
test('searchReplace 面板文案跟随实例 localeen-US', () => {
const ed = new MarkdownEditor(container, { value: 'foo', locale: 'en-US' });
ed.use('searchReplace');
ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', ctrlKey: true, bubbles: true, cancelable: true }));
const input = ed.el.querySelector('.me-search-find') as HTMLInputElement;
expect(input.placeholder).toBe('Find');
const replaceBtn = ed.el.querySelector('.me-search-replace-all');
expect(replaceBtn.textContent).toBe('Replace All');
ed.el.querySelector('.me-search-close').click();
ed.destroy();
});
test('searchReplace 面板文案跟随实例 localezh-CN', () => {
const ed = new MarkdownEditor(container, { value: 'foo', locale: 'zh-CN' });
ed.use('searchReplace');
ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', ctrlKey: true, bubbles: true, cancelable: true }));
const input = ed.el.querySelector('.me-search-find') as HTMLInputElement;
expect(input.placeholder).toBe('查找内容');
ed.el.querySelector('.me-search-close').click();
ed.destroy();
});
test('shortcutHelp 面板文案跟随实例 localeen-US', () => {
const ed = new MarkdownEditor(container, { value: 'foo', locale: 'en-US' });
ed.use('shortcutHelp');
ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: '?', bubbles: true }));
const overlay = document.querySelector('.me-shortcut-overlay') as HTMLElement;
expect(overlay).not.toBeNull();
expect(overlay.querySelector('h3')!.textContent).toContain('Shortcuts');
expect(overlay.textContent).toContain('Bold');
ed.destroy();
});
test('unuse shortcutHelp 移除已打开的面板(不残留 DOM)', () => {
const ed = new MarkdownEditor(container, { value: 'foo' });
ed.use('shortcutHelp');
ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: '?', bubbles: true }));
expect(document.querySelector('.me-shortcut-overlay')).not.toBeNull();
ed.unuse('shortcutHelp');
expect(document.querySelector('.me-shortcut-overlay')).toBeNull();
ed.destroy();
});
});