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
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 通过
125 lines
4.6 KiB
TypeScript
125 lines
4.6 KiB
TypeScript
/**
|
||
* highlight.ts 单元测试
|
||
* 覆盖内置轻量语法高亮器的安全性与标记输出
|
||
*/
|
||
|
||
import { highlight, normalizeLanguage, registerLanguage, getSupportedLanguages } from '../src/highlight';
|
||
|
||
describe('highlight - 基础', () => {
|
||
test('未知语言返回转义纯文本', () => {
|
||
const out = highlight('const x = 1;', 'nonexistent');
|
||
expect(out).toBe('const x = 1;');
|
||
});
|
||
|
||
test('支持的语言别名归一化', () => {
|
||
expect(normalizeLanguage('js')).toBe('javascript');
|
||
expect(normalizeLanguage('ts')).toBe('typescript');
|
||
expect(normalizeLanguage('py')).toBe('python');
|
||
expect(normalizeLanguage('sh')).toBe('bash');
|
||
expect(normalizeLanguage('yml')).toBe('yaml');
|
||
expect(normalizeLanguage('Java')).toBe('java');
|
||
expect(normalizeLanguage('unknown')).toBe('');
|
||
expect(normalizeLanguage('')).toBe('');
|
||
});
|
||
|
||
test('关键词被标记', () => {
|
||
const out = highlight('const x = 1', 'js');
|
||
expect(out).toContain('<span class="me-hl-keyword">const</span>');
|
||
});
|
||
|
||
test('字符串被标记', () => {
|
||
const out = highlight('const s = "hello"', 'js');
|
||
expect(out).toContain('<span class="me-hl-string">"hello"</span>');
|
||
});
|
||
|
||
test('注释被标记', () => {
|
||
const out = highlight('// comment\nconst x = 1', 'js');
|
||
expect(out).toContain('<span class="me-hl-comment">// comment</span>');
|
||
});
|
||
|
||
test('数字被标记', () => {
|
||
const out = highlight('const n = 42', 'js');
|
||
expect(out).toContain('<span class="me-hl-number">42</span>');
|
||
});
|
||
|
||
test('函数调用被标记', () => {
|
||
const out = highlight('foo(bar)', 'js');
|
||
expect(out).toContain('<span class="me-hl-function">foo</span>');
|
||
});
|
||
|
||
test('python 井号注释与 def 关键词', () => {
|
||
const out = highlight('# note\ndef main():', 'python');
|
||
expect(out).toContain('<span class="me-hl-comment"># note</span>');
|
||
expect(out).toContain('<span class="me-hl-keyword">def</span>');
|
||
});
|
||
|
||
test('内置全局对象被标记', () => {
|
||
const out = highlight('console.log("x")', 'js');
|
||
expect(out).toContain('<span class="me-hl-builtin">console</span>');
|
||
});
|
||
});
|
||
|
||
describe('highlight - 安全性', () => {
|
||
test('HTML 特殊字符被转义,不产生裸标签', () => {
|
||
const out = highlight('<script>alert(1)</script>', 'js');
|
||
expect(out).not.toContain('<script>');
|
||
expect(out).not.toContain('<script');
|
||
expect(out).toContain('<script>');
|
||
});
|
||
|
||
test('字符串中的引号安全', () => {
|
||
const out = highlight('const s = "a<b>&"', 'js');
|
||
expect(out).not.toContain('a<b');
|
||
expect(out).toContain('a<b>');
|
||
});
|
||
|
||
test('恶意输入不破坏 span 结构', () => {
|
||
const out = highlight('"</span><script>x</script>"', 'js');
|
||
// 所有 span 配对(每开一个 <span 就有一个 </span>)
|
||
const opens = (out.match(/<span/g) || []).length;
|
||
const closes = (out.match(/<\/span>/g) || []).length;
|
||
expect(opens).toBe(closes);
|
||
expect(out).not.toContain('<script');
|
||
});
|
||
});
|
||
|
||
describe('highlight - 语言注册', () => {
|
||
test('注册自定义语言', () => {
|
||
registerLanguage('foo', { keywords: ['frobnicate'], builtins: [] });
|
||
expect(normalizeLanguage('foo')).toBe('foo');
|
||
const out = highlight('frobnicate the widget', 'foo');
|
||
expect(out).toContain('<span class="me-hl-keyword">frobnicate</span>');
|
||
});
|
||
|
||
test('覆盖已有语言定义后规则缓存失效', () => {
|
||
registerLanguage('langA', { keywords: ['one'], builtins: [] });
|
||
expect(highlight('one', 'langA')).toContain('me-hl-keyword');
|
||
registerLanguage('langA', { keywords: ['two'], builtins: [] });
|
||
expect(highlight('two', 'langA')).toContain('me-hl-keyword');
|
||
expect(highlight('one', 'langA')).not.toContain('me-hl-keyword');
|
||
});
|
||
|
||
test('getSupportedLanguages 返回语言列表', () => {
|
||
const langs = getSupportedLanguages();
|
||
expect(Array.isArray(langs)).toBe(true);
|
||
expect(langs).toContain('javascript');
|
||
expect(langs).toContain('typescript');
|
||
expect(langs).toContain('python');
|
||
});
|
||
});
|
||
|
||
describe('highlight - 环境一致性', () => {
|
||
test('字符串高亮与引号转义环境无关(jsdom 断言)', () => {
|
||
const out = highlight('const s = "hi"', 'js');
|
||
expect(out).toContain('<span class="me-hl-string">"hi"</span>');
|
||
});
|
||
|
||
test('引号不被实体化,span 文本保持原样', () => {
|
||
const out = highlight('"a" + \'b\'', 'js');
|
||
expect(out).not.toContain('"');
|
||
// 双引号字符串应被完整标记
|
||
expect(out).toContain('<span class="me-hl-string">"a"</span>');
|
||
expect(out).toContain("<span class=\"me-hl-string\">'b'</span>");
|
||
});
|
||
});
|