// tests/unit/renderer/components/search/search-snippet-highlight.test.tsx // // 守 r194「搜索结果预览把 标签当普通文字显示」契约。 // // 现象:主进程 FTS5 的 snippet() 产出的是「命中片段包在 / // 里」的字符串;旧实现直接在结果行渲染 `{hit.snippet}` —— React 把它当 // 纯文本 ⇒ 用户看到的预览是 // // 本文介绍学习模型的基... // // 即标签的字面量,而不是高亮。命中预览是搜索面板里最显眼的输出,这个 // 缺陷等于「高亮功能从未生效」。 // // 本测锁三件事: // 1. 用户可见结果:命中片段渲染成真正的 元素,文本里不再出现 // "" 字面量; // 2. 安全:片段正文来自用户写的笔记,渲染路径**不得**把它当 HTML 解析 // (dangerouslySetInnerHTML)—— 笔记里的 "结尾命中'; const { container } = render(); // 没有任何 script 元素落进 DOM expect(container.querySelector('script')).toBeNull(); // 内容以文本形式完整保留 expect(container.textContent).toContain(''); expect(container.querySelectorAll('mark')).toHaveLength(1); }); it('空片段 → 不渲染任何内容且不崩', () => { const { container } = render(); expect(container.textContent).toBe(''); }); }); // === 面板级:用户实际看到的东西 === describe('SearchPanel 命中行(r194)', () => { const hit = { kind: 'note' as const, id: 'note-1', relPath: 'a.md', title: '深度学习综述', snippet: MARKED, score: 1, }; beforeEach(() => { (window as unknown as { api: unknown }).api = { search: vi.fn().mockResolvedValue({ ok: true, data: [hit] }), }; }); afterEach(() => { cleanup(); }); it('命中行渲染出 元素,预览里没有标签字面量', async () => { const { SearchPanel } = await import( '../../../../../src/renderer/src/components/search/SearchPanel' ); render( {}} />); // 输入查询词(面板 300ms debounce 后才发 IPC) // Overlay 走 Portal 渲染到 document.body —— input 不在 container 里 const input = document.querySelector('input')!; fireEvent.change(input, { target: { value: '学习' } }); await waitFor( () => { expect(document.querySelectorAll('mark').length).toBeGreaterThan(0); }, { timeout: 3000 }, ); expect(document.querySelector('mark')!.textContent).toBe('学习'); // 整个面板的可读文本里都不该出现标签字面量 expect(document.body.textContent).not.toContain(''); expect(document.body.textContent).not.toContain(''); }); });