/** * 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.2'); }); 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(' { 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(''); }); 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 注册自定义块', () => { let matched = false; registerBlockHandler({ name: 'testBlockIndex', priority: 50, test: (line) => { if (line === '!!!custom!!!') { matched = true; return true; } return null; }, parse: (_l, i) => ({ token: { type: 'testBlockIndex', content: 'hit' }, newIndex: i + 1 }), }); const html = parseMarkdown('!!!custom!!!\nafter'); // 自定义 handler 被真实命中并产出 token(回退渲染为 me-block-* div) expect(matched).toBe(true); expect(html).toContain('me-block-testBlockIndex'); }); }); 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')); });