// 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'); }); });