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:
+112
-4
@@ -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 面板文案跟随实例 locale(en-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 面板文案跟随实例 locale(zh-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 面板文案跟随实例 locale(en-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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user