Files
MetonaEditor/tests/index.test.ts
T
thzxx cb0caff4cf
CI / test-parser (push) Successful in 9m27s
CI / test-core (push) Successful in 9m34s
CI / test-rest (push) Successful in 9m27s
CI / e2e (push) Failing after 5m15s
CI / verify (20.x) (push) Successful in 9m54s
CI / verify (18.x) (push) Successful in 9m56s
CI / verify (24.x) (push) Successful in 9m48s
feat: v0.4.0 — 安全修复×6 + 增量性能×3 + Playwright E2E + 实例级 i18n
0.3.0 修复版:
- 脚注 id 属性注入 XSS 防护(行内引用 + 脚注区)
- 全局插件注入移至 afterCreate(searchReplace 等依赖 textarea 的插件真正生效)+ use() 同名防重
- 行内渲染缓存附加 refs 指纹,防跨文档引用链接串数据
- 白名单裸标签 <u>/</u> 透传,underline(Ctrl+U)预览可见
- getStatus()/渲染 env 使用实例 locale;replaceAll 替换文本按字面处理
- replaceAllRegex 保留 $1 捕获组语义

0.3.1 性能版:
- 高亮规则按语言缓存(registerLanguage 覆盖失效),bench +33%
- 统计增量计算:字数/词数/行数差异区间,击键零全量扫描
- outline 树构建 O(n²) → 迭代栈 O(n)
- 拖放图片 >500KB 拦截、scrollToLine 真实行高、undo/redo 光标恢复
- selectionChange/cursorMove 与浮动工具栏解耦、unregisterShortcut 大小写归一
- toast 接入 7 种 ANIMATIONS、实例主题订阅随 destroy 断开(dispose)

0.4.0 工程版:
- Playwright 真实浏览器冒烟测试(e2e/,22 项断言)+ CI e2e job
- sideEffects: false 便于 tree-shaking
- MeEditor.destroy() 全面复位(全局钩子清理 + 内部注入钩子重建)
- 实例 locale 统一作用于全部 UI 文案(状态栏/工具栏/右键菜单/大纲)

测试 782 → 826,全绿;typecheck/lint/build 通过
2026-08-09 12:31:14 +08:00

219 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* index.ts 单元测试
* 覆盖全局 APIcreate / use / on / off / setTheme / setLocale / destroy / getStatus
*/
import MeEditor, {
create, use, on, off, setTheme, setLocale, destroy, getStatus,
parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache,
registerBlockHandler, topologicalSort, validateConfig,
VERSION,
} from '../src/index';
import { MarkdownEditor } from '../src/core';
describe('index.ts - 全局 API', () => {
let container: HTMLElement;
beforeEach(() => {
document.body.innerHTML = '';
container = document.createElement('div');
container.id = 'test-host';
document.body.appendChild(container);
// Set MeEditor on window as index.ts does
(window as any).MeEditor = MeEditor;
});
afterEach(() => {
document.body.innerHTML = '';
});
test('VERSION 是字符串', () => {
expect(typeof VERSION).toBe('string');
expect(VERSION).toBe('0.4.0');
});
test('api.default 是 api 本身', () => {
expect(MeEditor).toBeDefined();
expect(MeEditor.VERSION).toBe(VERSION);
});
test('create 创建编辑器实例', () => {
const editor = create('#test-host', { value: '# Hello' });
expect(editor).toBeInstanceOf(MarkdownEditor);
expect(editor.getValue()).toBe('# Hello');
editor.destroy();
});
test('create 接受 HTMLElement', () => {
const editor = create(container, { value: 'text' });
expect(editor.getValue()).toBe('text');
editor.destroy();
});
test('use 注册全局插件(通过字符串)', () => {
const result = use('autoSave', { delay: 100 });
expect(result).toBe(MeEditor); // 返回 api(链式)
const status = getStatus();
expect(status.globalPlugins).toContain('autoSave');
});
test('use 未知预设插件 warn', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
use('non-existent-preset');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
test('use 无效插件对象 warn', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
use(null);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
test('全局插件被注入到新实例', () => {
// Reset global plugins by destroying
destroy();
use('autoSave');
const ed = create(container, { value: 'test' });
const plugins = ed.getPlugins();
expect(plugins.some((p: any) => p.name === 'autoSave')).toBe(true);
ed.destroy();
});
test('全局 searchReplace 在新实例上可用(DOM 已就绪)', () => {
destroy();
use('searchReplace');
const ed = create(container, { value: 'foo bar' });
ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', ctrlKey: true, bubbles: true }));
expect(ed.el.querySelector('.me-search')).not.toBeNull();
ed.destroy();
});
test('全局插件与 config.plugins 同名时只安装一次', () => {
destroy();
use('autoSave');
const ed = create(container, { value: 'test', plugins: ['autoSave'] });
const count = ed.getPlugins().filter((p: any) => p.name === 'autoSave').length;
expect(count).toBe(1);
ed.destroy();
});
test('on / off 全局钩子', () => {
const fn = jest.fn();
const unsub = on('afterCreate', fn);
expect(typeof unsub).toBe('function');
const ed = create(container, {});
expect(fn).toHaveBeenCalled();
off('afterCreate', fn);
ed.destroy();
});
test('setTheme 切换全局主题', () => {
expect(() => setTheme('dark')).not.toThrow();
});
test('setLocale 切换全局语言', () => {
expect(() => setLocale('en-US')).not.toThrow();
});
test('getStatus 返回状态对象', () => {
const status = getStatus();
expect(status).toHaveProperty('version');
expect(status).toHaveProperty('theme');
expect(status).toHaveProperty('locale');
expect(status).toHaveProperty('globalPlugins');
expect(status).toHaveProperty('presetPlugins');
expect(Array.isArray(status.globalPlugins)).toBe(true);
expect(Array.isArray(status.presetPlugins)).toBe(true);
});
test('destroy 清理全局资源', () => {
destroy();
const status = getStatus();
expect(status.globalPlugins.length).toBe(0);
});
test('destroy 清理用户全局钩子但保留内部注入钩子', () => {
destroy();
const fn = jest.fn();
on('afterCreate', fn);
expect(MarkdownEditor._hooks.get('afterCreate')!.length).toBeGreaterThan(0);
destroy();
const hooks = MarkdownEditor._hooks.get('afterCreate') || [];
expect(hooks.length).toBe(1); // 仅剩内部 injectGlobalPlugins
expect(hooks[0]).not.toBe(fn);
});
test('destroy 后 MeEditor.use 全局插件仍可用', () => {
destroy();
use('autoSave');
const ed = create(container, { value: 'x' });
expect(ed.getPlugins().some((p: any) => p.name === 'autoSave')).toBe(true);
ed.destroy();
});
test('window.MeEditor 被设置', () => {
expect((window as any).MeEditor).toBeDefined();
expect((window as any).MeEditor.VERSION).toBe(VERSION);
});
});
describe('index.ts - 解析器导出', () => {
test('parseMarkdown 可直接调用', () => {
const html = parseMarkdown('# Hello');
expect(html).toContain('<h1');
});
test('parseTokens 可直接调用', () => {
const { tokens, footnotes, refs } = parseTokens('# Hi\n\n**bold**');
expect(Array.isArray(tokens)).toBe(true);
expect(tokens.length).toBeGreaterThan(0);
expect(typeof footnotes).toBe('object');
expect(typeof refs).toBe('object');
});
test('renderTokens 可直接调用', () => {
const { tokens } = parseTokens('**hello**');
const html = renderTokens(tokens);
expect(html).toContain('<strong>');
});
test('safeUrl 过滤危险协议', () => {
expect(safeUrl('javascript:alert(1)')).toBe('');
expect(safeUrl('https://example.com')).toBe('https://example.com');
});
test('slugify 生成锚点 id', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
test('clearRenderCache 不抛错', () => {
expect(() => clearRenderCache()).not.toThrow();
});
test('registerBlockHandler 注册自定义块', () => {
registerBlockHandler({
name: 'testBlock',
priority: 50,
test: () => null,
parse: (_l, i) => ({ token: null, newIndex: i }),
});
// 不抛错即通过
expect(true).toBe(true);
});
});
describe('index.ts - 具名导出', () => {
test('create 是函数', () => expect(typeof create).toBe('function'));
test('use 是函数', () => expect(typeof use).toBe('function'));
test('on 是函数', () => expect(typeof on).toBe('function'));
test('off 是函数', () => expect(typeof off).toBe('function'));
test('setTheme 是函数', () => expect(typeof setTheme).toBe('function'));
test('setLocale 是函数', () => expect(typeof setLocale).toBe('function'));
test('destroy 是函数', () => expect(typeof destroy).toBe('function'));
test('getStatus 是函数', () => expect(typeof getStatus).toBe('function'));
test('topologicalSort 是函数', () => expect(typeof topologicalSort).toBe('function'));
test('validateConfig 是函数', () => expect(typeof validateConfig).toBe('function'));
});