Bug Fixes: - fix(core): insert() replace param was non-functional due to identical ternary branches - fix(types): correct project URL typo (MetonaToast → MetonaEditor) Improvements: - feat(core): scope CSS theme variables to .me-wrapper per-instance, preventing global style pollution - feat(themes): add optional target parameter to setThemeVariables() for element-scoped theming - perf(parser): improve bold/strikethrough regex to support inline delimiter chars (e.g. **a*b**, ~~a~b~~) - fix(parser): prevent *** cross-tag nesting by requiring first content char ≠ delimiter - docs(animations): clarify module purpose as future-use animation metadata registry Tests: - test(core): add 2 cases for insert() replace:true / replace:false behavior - All 409 tests passing (+2 new) Chores: - bump version 0.1.0 → 0.1.1 across all source files, package.json, README, and demo
1244 lines
37 KiB
JavaScript
1244 lines
37 KiB
JavaScript
/**
|
||
* core.js 单元测试
|
||
* 覆盖 MarkdownEditor 类的构造、API、命令、历史栈、模式、事件、销毁
|
||
*/
|
||
|
||
import { MarkdownEditor } from '../src/core.js';
|
||
|
||
// jsdom 不提供 PointerEvent,用 MouseEvent 作为基类 polyfill
|
||
if (typeof window !== 'undefined' && typeof window.PointerEvent === 'undefined') {
|
||
window.PointerEvent = class PointerEvent extends MouseEvent {
|
||
constructor(type, init = {}) {
|
||
super(type, init);
|
||
this.pointerId = init.pointerId || 0;
|
||
this.pointerType = init.pointerType || 'mouse';
|
||
this.isPrimary = init.isPrimary !== false;
|
||
}
|
||
};
|
||
}
|
||
|
||
describe('MarkdownEditor - 构造', () => {
|
||
let container;
|
||
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
container = document.createElement('div');
|
||
container.id = 'host';
|
||
document.body.appendChild(container);
|
||
});
|
||
|
||
test('通过元素构造', () => {
|
||
const ed = new MarkdownEditor(container, { value: '# hi' });
|
||
expect(ed.el).toBeInstanceOf(HTMLElement);
|
||
expect(ed.container).toBe(container);
|
||
expect(ed.getValue()).toBe('# hi');
|
||
});
|
||
|
||
test('通过选择器构造', () => {
|
||
const ed = new MarkdownEditor('#host', { value: 'text' });
|
||
expect(ed.el).toBeDefined();
|
||
expect(ed.getValue()).toBe('text');
|
||
});
|
||
|
||
test('容器不存在时标记为已销毁且不抛错', () => {
|
||
const ed = new MarkdownEditor('#not-exist', {});
|
||
expect(ed.isDestroyed()).toBe(true);
|
||
});
|
||
|
||
test('生成唯一 id', () => {
|
||
const a = new MarkdownEditor(container, {});
|
||
const b = new MarkdownEditor(document.createElement('div'), {});
|
||
expect(a.id).toBeTruthy();
|
||
expect(b.id).toBeTruthy();
|
||
expect(a.id).not.toBe(b.id);
|
||
});
|
||
|
||
test('DOM 结构完整', () => {
|
||
const ed = new MarkdownEditor(container, {});
|
||
expect(ed.el.className).toContain('me-wrapper');
|
||
expect(ed.toolbarEl).toBeDefined();
|
||
expect(ed.bodyEl).toBeDefined();
|
||
expect(ed.textarea).toBeInstanceOf(HTMLTextAreaElement);
|
||
expect(ed.previewEl).toBeDefined();
|
||
});
|
||
|
||
test('工具栏默认渲染按钮', () => {
|
||
const ed = new MarkdownEditor(container, {});
|
||
const btns = ed.toolbarEl.querySelectorAll('.me-btn');
|
||
expect(btns.length).toBeGreaterThan(10);
|
||
});
|
||
|
||
test('toolbar: false 隐藏工具栏按钮', () => {
|
||
const ed = new MarkdownEditor(container, { toolbar: false });
|
||
const btns = ed.toolbarEl.querySelectorAll('.me-btn');
|
||
expect(btns.length).toBe(0);
|
||
});
|
||
|
||
test('初始 value 写入 textarea', () => {
|
||
const ed = new MarkdownEditor(container, { value: '# Title' });
|
||
expect(ed.textarea.value).toBe('# Title');
|
||
});
|
||
|
||
test('wordCount 关闭时不创建状态栏', () => {
|
||
const ed = new MarkdownEditor(container, { wordCount: false });
|
||
expect(ed.statusEl).toBeNull();
|
||
});
|
||
|
||
test('wordCount 开启时创建状态栏', () => {
|
||
const ed = new MarkdownEditor(container, { wordCount: true });
|
||
expect(ed.statusEl).not.toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 内容 API', () => {
|
||
let ed;
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
ed = new MarkdownEditor(document.createElement('div'), { value: '' });
|
||
});
|
||
afterEach(() => ed.destroy());
|
||
|
||
test('getValue / setValue', () => {
|
||
ed.setValue('# hello');
|
||
expect(ed.getValue()).toBe('# hello');
|
||
expect(ed.textarea.value).toBe('# hello');
|
||
});
|
||
|
||
test('setValue silent 不触发 change', () => {
|
||
let called = 0;
|
||
ed.on('change', () => called++);
|
||
ed.setValue('x', { silent: true });
|
||
expect(called).toBe(0);
|
||
});
|
||
|
||
test('setValue 默认触发 change', () => {
|
||
let called = 0;
|
||
ed.on('change', () => called++);
|
||
ed.setValue('x');
|
||
expect(called).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('getHTML 返回渲染后 HTML', () => {
|
||
ed.setValue('# Title');
|
||
const html = ed.getHTML();
|
||
expect(html).toContain('<h1');
|
||
expect(html).toContain('Title');
|
||
});
|
||
|
||
test('insert 在光标处插入', () => {
|
||
ed.setValue('abc');
|
||
ed.textarea.selectionStart = ed.textarea.selectionEnd = 1;
|
||
ed.insert('X');
|
||
expect(ed.getValue()).toBe('aXbc');
|
||
});
|
||
|
||
test('insert replace:true 替换选区', () => {
|
||
ed.setValue('hello world');
|
||
ed.textarea.selectionStart = 0;
|
||
ed.textarea.selectionEnd = 5;
|
||
ed.insert('hi', { replace: true });
|
||
expect(ed.getValue()).toBe('hi world');
|
||
});
|
||
|
||
test('insert replace:false 在光标处插入(保留选区后的内容)', () => {
|
||
ed.setValue('abcdef');
|
||
ed.textarea.selectionStart = 3;
|
||
ed.textarea.selectionEnd = 3;
|
||
ed.insert('X');
|
||
expect(ed.getValue()).toBe('abcXdef');
|
||
});
|
||
|
||
test('wrap 包裹选区', () => {
|
||
ed.setValue('hello');
|
||
ed.textarea.selectionStart = 0;
|
||
ed.textarea.selectionEnd = 5;
|
||
ed.wrap('**', '**');
|
||
expect(ed.getValue()).toBe('**hello**');
|
||
});
|
||
|
||
test('focus / blur 不抛错', () => {
|
||
expect(() => ed.focus()).not.toThrow();
|
||
expect(() => ed.blur()).not.toThrow();
|
||
});
|
||
|
||
test('enable / disable', () => {
|
||
ed.disable();
|
||
expect(ed.isDisabled()).toBe(true);
|
||
expect(ed.el.className).toContain('me-disabled');
|
||
ed.enable();
|
||
expect(ed.isDisabled()).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - exec 命令', () => {
|
||
let ed;
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
ed = new MarkdownEditor(document.createElement('div'), { value: 'hello world' });
|
||
});
|
||
afterEach(() => ed.destroy());
|
||
|
||
function select(start, end) {
|
||
ed.textarea.selectionStart = start;
|
||
ed.textarea.selectionEnd = end === undefined ? start : end;
|
||
}
|
||
|
||
test('bold 包裹选区', () => {
|
||
select(0, 5);
|
||
ed.exec('bold');
|
||
expect(ed.getValue()).toBe('**hello** world');
|
||
});
|
||
|
||
test('italic 包裹选区', () => {
|
||
select(0, 5);
|
||
ed.exec('italic');
|
||
expect(ed.getValue()).toBe('*hello* world');
|
||
});
|
||
|
||
test('strikethrough 包裹选区', () => {
|
||
select(0, 5);
|
||
ed.exec('strikethrough');
|
||
expect(ed.getValue()).toBe('~~hello~~ world');
|
||
});
|
||
|
||
test('code 包裹选区', () => {
|
||
select(0, 5);
|
||
ed.exec('code');
|
||
expect(ed.getValue()).toBe('`hello` world');
|
||
});
|
||
|
||
test('h1 切换行首前缀', () => {
|
||
select(0, 0);
|
||
ed.exec('h1');
|
||
expect(ed.getValue()).toBe('# hello world');
|
||
});
|
||
|
||
test('quote 切换行首前缀', () => {
|
||
select(0, 0);
|
||
ed.exec('quote');
|
||
expect(ed.getValue()).toBe('> hello world');
|
||
});
|
||
|
||
test('ul 切换行首前缀', () => {
|
||
select(0, 0);
|
||
ed.exec('ul');
|
||
expect(ed.getValue()).toBe('- hello world');
|
||
});
|
||
|
||
test('ol 切换行首前缀', () => {
|
||
select(0, 0);
|
||
ed.exec('ol');
|
||
expect(ed.getValue()).toBe('1. hello world');
|
||
});
|
||
|
||
test('重复执行同一前缀命令会取消前缀', () => {
|
||
select(0, 0);
|
||
ed.exec('h1');
|
||
ed.exec('h1');
|
||
expect(ed.getValue()).toBe('hello world');
|
||
});
|
||
|
||
test('hr 插入分隔线', () => {
|
||
ed.setValue('a');
|
||
ed.textarea.selectionStart = ed.textarea.selectionEnd = 1;
|
||
ed.exec('hr');
|
||
expect(ed.getValue()).toContain('---');
|
||
});
|
||
|
||
test('link 插入链接', () => {
|
||
ed.setValue('text');
|
||
select(0, 4);
|
||
ed.exec('link');
|
||
expect(ed.getValue()).toMatch(/\[text\]\(https?:\/\//);
|
||
});
|
||
|
||
test('image 插入图片', () => {
|
||
ed.setValue('alt');
|
||
select(0, 3);
|
||
ed.exec('image');
|
||
expect(ed.getValue()).toMatch(/!\[alt\]\(https?:\/\//);
|
||
});
|
||
|
||
test('table 插入表格', () => {
|
||
ed.setValue('');
|
||
ed.exec('table');
|
||
const v = ed.getValue();
|
||
expect(v).toContain('|');
|
||
expect(v).toContain('---');
|
||
});
|
||
|
||
test('未知命令不抛错', () => {
|
||
expect(() => ed.exec('nonexistent')).not.toThrow();
|
||
});
|
||
|
||
test('exec 返回 this 支持链式', () => {
|
||
expect(ed.exec('bold')).toBe(ed);
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 历史栈', () => {
|
||
let ed;
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
ed = new MarkdownEditor(document.createElement('div'), { value: '' });
|
||
});
|
||
afterEach(() => ed.destroy());
|
||
|
||
test('初始可撤销为 false', () => {
|
||
expect(ed.canUndo()).toBe(false);
|
||
});
|
||
|
||
test('setValue 后可撤销', () => {
|
||
ed.setValue('a');
|
||
expect(ed.canUndo()).toBe(true);
|
||
});
|
||
|
||
test('undo 回退', () => {
|
||
ed.setValue('a');
|
||
ed.setValue('b');
|
||
ed.undo();
|
||
expect(ed.getValue()).toBe('a');
|
||
});
|
||
|
||
test('redo 前进', () => {
|
||
ed.setValue('a');
|
||
ed.setValue('b');
|
||
ed.undo();
|
||
ed.redo();
|
||
expect(ed.getValue()).toBe('b');
|
||
});
|
||
|
||
test('无历史时 undo/redo 无副作用', () => {
|
||
expect(() => ed.undo()).not.toThrow();
|
||
expect(() => ed.redo()).not.toThrow();
|
||
});
|
||
|
||
test('undo/redo 返回 this', () => {
|
||
ed.setValue('a');
|
||
expect(ed.undo()).toBe(ed);
|
||
expect(ed.redo()).toBe(ed);
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 模式与全屏', () => {
|
||
let ed;
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
ed = new MarkdownEditor(document.createElement('div'), { mode: 'split' });
|
||
});
|
||
afterEach(() => ed.destroy());
|
||
|
||
test('初始模式', () => {
|
||
expect(ed.getMode()).toBe('split');
|
||
});
|
||
|
||
test('setMode 切换', () => {
|
||
ed.setMode('edit');
|
||
expect(ed.getMode()).toBe('edit');
|
||
expect(ed.bodyEl.className).toContain('me-mode-edit');
|
||
});
|
||
|
||
test('setMode 切换到 preview', () => {
|
||
ed.setMode('preview');
|
||
expect(ed.getMode()).toBe('preview');
|
||
expect(ed.bodyEl.className).toContain('me-mode-preview');
|
||
});
|
||
|
||
test('非法模式被忽略', () => {
|
||
ed.setMode('invalid');
|
||
expect(ed.getMode()).toBe('split');
|
||
});
|
||
|
||
test('toggleFullscreen 切换全屏状态', () => {
|
||
expect(ed.isFullscreen()).toBe(false);
|
||
ed.toggleFullscreen();
|
||
expect(ed.isFullscreen()).toBe(true);
|
||
expect(ed.el.className).toContain('me-fullscreen');
|
||
ed.toggleFullscreen();
|
||
expect(ed.isFullscreen()).toBe(false);
|
||
});
|
||
|
||
test('exitFullscreen', () => {
|
||
ed.toggleFullscreen();
|
||
ed.exitFullscreen();
|
||
expect(ed.isFullscreen()).toBe(false);
|
||
});
|
||
|
||
test('setMode 触发 modeChange 事件', () => {
|
||
let mode = null;
|
||
ed.on('modeChange', (m) => { mode = m; });
|
||
ed.setMode('edit');
|
||
expect(mode).toBe('edit');
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 统计与状态', () => {
|
||
let ed;
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
ed = new MarkdownEditor(document.createElement('div'), { value: '' });
|
||
});
|
||
afterEach(() => ed.destroy());
|
||
|
||
test('getStats 返回统计字段', () => {
|
||
ed.setValue('hello 世界');
|
||
const stats = ed.getStats();
|
||
expect(stats).toHaveProperty('characters');
|
||
expect(stats).toHaveProperty('words');
|
||
expect(stats).toHaveProperty('chineseChars');
|
||
expect(stats).toHaveProperty('englishWords');
|
||
expect(stats).toHaveProperty('lines');
|
||
expect(stats).toHaveProperty('readingTime');
|
||
expect(stats.chineseChars).toBe(2);
|
||
});
|
||
|
||
test('getStatus 返回状态字段', () => {
|
||
const status = ed.getStatus();
|
||
expect(status).toHaveProperty('id');
|
||
expect(status).toHaveProperty('mode');
|
||
expect(status).toHaveProperty('theme');
|
||
expect(status).toHaveProperty('locale');
|
||
expect(status).toHaveProperty('fullscreen');
|
||
expect(status).toHaveProperty('destroyed');
|
||
expect(status.destroyed).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 事件', () => {
|
||
let ed;
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
ed = new MarkdownEditor(document.createElement('div'), { value: '' });
|
||
});
|
||
afterEach(() => ed.destroy());
|
||
|
||
test('on 返回取消函数', () => {
|
||
const off = ed.on('change', () => {});
|
||
expect(typeof off).toBe('function');
|
||
});
|
||
|
||
test('off 取消监听', () => {
|
||
let called = 0;
|
||
const fn = () => called++;
|
||
ed.on('change', fn);
|
||
ed.setValue('a');
|
||
const before = called;
|
||
ed.off('change', fn);
|
||
ed.setValue('b');
|
||
expect(called).toBe(before);
|
||
});
|
||
|
||
test('静态 on/off 钩子', () => {
|
||
let triggered = 0;
|
||
const fn = () => { triggered++; };
|
||
MarkdownEditor.on('afterCreate', fn);
|
||
const e = new MarkdownEditor(document.createElement('div'), {});
|
||
expect(triggered).toBeGreaterThan(0);
|
||
MarkdownEditor.off('afterCreate', fn);
|
||
e.destroy();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 插件', () => {
|
||
let ed;
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
ed = new MarkdownEditor(document.createElement('div'), {});
|
||
});
|
||
afterEach(() => ed.destroy());
|
||
|
||
test('use 安装对象插件', () => {
|
||
let installed = false;
|
||
ed.use({ name: 'test', install: () => { installed = true; } });
|
||
expect(installed).toBe(true);
|
||
expect(ed.getPlugins().length).toBe(1);
|
||
});
|
||
|
||
test('use 安装未知预设插件时 warn 且不崩溃', () => {
|
||
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||
ed.use('not-a-preset');
|
||
expect(spy).toHaveBeenCalled();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('addToolbarButton 追加按钮', () => {
|
||
ed.addToolbarButton({ action: 'custom', title: 'Custom' });
|
||
const btn = ed.toolbarEl.querySelector('.me-btn-custom-custom');
|
||
expect(btn).not.toBeNull();
|
||
});
|
||
|
||
test('销毁时调用插件 destroy', () => {
|
||
let destroyed = false;
|
||
const ed2 = new MarkdownEditor(document.createElement('div'), {});
|
||
ed2.use({ name: 't', install() {}, destroy() { destroyed = true; } });
|
||
ed2.destroy();
|
||
expect(destroyed).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 销毁', () => {
|
||
test('destroy 移除 DOM 并标记', () => {
|
||
document.body.innerHTML = '';
|
||
const ed = new MarkdownEditor(document.createElement('div'), {});
|
||
const el = ed.el;
|
||
ed.destroy();
|
||
expect(ed.isDestroyed()).toBe(true);
|
||
// el 已置空
|
||
expect(ed.el).toBeNull();
|
||
});
|
||
|
||
test('重复 destroy 不抛错', () => {
|
||
document.body.innerHTML = '';
|
||
const ed = new MarkdownEditor(document.createElement('div'), {});
|
||
ed.destroy();
|
||
expect(() => ed.destroy()).not.toThrow();
|
||
});
|
||
|
||
test('destroy 触发 destroy 事件', () => {
|
||
document.body.innerHTML = '';
|
||
const ed = new MarkdownEditor(document.createElement('div'), {});
|
||
let fired = false;
|
||
ed.on('destroy', () => { fired = true; });
|
||
ed.destroy();
|
||
expect(fired).toBe(true);
|
||
});
|
||
});
|
||
|
||
// ============ 补充分支测试 ============
|
||
|
||
describe('MarkdownEditor - 构造边界', () => {
|
||
test('非浏览器环境直接返回(_destroyed=true)', () => {
|
||
// jsdom 是浏览器环境,此分支需通过模拟无法直接覆盖
|
||
// 这里只验证 isBrowser() 返回 true 时正常路径
|
||
const ed = new MarkdownEditor(document.createElement('div'), {});
|
||
expect(ed.isDestroyed()).toBe(false);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('options.style 对象会被克隆', () => {
|
||
const style = { color: 'red' };
|
||
const ed = new MarkdownEditor(document.createElement('div'), { style });
|
||
expect(ed.config.style).not.toBe(style);
|
||
expect(ed.config.style.color).toBe('red');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('非法 mode 回退到 split', () => {
|
||
const ed = new MarkdownEditor(document.createElement('div'), { mode: 'invalid-mode' });
|
||
expect(ed.getMode()).toBe('split');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('config.plugins 数组会被安装', () => {
|
||
const plugin = {
|
||
name: 'test-plugin',
|
||
install: jest.fn(),
|
||
destroy: jest.fn(),
|
||
};
|
||
const ed = new MarkdownEditor(document.createElement('div'), { plugins: [plugin] });
|
||
expect(plugin.install).toHaveBeenCalledWith(ed);
|
||
ed.destroy();
|
||
expect(plugin.destroy).toHaveBeenCalledWith(ed);
|
||
});
|
||
|
||
test('autofocus 自动聚焦', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { autofocus: true });
|
||
// jsdom 中聚焦需要元素挂载到 document
|
||
expect(ed.textarea).toBeDefined();
|
||
ed.destroy();
|
||
});
|
||
|
||
test('onCreate 回调被调用', () => {
|
||
const onCreate = jest.fn();
|
||
const ed = new MarkdownEditor(document.createElement('div'), { onCreate });
|
||
expect(onCreate).toHaveBeenCalledWith(ed);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('onCreate 抛错被捕获', () => {
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const ed = new MarkdownEditor(document.createElement('div'), {
|
||
onCreate: () => { throw new Error('boom'); },
|
||
});
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('自定义 render 函数覆盖内置解析器', () => {
|
||
const customRender = jest.fn(() => '<p>custom</p>');
|
||
const ed = new MarkdownEditor(document.createElement('div'), {
|
||
value: '# test',
|
||
render: customRender,
|
||
});
|
||
expect(customRender).toHaveBeenCalled();
|
||
expect(ed.getHTML()).toBe('<p>custom</p>');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('自定义 highlight 函数被调用', () => {
|
||
const highlight = jest.fn(() => '<span class="hl">code</span>');
|
||
const ed = new MarkdownEditor(document.createElement('div'), {
|
||
value: '```js\nvar x = 1\n```',
|
||
highlight,
|
||
});
|
||
expect(highlight).toHaveBeenCalledWith('var x = 1', 'js');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('自定义 sanitize 净化 HTML', () => {
|
||
const ed = new MarkdownEditor(document.createElement('div'), {
|
||
value: 'hello',
|
||
sanitize: (html) => '<div>sanitized</div>',
|
||
});
|
||
expect(ed.getHTML()).toBe('<div>sanitized</div>');
|
||
ed.destroy();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 键盘快捷键', () => {
|
||
let ed;
|
||
beforeEach(() => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
ed = new MarkdownEditor(c, { value: 'hello world' });
|
||
});
|
||
afterEach(() => ed.destroy());
|
||
|
||
function key(key, opts = {}) {
|
||
const ev = new KeyboardEvent('keydown', {
|
||
key,
|
||
ctrlKey: !!opts.ctrl,
|
||
shiftKey: !!opts.shift,
|
||
bubbles: true,
|
||
cancelable: true,
|
||
});
|
||
ed.textarea.dispatchEvent(ev);
|
||
return ev;
|
||
}
|
||
|
||
test('Tab 插入缩进(无选区,默认 tabSize=2)', () => {
|
||
ed.setValue('hello world');
|
||
// 光标放在位置 5(即 'hello' 之后、原空格之前)
|
||
ed.textarea.setSelectionRange(5, 5);
|
||
key('Tab');
|
||
// 插入 2 空格:'hello' + ' ' + ' world' = 'hello world'(含原空格共 3 空格)
|
||
expect(ed.getValue()).toBe('hello world');
|
||
});
|
||
|
||
test('Tab 在行尾插入缩进', () => {
|
||
ed.setValue('hello');
|
||
ed.textarea.setSelectionRange(5, 5);
|
||
key('Tab');
|
||
expect(ed.getValue()).toBe('hello ');
|
||
});
|
||
|
||
test('Shift+Tab 反缩进(有选区)', () => {
|
||
ed.setValue(' line1\n line2');
|
||
ed.textarea.setSelectionRange(0, 14);
|
||
key('Tab', { shift: true });
|
||
// 反缩进会移除每行行首的空格
|
||
expect(ed.getValue()).toBe('line1\nline2');
|
||
});
|
||
|
||
test('Tab 有选区时整块缩进', () => {
|
||
ed.setValue('line1\nline2');
|
||
ed.textarea.setSelectionRange(0, 11);
|
||
key('Tab');
|
||
expect(ed.getValue()).toBe(' line1\n line2');
|
||
});
|
||
|
||
test('Ctrl+B 触发 bold', () => {
|
||
ed.textarea.setSelectionRange(0, 5);
|
||
key('b', { ctrl: true });
|
||
expect(ed.getValue()).toBe('**hello** world');
|
||
});
|
||
|
||
test('Ctrl+I 触发 italic', () => {
|
||
ed.textarea.setSelectionRange(0, 5);
|
||
key('i', { ctrl: true });
|
||
expect(ed.getValue()).toBe('*hello* world');
|
||
});
|
||
|
||
test('Ctrl+E 触发 code', () => {
|
||
ed.textarea.setSelectionRange(0, 5);
|
||
key('e', { ctrl: true });
|
||
expect(ed.getValue()).toBe('`hello` world');
|
||
});
|
||
|
||
test('Ctrl+K 触发 link', () => {
|
||
ed.textarea.setSelectionRange(0, 5);
|
||
key('k', { ctrl: true });
|
||
// link 使用 prompt,jsdom 返回 null,但不应抛错
|
||
expect(ed.getValue()).toBeDefined();
|
||
});
|
||
|
||
test('Ctrl+U 触发 underline', () => {
|
||
ed.textarea.setSelectionRange(0, 5);
|
||
key('u', { ctrl: true });
|
||
expect(ed.getValue()).toBe('<u>hello</u> world');
|
||
});
|
||
|
||
test('Ctrl+1 触发 h1', () => {
|
||
ed.textarea.setSelectionRange(0, 0);
|
||
key('1', { ctrl: true });
|
||
expect(ed.getValue()).toBe('# hello world');
|
||
});
|
||
|
||
test('Ctrl+2 触发 h2', () => {
|
||
ed.textarea.setSelectionRange(0, 0);
|
||
key('2', { ctrl: true });
|
||
expect(ed.getValue()).toBe('## hello world');
|
||
});
|
||
|
||
test('Ctrl+3 触发 h3', () => {
|
||
ed.textarea.setSelectionRange(0, 0);
|
||
key('3', { ctrl: true });
|
||
expect(ed.getValue()).toBe('### hello world');
|
||
});
|
||
|
||
test('Ctrl+Q 触发 quote', () => {
|
||
ed.textarea.setSelectionRange(0, 0);
|
||
key('q', { ctrl: true });
|
||
expect(ed.getValue()).toBe('> hello world');
|
||
});
|
||
|
||
test('Ctrl+Z 撤销', () => {
|
||
const initial = ed.getValue();
|
||
ed.exec('bold');
|
||
key('z', { ctrl: true });
|
||
expect(ed.getValue()).toBe(initial);
|
||
});
|
||
|
||
test('Ctrl+Shift+Z 重做', () => {
|
||
const initial = ed.getValue();
|
||
ed.exec('bold');
|
||
key('z', { ctrl: true }); // undo
|
||
key('z', { ctrl: true, shift: true }); // redo
|
||
expect(ed.getValue()).not.toBe(initial);
|
||
});
|
||
|
||
test('Ctrl+Y 重做', () => {
|
||
const initial = ed.getValue();
|
||
ed.exec('bold');
|
||
key('z', { ctrl: true }); // undo
|
||
key('y', { ctrl: true }); // redo
|
||
expect(ed.getValue()).not.toBe(initial);
|
||
});
|
||
|
||
test('Ctrl+S 触发 save 事件', () => {
|
||
const handler = jest.fn();
|
||
ed.on('save', handler);
|
||
key('s', { ctrl: true });
|
||
expect(handler).toHaveBeenCalled();
|
||
});
|
||
|
||
test('Ctrl+S 调用 onSave 回调', () => {
|
||
const onSave = jest.fn();
|
||
ed.config.onSave = onSave;
|
||
key('s', { ctrl: true });
|
||
expect(onSave).toHaveBeenCalled();
|
||
});
|
||
|
||
test('无修饰键的按键不触发快捷键', () => {
|
||
const initial = ed.getValue();
|
||
key('b');
|
||
expect(ed.getValue()).toBe(initial);
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 同步滚动', () => {
|
||
test('分屏模式下 textarea 滚动联动 preview', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const longText = Array.from({ length: 100 }, (_, i) => `line ${i}`).join('\n');
|
||
const ed = new MarkdownEditor(c, { value: longText, mode: 'split', syncScroll: true });
|
||
ed.textarea.scrollTop = 100;
|
||
ed.textarea.dispatchEvent(new Event('scroll'));
|
||
// preview 应被同步滚动(scrollTop > 0 或为 0,取决于尺寸)
|
||
expect(typeof ed.previewPane.scrollTop).toBe('number');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('syncScroll 关闭时不联动', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
value: 'line1\nline2\nline3',
|
||
syncScroll: false,
|
||
});
|
||
const before = ed.previewPane.scrollTop;
|
||
ed.textarea.scrollTop = 50;
|
||
ed.textarea.dispatchEvent(new Event('scroll'));
|
||
expect(ed.previewPane.scrollTop).toBe(before);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('非 split 模式不联动', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: 'test', mode: 'edit' });
|
||
const before = ed.previewPane ? ed.previewPane.scrollTop : 0;
|
||
ed.textarea.scrollTop = 50;
|
||
ed.textarea.dispatchEvent(new Event('scroll'));
|
||
ed.destroy();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 分隔条拖拽', () => {
|
||
test('pointerdown 触发拖拽(split 模式)', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: 'test', mode: 'split' });
|
||
// jsdom 中 getBoundingClientRect 返回全 0,但不应抛错
|
||
expect(() => {
|
||
ed.dividerEl.dispatchEvent(new PointerEvent('pointerdown', { clientX: 200, bubbles: true }));
|
||
window.dispatchEvent(new PointerEvent('pointermove', { clientX: 250 }));
|
||
window.dispatchEvent(new PointerEvent('pointerup'));
|
||
}).not.toThrow();
|
||
expect(ed.isDestroyed()).toBe(false);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('非 split 模式不触发拖拽', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: 'test', mode: 'edit' });
|
||
expect(() => {
|
||
ed.dividerEl.dispatchEvent(new PointerEvent('pointerdown', { clientX: 200, bubbles: true }));
|
||
}).not.toThrow();
|
||
ed.destroy();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 回调异常隔离', () => {
|
||
test('onChange 抛错不影响后续 input', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
value: 'init',
|
||
onChange: () => { throw new Error('boom'); },
|
||
});
|
||
expect(() => {
|
||
ed.textarea.value = 'new';
|
||
ed.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||
}).not.toThrow();
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('onInput 抛错被捕获', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
value: 'init',
|
||
onInput: () => { throw new Error('boom'); },
|
||
});
|
||
ed.textarea.value = 'new';
|
||
ed.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('onFocus 抛错被捕获', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
onFocus: () => { throw new Error('boom'); },
|
||
});
|
||
ed.textarea.dispatchEvent(new Event('focus', { bubbles: true }));
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('onBlur 抛错被捕获', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
onBlur: () => { throw new Error('boom'); },
|
||
});
|
||
ed.textarea.dispatchEvent(new Event('blur', { bubbles: true }));
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('onModeChange 抛错被捕获', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
onModeChange: () => { throw new Error('boom'); },
|
||
});
|
||
ed.setMode('edit');
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('onFullscreen 抛错被捕获', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
onFullscreen: () => { throw new Error('boom'); },
|
||
});
|
||
ed.toggleFullscreen();
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('onDestroy 抛错被捕获', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
onDestroy: () => { throw new Error('boom'); },
|
||
});
|
||
ed.destroy();
|
||
expect(spy).toHaveBeenCalled();
|
||
spy.mockRestore();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 插件边界', () => {
|
||
test('use 字符串引用未知预设插件 warn', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
ed.use('non-existent-preset');
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('use 无效插件对象 warn', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
ed.use(null);
|
||
ed.use(123);
|
||
ed.use({ name: 'no-install' });
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('use install 抛错被捕获', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
const badPlugin = {
|
||
name: 'bad',
|
||
install: () => { throw new Error('install boom'); },
|
||
};
|
||
ed.use(badPlugin);
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('addToolbarButton 无 action 直接返回', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
const before = ed.toolbarEl.querySelectorAll('.me-btn').length;
|
||
ed.addToolbarButton(null);
|
||
ed.addToolbarButton({ title: 'no-action' });
|
||
expect(ed.toolbarEl.querySelectorAll('.me-btn').length).toBe(before);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('addToolbarButton 使用 onClick 回调', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
const onClick = jest.fn();
|
||
ed.addToolbarButton({
|
||
action: 'custom',
|
||
title: 'Custom',
|
||
icon: '<svg></svg>',
|
||
onClick,
|
||
});
|
||
const btn = ed.toolbarEl.querySelector('.me-btn-custom-custom');
|
||
expect(btn).not.toBeNull();
|
||
btn.click();
|
||
expect(onClick).toHaveBeenCalledWith(ed);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('addToolbarButton 使用 text 文本', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
ed.addToolbarButton({
|
||
action: 'txt',
|
||
title: 'Text Button',
|
||
text: 'T',
|
||
});
|
||
const btn = ed.toolbarEl.querySelector('.me-btn-custom-txt');
|
||
expect(btn).not.toBeNull();
|
||
expect(btn.innerHTML).toContain('>T<');
|
||
ed.destroy();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 工具栏点击', () => {
|
||
test('点击空白区域不抛错', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
expect(() => {
|
||
ed.toolbarEl.click();
|
||
}).not.toThrow();
|
||
ed.destroy();
|
||
});
|
||
|
||
test('点击 mode 按钮切换模式', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { mode: 'split' });
|
||
const editBtn = ed.toolbarEl.querySelector('.me-btn[data-mode="edit"]');
|
||
editBtn.click();
|
||
expect(ed.getMode()).toBe('edit');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('点击 action 按钮执行命令', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: 'test' });
|
||
const boldBtn = ed.toolbarEl.querySelector('.me-btn[data-action="bold"]');
|
||
ed.textarea.setSelectionRange(0, 4);
|
||
boldBtn.click();
|
||
expect(ed.getValue()).toBe('**test**');
|
||
ed.destroy();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 历史栈合并', () => {
|
||
test('连续输入触发防抖合并', (done) => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: '' });
|
||
// 连续快速输入
|
||
ed.textarea.value = 'a';
|
||
ed.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||
ed.textarea.value = 'ab';
|
||
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);
|
||
});
|
||
|
||
test('历史栈超过上限自动裁剪', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: '', historyLimit: 3 });
|
||
for (let i = 0; i < 10; i++) {
|
||
ed.setValue('v' + i, { silent: false });
|
||
}
|
||
expect(ed._history.length).toBeLessThanOrEqual(3);
|
||
ed.destroy();
|
||
});
|
||
});
|
||
|
||
describe('MarkdownEditor - 零散分支补全', () => {
|
||
test('className 配置项被应用到 wrapper', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { className: 'my-cls another' });
|
||
expect(ed.el.className).toContain('my-cls');
|
||
expect(ed.el.className).toContain('another');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('render 抛错时显示错误信息', () => {
|
||
document.body.innerHTML = '';
|
||
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {
|
||
value: '# test',
|
||
render: () => { throw new Error('render boom'); },
|
||
});
|
||
const html = ed.previewEl.innerHTML;
|
||
expect(html).toContain('渲染失败');
|
||
expect(html).toContain('render boom');
|
||
expect(spy).toHaveBeenCalled();
|
||
ed.destroy();
|
||
spy.mockRestore();
|
||
});
|
||
|
||
test('canRedo 返回正确值', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: '' });
|
||
expect(ed.canRedo()).toBe(false);
|
||
ed.setValue('a');
|
||
ed.setValue('b');
|
||
ed.undo();
|
||
expect(ed.canRedo()).toBe(true);
|
||
ed.redo();
|
||
expect(ed.canRedo()).toBe(false);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('undo/redo 触发 onChange 回调', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const onChange = jest.fn();
|
||
const ed = new MarkdownEditor(c, { value: '', onChange });
|
||
onChange.mockClear();
|
||
ed.setValue('a');
|
||
ed.setValue('b');
|
||
onChange.mockClear();
|
||
ed.undo();
|
||
expect(onChange).toHaveBeenCalled();
|
||
onChange.mockClear();
|
||
ed.redo();
|
||
expect(onChange).toHaveBeenCalled();
|
||
ed.destroy();
|
||
});
|
||
|
||
test('exec("indent")/exec("outdent") 等价于 Tab/Shift+Tab', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: 'hello' });
|
||
ed.textarea.selectionStart = ed.textarea.selectionEnd = 0;
|
||
ed.exec('indent');
|
||
expect(ed.getValue()).toBe(' hello');
|
||
ed.textarea.selectionStart = 0;
|
||
ed.textarea.selectionEnd = 7;
|
||
ed.exec('outdent');
|
||
expect(ed.getValue()).toBe('hello');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('exec("undo")/exec("redo") 等价于 undo()/redo()', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: '' });
|
||
ed.setValue('a');
|
||
ed.setValue('b');
|
||
ed.exec('undo');
|
||
expect(ed.getValue()).toBe('a');
|
||
ed.exec('redo');
|
||
expect(ed.getValue()).toBe('b');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('exec("edit"/"split"/"preview") 切换模式', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { mode: 'split' });
|
||
ed.exec('edit');
|
||
expect(ed.getMode()).toBe('edit');
|
||
ed.exec('split');
|
||
expect(ed.getMode()).toBe('split');
|
||
ed.exec('preview');
|
||
expect(ed.getMode()).toBe('preview');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('exec("fullscreen") 切换全屏', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
expect(ed._fullscreen).toBe(false);
|
||
ed.exec('fullscreen');
|
||
expect(ed._fullscreen).toBe(true);
|
||
ed.exec('fullscreen');
|
||
expect(ed._fullscreen).toBe(false);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('_toggleLinePrefix 替换已存在的不同前缀', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, { value: '- hello' });
|
||
ed.textarea.selectionStart = ed.textarea.selectionEnd = 0;
|
||
ed.exec('h1');
|
||
// 原本行首是 "- ",执行 h1 后应替换为 "# "
|
||
expect(ed.getValue()).toBe('# hello');
|
||
ed.destroy();
|
||
});
|
||
|
||
test('setValue 触发 onChange 回调', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const onChange = jest.fn();
|
||
const ed = new MarkdownEditor(c, { value: '', onChange });
|
||
onChange.mockClear();
|
||
ed.setValue('new');
|
||
expect(onChange).toHaveBeenCalledWith('new', ed);
|
||
ed.destroy();
|
||
});
|
||
|
||
test('getStatus 包含 plugins 字段', () => {
|
||
document.body.innerHTML = '';
|
||
const c = document.createElement('div');
|
||
document.body.appendChild(c);
|
||
const ed = new MarkdownEditor(c, {});
|
||
ed.use({ name: 'demo-plugin', install: () => {} });
|
||
const s = ed.getStatus();
|
||
expect(Array.isArray(s.plugins)).toBe(true);
|
||
expect(s.plugins).toContain('demo-plugin');
|
||
ed.destroy();
|
||
});
|
||
});
|