feat: v0.2.1 — reference links, context menu, RTL, ja/ko, regex search, hooks, copy API
## Added - Reference link/image resolution: [text][ref] + ![alt][ref] with [ref]: url definitions - Right-click context menu: undo/redo/cut/copy/paste/selectAll + custom items - RTL CSS layout support for Arabic, Hebrew, Persian etc. - Japanese (ja) and Korean (ko) locales with 60+ keys each - Divider position localStorage persistence - Regex search toggle in search/replace panel - Export HTML with embedded CSS styles - beforeChange / afterChange lifecycle hooks (instance + global) - copyAsMarkdown() / copyAsHTML() clipboard APIs - CHANGELOG.md, CONTRIBUTING.md, CI workflow (.github/workflows/ci.yml) - 2 new test suites: index.test.ts, styles.test.ts (684 total tests, +74) ## Changed - autoSave plugin: closure-based state per instance instead of this context - Plugin install() now receives options as second argument - RTL locale detection: now uses language prefix (ar-SA → RTL) - Rollup dev mode: only builds UMD format - prepublishOnly now includes typecheck + test - Version bumped to 0.2.1 ## Fixed - [text][ref] now correctly renders as link (was raw text) - ![alt][ref] no longer produces empty src - autoSave plugin state isolation across multiple editor instances - Footnote definitions no longer consumed by refDef handler
This commit is contained in:
+217
-1
@@ -536,7 +536,7 @@ describe('MarkdownEditor - 构造边界', () => {
|
||||
destroy: jest.fn(),
|
||||
};
|
||||
const ed = new MarkdownEditor(document.createElement('div'), { plugins: [plugin] });
|
||||
expect(plugin.install).toHaveBeenCalledWith(ed);
|
||||
expect(plugin.install).toHaveBeenCalledWith(ed, {});
|
||||
ed.destroy();
|
||||
expect(plugin.destroy).toHaveBeenCalledWith(ed);
|
||||
});
|
||||
@@ -1731,3 +1731,219 @@ describe('MarkdownEditor - v0.2.0 getStatus plugins', () => {
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
// ============ v0.2.1 覆盖率补齐测试 ============
|
||||
|
||||
describe('MarkdownEditor - v0.2.1 工具栏键盘导航', () => {
|
||||
test('ArrowRight 聚焦下一个按钮', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { toolbar: ['bold', 'italic', 'code'] });
|
||||
const btns = ed.toolbarEl.querySelectorAll('.me-btn');
|
||||
const first = btns[0] as HTMLButtonElement;
|
||||
first.focus();
|
||||
first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
||||
// 焦点应移动
|
||||
expect(document.activeElement).not.toBe(first);
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('ArrowLeft 聚焦上一个按钮', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { toolbar: ['bold', 'italic'] });
|
||||
const btns = ed.toolbarEl.querySelectorAll('.me-btn');
|
||||
(btns[1] as HTMLButtonElement).focus();
|
||||
(btns[1] as HTMLButtonElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
|
||||
expect(document.activeElement).toBe(btns[0]);
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('Home 聚焦第一个按钮', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { toolbar: ['bold', 'italic', 'code'] });
|
||||
const btns = ed.toolbarEl.querySelectorAll('.me-btn');
|
||||
(btns[2] as HTMLButtonElement).focus();
|
||||
(btns[2] as HTMLButtonElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
|
||||
expect(document.activeElement).toBe(btns[0]);
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('End 聚焦最后一个按钮', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { toolbar: ['bold', 'italic', 'code'] });
|
||||
const btns = ed.toolbarEl.querySelectorAll('.me-btn');
|
||||
(btns[0] as HTMLButtonElement).focus();
|
||||
(btns[0] as HTMLButtonElement).dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
|
||||
expect(document.activeElement).toBe(btns[btns.length - 1]);
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarkdownEditor - v0.2.1 ARIA live', () => {
|
||||
test('_initAriaLive 创建 sr-only 元素', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, {});
|
||||
const live = ed.el.querySelector('.me-sr-only');
|
||||
expect(live).not.toBeNull();
|
||||
expect(live!.getAttribute('aria-live')).toBe('polite');
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('_announce 设置消息', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, {});
|
||||
ed._announce('test message');
|
||||
const live = ed.el.querySelector('.me-sr-only') as HTMLElement;
|
||||
expect(live).not.toBeNull();
|
||||
// jsdom 中 requestAnimationFrame 可能不被支持,但不应抛错
|
||||
expect(() => ed._announce('hello')).not.toThrow();
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarkdownEditor - v0.2.1 当前行高亮', () => {
|
||||
test('_updateCurrentLine 高亮当前行', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'line1\nline2\nline3', lineNumbers: true });
|
||||
ed.textarea.setSelectionRange(7, 7); // 第二行
|
||||
ed._updateCurrentLine();
|
||||
const active = ed.gutter.querySelector('.me-gutter-active');
|
||||
expect(active).not.toBeNull();
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('_updateCurrentLine 无 gutter 时不抛错', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'test', lineNumbers: false });
|
||||
expect(() => ed._updateCurrentLine()).not.toThrow();
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarkdownEditor - v0.2.1 右键上下文菜单', () => {
|
||||
test('contextmenu 事件显示菜单', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'hello' });
|
||||
ed.el.dispatchEvent(new MouseEvent('contextmenu', { clientX: 100, clientY: 100, bubbles: true }));
|
||||
const menu = document.querySelector('.me-context-menu');
|
||||
expect(menu).not.toBeNull();
|
||||
// 清理
|
||||
if (menu) menu.remove();
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('上下文菜单点击外部关闭', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'hello' });
|
||||
ed.el.dispatchEvent(new MouseEvent('contextmenu', { clientX: 100, clientY: 100, bubbles: true }));
|
||||
expect(document.querySelector('.me-context-menu')).not.toBeNull();
|
||||
document.body.click();
|
||||
// once listener removes it
|
||||
expect(document.querySelector('.me-context-menu')).toBeNull();
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('registerContextMenu 添加自定义菜单项', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, {});
|
||||
ed.registerContextMenu([{ label: 'Custom Action', onClick: jest.fn() }]);
|
||||
expect(ed._contextMenuItems.length).toBe(1);
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarkdownEditor - v0.2.1 copy API', () => {
|
||||
test('copyAsMarkdown 不抛错', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: '# hello' });
|
||||
expect(() => ed.copyAsMarkdown()).not.toThrow();
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('copyAsHTML 不抛错', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: '# hello' });
|
||||
expect(() => ed.copyAsHTML()).not.toThrow();
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarkdownEditor - v0.2.1 beforeChange/afterChange 钩子', () => {
|
||||
test('setValue 触发 beforeChange', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'old' });
|
||||
const handler = jest.fn();
|
||||
ed.on('beforeChange', handler);
|
||||
ed.setValue('new');
|
||||
expect(handler).toHaveBeenCalledWith('old', 'new', ed);
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('setValue 触发 afterChange', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { value: 'old' });
|
||||
const handler = jest.fn();
|
||||
ed.on('afterChange', handler);
|
||||
ed.setValue('new');
|
||||
expect(handler).toHaveBeenCalledWith('new', ed);
|
||||
ed.destroy();
|
||||
});
|
||||
|
||||
test('全局 beforeChange/afterChange 钩子', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const beforeFn = jest.fn();
|
||||
const afterFn = jest.fn();
|
||||
MarkdownEditor.on('beforeChange', beforeFn);
|
||||
MarkdownEditor.on('afterChange', afterFn);
|
||||
const ed = new MarkdownEditor(c, { value: 'test' });
|
||||
ed.setValue('updated');
|
||||
expect(beforeFn).toHaveBeenCalled();
|
||||
expect(afterFn).toHaveBeenCalled();
|
||||
MarkdownEditor.off('beforeChange', beforeFn);
|
||||
MarkdownEditor.off('afterChange', afterFn);
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarkdownEditor - v0.2.1 分隔条持久化', () => {
|
||||
test('_saveDividerPosition / _restoreDividerPosition 不抛错', () => {
|
||||
document.body.innerHTML = '';
|
||||
const c = document.createElement('div');
|
||||
document.body.appendChild(c);
|
||||
const ed = new MarkdownEditor(c, { mode: 'split' });
|
||||
expect(() => ed._saveDividerPosition()).not.toThrow();
|
||||
expect(() => ed._restoreDividerPosition()).not.toThrow();
|
||||
ed.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user