You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
150 lines
5.2 KiB
150 lines
5.2 KiB
/**
|
|
* markdown.ts Markdown 渲染器单元测试
|
|
*
|
|
* 重点覆盖:
|
|
* 1. 基础语法正确性(标题、粗体、斜体、列表、链接、引用、代码)
|
|
* 2. XSS 防护(先转义再转换、链接白名单 http/https)
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { renderMarkdown } from '../src/markdown';
|
|
|
|
describe('renderMarkdown - 基础语法', () => {
|
|
it('空字符串返回空', () => {
|
|
expect(renderMarkdown('')).toBe('');
|
|
});
|
|
|
|
it('null/undefined 返回空', () => {
|
|
expect(renderMarkdown(null as never)).toBe('');
|
|
expect(renderMarkdown(undefined as never)).toBe('');
|
|
});
|
|
|
|
it('普通文本渲染为段落', () => {
|
|
expect(renderMarkdown('你好世界')).toContain('<p class="csk-md-p">你好世界</p>');
|
|
});
|
|
|
|
it('标题渲染 h1-h6', () => {
|
|
expect(renderMarkdown('# 标题一')).toContain('<h1 class="csk-md-h1">');
|
|
expect(renderMarkdown('## 标题二')).toContain('<h2 class="csk-md-h2">');
|
|
expect(renderMarkdown('### 标题三')).toContain('<h3 class="csk-md-h3">');
|
|
});
|
|
|
|
it('粗体渲染', () => {
|
|
expect(renderMarkdown('**粗体文本**')).toContain('<strong>粗体文本</strong>');
|
|
});
|
|
|
|
it('斜体渲染', () => {
|
|
expect(renderMarkdown('*斜体文本*')).toContain('<em>斜体文本</em>');
|
|
});
|
|
|
|
it('删除线渲染', () => {
|
|
expect(renderMarkdown('~~删除文本~~')).toContain('<del>删除文本</del>');
|
|
});
|
|
|
|
it('无序列表渲染', () => {
|
|
const result = renderMarkdown('- 项目一\n- 项目二');
|
|
expect(result).toContain('<ul class="csk-md-ul">');
|
|
expect(result).toContain('<li>');
|
|
expect(result).toContain('项目一');
|
|
expect(result).toContain('项目二');
|
|
});
|
|
|
|
it('有序列表渲染', () => {
|
|
const result = renderMarkdown('1. 第一步\n2. 第二步');
|
|
expect(result).toContain('<ol class="csk-md-ol">');
|
|
expect(result).toContain('<li>');
|
|
});
|
|
|
|
it('引用渲染', () => {
|
|
const result = renderMarkdown('> 这是引用');
|
|
expect(result).toContain('<blockquote class="csk-md-blockquote">');
|
|
expect(result).toContain('这是引用');
|
|
});
|
|
|
|
it('链接渲染(http 协议)', () => {
|
|
const result = renderMarkdown('[百度](https://www.baidu.com)');
|
|
expect(result).toContain('href="https://www.baidu.com"');
|
|
expect(result).toContain('target="_blank"');
|
|
expect(result).toContain('rel="noopener noreferrer"');
|
|
});
|
|
|
|
it('水平线渲染', () => {
|
|
expect(renderMarkdown('---')).toContain('<hr class="csk-md-hr">');
|
|
});
|
|
});
|
|
|
|
describe('renderMarkdown - 代码', () => {
|
|
it('行内代码渲染', () => {
|
|
const result = renderMarkdown('使用 `console.log` 调试');
|
|
expect(result).toContain('<code class="csk-md-inline-code">console.log</code>');
|
|
});
|
|
|
|
it('代码块渲染', () => {
|
|
const result = renderMarkdown('```js\nconsole.log("hello");\n```');
|
|
expect(result).toContain('<pre class="csk-md-code-block">');
|
|
expect(result).toContain('class="language-js"');
|
|
expect(result).toContain('console.log');
|
|
});
|
|
|
|
it('代码块内容不被 Markdown 处理', () => {
|
|
const result = renderMarkdown('```\n**粗体** *斜体*\n```');
|
|
// 代码块内的 Markdown 语法不应被转换
|
|
expect(result).toContain('**粗体**');
|
|
expect(result).toContain('*斜体*');
|
|
expect(result).not.toContain('<strong>');
|
|
});
|
|
});
|
|
|
|
describe('renderMarkdown - XSS 防护', () => {
|
|
it('转义 HTML 标签', () => {
|
|
const result = renderMarkdown('<script>alert("xss")</script>');
|
|
expect(result).not.toContain('<script>');
|
|
expect(result).toContain('<script>');
|
|
});
|
|
|
|
it('转义 img 标签的 onerror', () => {
|
|
const result = renderMarkdown('<img src=x onerror=alert(1)>');
|
|
// < 被转义为 <,标签不会执行,onerror 变为纯文本
|
|
expect(result).not.toContain('<img');
|
|
expect(result).toContain('<img');
|
|
});
|
|
|
|
it('链接只允许 http/https 协议', () => {
|
|
const result = renderMarkdown('[恶意](javascript:alert(1))');
|
|
expect(result).toContain('href="#"');
|
|
expect(result).not.toContain('javascript:');
|
|
});
|
|
|
|
it('允许 http 链接', () => {
|
|
const result = renderMarkdown('[安全](http://example.com)');
|
|
expect(result).toContain('href="http://example.com"');
|
|
});
|
|
|
|
it('代码块内容同样被转义', () => {
|
|
const result = renderMarkdown('```\n<script>alert(1)</script>\n```');
|
|
expect(result).not.toContain('<script>alert');
|
|
expect(result).toContain('<script>');
|
|
});
|
|
|
|
it('行内代码内容同样被转义', () => {
|
|
const result = renderMarkdown('`<img src=x>`');
|
|
expect(result).toContain('<img');
|
|
expect(result).not.toContain('<img');
|
|
});
|
|
});
|
|
|
|
describe('renderMarkdown - 复合内容', () => {
|
|
it('混合标题和段落', () => {
|
|
const result = renderMarkdown('# 标题\n\n这是正文');
|
|
expect(result).toContain('<h1');
|
|
expect(result).toContain('<p class="csk-md-p">这是正文</p>');
|
|
});
|
|
|
|
it('多段落正确分隔', () => {
|
|
const result = renderMarkdown('第一段\n\n第二段');
|
|
expect(result).toContain('第一段');
|
|
expect(result).toContain('第二段');
|
|
// 应有两个 <p> 标签
|
|
const pCount = (result.match(/<p class="csk-md-p">/g) || []).length;
|
|
expect(pCount).toBe(2);
|
|
});
|
|
});
|