// tests/unit/renderer/reader/markdown-reader.test.tsx
//
// 守 MarkdownReader 的行为契约
// (src/renderer/src/components/reader/MarkdownReader.tsx 头注指向本文件,
// 原先标 🔒(守卫缺失);其中「安全前置」一条是源码注释**点名**要守的)。
//
// 组件做什么:markdown-it 渲染 → 字符串层净化 → dangerouslySetInnerHTML。
// 用户可见的结果分三块:
// 1. 渲染 —— 标题 / 列表 / 粗体 / 代码块 / 表格都变成真的 HTML 元素;
// 文档第一行的 # 标题额外给 SR 一个隐藏 h1;容器带 filename 的 aria-label。
// 2. 【安全前置】—— markdown-it 实例必须带 `html: false`:
// 正文里的原始 HTML **不被解析**(只作为文本显示),
// 否则 \n\n
\n'}
filename="a.md"
/>,
);
await waitForRendered(container);
// DOM 里不得出现脚本 / 图片元素
expect(container.querySelector('script')).toBeNull();
expect(container.querySelector('img')).toBeNull();
// 字面量以文本形式完整保留(用户能看见自己写的东西)
expect(container.textContent).toContain('');
expect(container.textContent).toContain('onerror');
});
it('危险 scheme 不会变成可点链接,原文仍可见', async () => {
const { container } = render(
,
);
await waitForRendered(container);
// 渲染结果里只有**安全**的那个链接(markdown-it 的 validateLink 先挡一道,
// 字符串净化再兜一道 —— 两道都在 ⇒ 用户笔记里的危险链接永远点不动)
const hrefs = Array.from(container.querySelectorAll('article a')).map((a) =>
(a.getAttribute('href') ?? '').toLowerCase(),
);
expect(hrefs).toEqual(['https://example.com']);
for (const href of hrefs) {
expect(href).not.toMatch(/^(javascript|vbscript|data|file):/);
}
// 且原文没有被静默吞掉(用户能看到自己写了什么)
expect(container.textContent).toContain('javascript:alert(1)');
expect(container.textContent).toContain('javascript:alert(2)');
});
it('裸 URL 被自动链接化,http(s) 链接保持原样', async () => {
const { container } = render(
,
);
await waitForRendered(container);
const anchor = container.querySelector('article a')!;
expect(anchor.getAttribute('href')).toBe('https://example.com');
});
});
describe('MarkdownReader:空态与失败态', () => {
it('空白正文 → 明确空态提示,而不是一个空 article', async () => {
const { container } = render();
await waitFor(() => expect(screen.getByText('reader.emptyDoc')).toBeTruthy());
expect(container.querySelector('article')).toBeNull();
});
it('markdown-it 初始化失败 → 错误态提示,不是永久白屏', async () => {
// 真实故障形态:chunk 到了但实例化炸了(asar hash 不符 / 沙盒环境)。
// 工厂返回「构造即抛」的类,错误信息能原样传到 UI 上。
vi.doMock('markdown-it', () => ({
default: class {
constructor() {
throw new Error('chunk load failed');
}
},
}));
const { container } = render();
await waitFor(() =>
expect(screen.getByText('reader.errorMarkdownLoaderReaderTitle')).toBeTruthy(),
);
expect(container.querySelector('article')).toBeNull();
// 错误原因对用户可见(可截图报障),不是空壳
expect(container.textContent).toContain('chunk load failed');
vi.doUnmock('markdown-it');
});
});