Files
pure-note/web/tests/markdown.test.tsx
T
wangairnan e9316c9169 前端:React 19 + Vite 8 + Tailwind 4 博客与管理后台 SPA
- 渲染管线(§8.2):react-markdown + remark-gfm + rehype-sanitize
  (GitHub schema 扩展 hljs class 与 checked)+ rehype-highlight 纯 class
  高亮;链接强制 target=_blank + rel=nofollow noopener noreferrer;
  schema 快照与恶意 Markdown 冒烟测试(vitest 6 用例)
- 编辑器:CodeMirror 6 源码编辑 + 分屏实时预览 + 工具栏 +
  粘贴/拖拽图片上传;slug 首存定稿(冲突 409 就地高亮)、
  摘要留空自动截取、2s 防抖自动保存
- 页面:博客首页(置顶/分页/标签云)、详情(面包屑/上一篇下一篇)、
  标签页、登录、管理列表/回收站/设置/改密、404
- lib:api 客户端(统一包络 + 内存态 CSRF,禁 localStorage)、
  AuthContext(/api/me 重取)、暗色主题(class 切换 + 系统跟随)
- index.html 内嵌 Go template 占位符供服务端 meta 注入;
  vite base 固定 '/'(防深链白屏);/api 代理 127.0.0.1:8080
2026-09-08 08:14:52 +08:00

75 lines
2.9 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.
// 渲染管线安全测试(§13 前端测试组):
// 1) sanitize schema 快照:禁 script/iframe/style/事件属性,允许任务列表 checkbox;
// 2) 对恶意 Markdown 的冒烟用例:渲染输出无脚本执行面。
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import MarkdownViewer from '../src/components/MarkdownViewer'
import { sanitizeSchema } from '../src/lib/sanitize'
const tagNames = (sanitizeSchema as { tagNames?: string[] }).tagNames ?? []
describe('sanitize schema 白名单', () => {
it('禁止危险标签', () => {
for (const dangerous of ['script', 'iframe', 'object', 'embed', 'form', 'style', 'link', 'meta']) {
expect(tagNames).not.toContain(dangerous)
}
})
it('允许任务列表 checkbox(input type=checkbox + checked/disabled)', () => {
const attrs = (sanitizeSchema as { attributes: Record<string, unknown> }).attributes
expect(attrs.input).toBeDefined()
})
})
describe('渲染管线对恶意 Markdown 的冒烟', () => {
const malicious = [
'<script>window.__xss=1</script>',
'<img src=x onerror="window.__xss=1">',
'[click](javascript:alert(1))',
'<iframe src="https://evil.example"></iframe>',
'<a href="javascript:alert(1)">x</a>',
'<div style="background:url(javascript:alert(1))">x</div>',
'<svg><script>alert(1)</script></svg>',
].join('\n\n')
it('输出不包含任何脚本/事件/危险协议', () => {
const { container } = render(<MarkdownViewer source={malicious} />)
expect(window.__xss).toBeUndefined()
expect(container.querySelector('script')).toBeNull()
expect(container.querySelector('iframe')).toBeNull()
expect(container.querySelectorAll('[onclick], [onerror], [onload]')).toHaveLength(0)
container.querySelectorAll('a').forEach((a) => {
const href = a.getAttribute('href') ?? ''
expect(href.startsWith('javascript:')).toBe(false)
})
expect(container.innerHTML.includes('style=')).toBe(false)
})
it('任务列表渲染为 disabled checkbox', () => {
const { container } = render(<MarkdownViewer source={'- [x] 完成\n- [ ] 待办'} />)
const boxes = container.querySelectorAll('input[type="checkbox"]')
expect(boxes.length).toBe(2)
boxes.forEach((b) => expect(b.hasAttribute('disabled')).toBe(true))
})
it('普通 GFM(表格/删除线)正常渲染', () => {
const { container } = render(
<MarkdownViewer source={'| a | b |\n| --- | --- |\n| 1 | 2 |\n\n~~删除~~'} />,
)
expect(container.querySelector('table')).not.toBeNull()
expect(container.querySelector('del')).not.toBeNull()
})
it('不泄漏 react-markdown 的 node prop 到 DOM', () => {
const { container } = render(<MarkdownViewer source={'- [x] 完成\n\n[链接](https://example.com)'} />)
expect(container.innerHTML.includes('node=')).toBe(false)
})
})
// 供断言使用
declare global {
interface Window {
__xss?: number
}
}