feat: v0.2.3 — 浮动格式工具栏 + 10个新API + 德语翻译 + 类型修复
CI / test (18.x) (push) Canceled after 0s
CI / test (20.x) (push) Canceled after 0s
CI / test (22.x) (push) Canceled after 0s
CI / test (24.x) (push) Canceled after 0s

### Added
- 浮动格式工具栏:选中文本自动弹出 bold/italic/code/link/strikethrough
- 10 个新 API: getSelectedText/getCursorPosition/setCursorPosition/scrollToLine/selectLine/selectAll/replaceAll/replaceAllRegex/lineCount/getLine
- 2 个新事件: selectionChange / cursorMove
- 德语 (de) locale 完整翻译
- floatingToolbar 配置项(默认 true)

### Fixed
- onLinkClick 移除 as any,正式加入 EditorOptions/DEFAULTS
- 所有版本号统一为 0.2.3

### Tests
- core.test.ts: 183 → 214 (+31 tests)
- 总计 ~725 tests
This commit is contained in:
2026-07-25 14:53:43 +08:00
parent 5184e9e8c1
commit 708f0937d8
13 changed files with 569 additions and 31 deletions
+288
View File
@@ -2016,3 +2016,291 @@ describe('MarkdownEditor - v0.2.2 大纲滚动跟踪', () => {
ed.destroy();
});
});
// ============ v0.2.3 光标/选区 API ============
describe('MarkdownEditor - v0.2.3 光标/选区 API', () => {
let ed;
beforeEach(() => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
ed = new MarkdownEditor(c, { value: 'line one\nline two\nline three' });
});
afterEach(() => ed.destroy());
test('getSelectedText 返回选区文本', () => {
ed.textarea.setSelectionRange(0, 8);
expect(ed.getSelectedText()).toBe('line one');
});
test('getSelectedText 无选区返回空字符串', () => {
ed.textarea.setSelectionRange(5, 5);
expect(ed.getSelectedText()).toBe('');
});
test('getCursorPosition 返回行列', () => {
ed.textarea.setSelectionRange(0, 0);
let pos = ed.getCursorPosition();
expect(pos.line).toBe(1);
expect(pos.column).toBe(0);
// 第二行位置 5
ed.textarea.setSelectionRange(14, 14); // 'line two' 的 'line ' 之后
pos = ed.getCursorPosition();
expect(pos.line).toBe(2);
expect(pos.column).toBeGreaterThanOrEqual(0);
});
test('setCursorPosition 移动到指定位置', () => {
ed.setCursorPosition(2, 3);
expect(ed.textarea.selectionStart).toBeGreaterThan(0);
expect(ed.textarea.selectionStart).toBe(ed.textarea.selectionEnd);
});
test('setCursorPosition 超出范围自动 clamp', () => {
ed.setCursorPosition(99, 999);
// 不应抛错,应在末尾
expect(ed.textarea.selectionStart).toBeGreaterThanOrEqual(0);
});
test('scrollToLine 不抛错', () => {
// 构建多行内容测试滚动
ed.setValue(Array.from({ length: 50 }, (_, i) => `line ${i}`).join('\n'));
expect(() => ed.scrollToLine(30)).not.toThrow();
expect(() => ed.scrollToLine(1)).not.toThrow();
expect(() => ed.scrollToLine(999)).not.toThrow();
});
test('selectLine 选中指定行', () => {
ed.selectLine(2);
const sel = ed.getSelectedText();
expect(sel).toBe('line two');
});
test('selectLine 超出范围自动 clamp', () => {
ed.selectLine(100);
expect(() => ed.selectLine(0)).not.toThrow();
});
test('selectAll 选中全部内容', () => {
ed.selectAll();
expect(ed.getSelectedText()).toBe('line one\nline two\nline three');
});
test('selectAll 在空内容上不抛错', () => {
ed.setValue('');
expect(() => ed.selectAll()).not.toThrow();
});
});
// ============ v0.2.3 内容操作 API ============
describe('MarkdownEditor - v0.2.3 内容操作 API', () => {
let ed;
beforeEach(() => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
ed = new MarkdownEditor(c, { value: 'hello world, hello universe' });
});
afterEach(() => ed.destroy());
test('replaceAll 替换所有匹配', () => {
const count = ed.replaceAll('hello', 'hi');
expect(count).toBe(2);
expect(ed.getValue()).toBe('hi world, hi universe');
});
test('replaceAll 无匹配返回 0', () => {
const count = ed.replaceAll('xyz', 'abc');
expect(count).toBe(0);
});
test('replaceAll 空字符串返回 0', () => {
expect(ed.replaceAll('', 'x')).toBe(0);
});
test('replaceAll caseSensitive 选项', () => {
const count = ed.replaceAll('Hello', 'hi', false);
expect(count).toBe(2);
});
test('replaceAllRegex 正则替换', () => {
const count = ed.replaceAllRegex(/hello/gi, 'hi');
expect(count).toBe(2);
expect(ed.getValue()).toBe('hi world, hi universe');
});
test('replaceAllRegex 无匹配返回 0', () => {
expect(ed.replaceAllRegex(/xyz/g, 'abc')).toBe(0);
});
test('replaceAllRegex 空模式返回 0', () => {
expect(ed.replaceAllRegex(null, 'x')).toBe(0);
});
test('replaceAll 触发 change 事件', () => {
const handler = jest.fn();
ed.on('change', handler);
ed.replaceAll('hello', 'hi');
expect(handler).toHaveBeenCalled();
});
test('lineCount 返回行数', () => {
expect(ed.lineCount()).toBe(1);
ed.setValue('a\nb\nc');
expect(ed.lineCount()).toBe(3);
});
test('lineCount 空内容返回 0', () => {
ed.setValue('');
expect(ed.lineCount()).toBe(0);
});
test('getLine 获取指定行', () => {
ed.setValue('line1\nline2\nline3');
expect(ed.getLine(1)).toBe('line1');
expect(ed.getLine(2)).toBe('line2');
expect(ed.getLine(3)).toBe('line3');
});
test('getLine 越界返回空字符串', () => {
expect(ed.getLine(0)).toBe('');
expect(ed.getLine(999)).toBe('');
});
});
// ============ v0.2.3 浮动工具栏 ============
describe('MarkdownEditor - v0.2.3 浮动工具栏', () => {
test('floatingToolbar 默认创建', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'hello world' });
// 浮动工具栏应被注入 CSS
const style = document.getElementById('me-float-style');
expect(style).not.toBeNull();
// _floatingToolbar 在首次 selection 时才创建 DOM
expect(ed._floatingToolbar).toBeNull();
// _selectionTimer 已启动
expect(ed._selectionTimer).not.toBeNull();
ed.destroy();
});
test('floatingToolbar: false 不启用', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'hello', floatingToolbar: false });
// 浮动工具栏应不启动(_selectionTimer 为 null
expect(ed._selectionTimer).toBeNull();
expect(ed._floatingToolbar).toBeNull();
ed.destroy();
});
test('选中文本时显示浮动工具栏', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'hello world' });
// 手动创建并显示
ed._buildFloatingToolbar();
expect(ed._floatingToolbar).not.toBeNull();
expect(ed._floatingToolbar!.classList.contains('me-float-toolbar')).toBe(true);
// 检查包含按钮
const btns = ed._floatingToolbar!.querySelectorAll('.me-btn');
expect(btns.length).toBe(5); // bold, italic, code, link, strikethrough
ed.destroy();
});
test('destroy 清理浮动工具栏', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'test' });
ed._buildFloatingToolbar();
expect(ed._floatingToolbar).not.toBeNull();
ed.destroy();
expect(ed._floatingToolbar).toBeNull();
expect(ed._selectionTimer).toBeNull();
});
test('selectionChange 事件触发', (done) => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'hello world' });
const handler = jest.fn();
ed.on('selectionChange', handler);
ed.textarea.setSelectionRange(0, 5);
// selectionChange 通过 interval 检测,等 300ms
setTimeout(() => {
expect(handler).toHaveBeenCalled();
expect(handler.mock.calls[0][0]).toHaveProperty('start', 0);
expect(handler.mock.calls[0][0]).toHaveProperty('end', 5);
expect(handler.mock.calls[0][0]).toHaveProperty('text', 'hello');
ed.destroy();
done();
}, 400);
});
test('cursorMove 事件在按键后触发', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { value: 'ab' });
const handler = jest.fn();
ed.on('cursorMove', handler);
ed.textarea.setSelectionRange(1, 1);
ed.textarea.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight', bubbles: true }));
expect(handler).toHaveBeenCalled();
ed.destroy();
});
});
// ============ v0.2.3 config.onLinkClick 修复验证 ============
describe('MarkdownEditor - v0.2.3 onLinkClick 类型修复', () => {
test('onLinkClick 在 DEFAULTS 中存在', () => {
const { DEFAULTS } = require('../src/constants');
expect('onLinkClick' in DEFAULTS).toBe(true);
expect(DEFAULTS.onLinkClick).toBeNull();
});
test('onLinkClick 回调正常触发', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const handler = jest.fn();
const ed = new MarkdownEditor(c, {
value: '[click](https://example.com)',
onLinkClick: handler,
mode: 'split',
});
// Need to render first
const link = ed.previewEl.querySelector('a');
if (link) {
link.click();
expect(handler).toHaveBeenCalledWith('https://example.com', 'click', ed);
}
ed.destroy();
});
});
// ============ v0.2.3 getStatus 新字段验证 ============
describe('MarkdownEditor - v0.2.3 getStatus', () => {
test('getStatus 返回完整结构', () => {
document.body.innerHTML = '';
const c = document.createElement('div');
document.body.appendChild(c);
const ed = new MarkdownEditor(c, { readOnly: true });
const s = ed.getStatus();
expect(s.readOnly).toBe(true);
expect(s.disabled).toBe(false);
expect(Array.isArray(s.plugins)).toBe(true);
ed.destroy();
});
});
+1 -1
View File
@@ -29,7 +29,7 @@ describe('index.ts - 全局 API', () => {
test('VERSION 是字符串', () => {
expect(typeof VERSION).toBe('string');
expect(VERSION).toBe('0.2.1');
expect(VERSION).toBe('0.2.3');
});
test('api.default 是 api 本身', () => {