CI / test-parser (push) Successful in 9m29s
CI / test-core (push) Successful in 9m36s
CI / test-rest (push) Successful in 9m29s
CI / e2e (push) Successful in 9m44s
CI / verify (18.x) (push) Successful in 9m50s
CI / verify (20.x) (push) Successful in 9m50s
CI / verify (24.x) (push) Successful in 9m45s
219 lines
7.1 KiB
TypeScript
219 lines
7.1 KiB
TypeScript
/**
|
||
* index.ts 单元测试
|
||
* 覆盖全局 API:create / 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.1');
|
||
});
|
||
|
||
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'));
|
||
});
|