From d3ee430aa91eff6ec0e5f4590708f20528b84b9b Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Fri, 7 Aug 2026 07:14:15 +0800 Subject: [PATCH] =?UTF-8?q?test(web):=20MessageList=20=E8=99=9A=E6=8B=9F?= =?UTF-8?q?=E5=8C=96=E7=9A=84=E5=AF=B9=E6=8A=97=E6=80=A7=20smoke=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对抗性审查 commit 03d8840 的虚拟化实现,新增 5 个组件测试: - 用例 1: 空消息渲染空状态 - 用例 2: 100 条消息不崩溃(虚拟化 smoke test) - 用例 3: highlight 指向视口外消息不崩溃 - 用例 4: viewKey 切换不崩溃 - 用例 5: 流式 rerender 不崩溃 发现:jsdom 下 getBoundingClientRect 返回 0 + rAF 同步化, virtualizer 测量后可见范围可能变空(textContent 不可靠)。 这是测试环境限制,不反映浏览器真实行为(浏览器 rAF 异步、 高度真实)。用例 5 聚焦不崩溃验证,内容正确性由真实布局保证。 --- web/src/components/Chat/MessageList.test.tsx | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 web/src/components/Chat/MessageList.test.tsx diff --git a/web/src/components/Chat/MessageList.test.tsx b/web/src/components/Chat/MessageList.test.tsx new file mode 100644 index 0000000..685d69c --- /dev/null +++ b/web/src/components/Chat/MessageList.test.tsx @@ -0,0 +1,62 @@ +import { render } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { MessageList } from './MessageList'; +import type { ChatMessage } from '../../types/protocol'; + +function makeMessage( + id: string, + role: 'user' | 'assistant' = 'assistant', + content = 'hello', +): ChatMessage { + return { + id, + role, + content, + timestamp: Math.floor(Date.now() / 1000), + type: 'message', + }; +} + +describe('MessageList (virtualized)', () => { + it('1. renders empty state when no messages', () => { + const { getByText } = render(); + expect(getByText('开始新的对话')).toBeDefined(); + }); + + it('2. renders without crash with many messages (virtualization smoke test)', () => { + const messages = Array.from({ length: 100 }, (_, i) => + makeMessage(`m${i}`, 'assistant', `content ${i}`), + ); + const { container } = render(); + // 虚拟化下 DOM 节点数应远少于 100(jsdom 下可能渲染部分或全部,但不崩溃) + expect(container.firstChild).not.toBeNull(); + }); + + it('3. highlightedMessageId does not crash when target is virtualized out', () => { + const messages = Array.from({ length: 50 }, (_, i) => makeMessage(`m${i}`)); + const { rerender } = render(); + // 高亮一条靠后的消息(可能在视口外),不应崩溃 + expect(() => + rerender(), + ).not.toThrow(); + }); + + it('4. viewKey switch does not crash', () => { + const messages = Array.from({ length: 10 }, (_, i) => makeMessage(`m${i}`)); + const { rerender } = render(); + expect(() => rerender()).not.toThrow(); + }); + + it('5. streaming (content grows on rerender) does not crash', () => { + // 注意:jsdom 下 getBoundingClientRect 返回 0 + rAF 同步化,virtualizer 测量后 + // 可见范围可能变空(textContent 不可靠)。本用例只验证流式 rerender 不崩溃, + // 内容渲染的正确性由浏览器真实布局保证,jsdom 无法可靠断言。 + const base = Array.from({ length: 5 }, (_, i) => makeMessage(`m${i}`)); + const { rerender } = render(); + const streamed = [...base]; + streamed[4] = { ...streamed[4], content: 'streaming part1' }; + expect(() => rerender()).not.toThrow(); + streamed[4] = { ...streamed[4], content: 'streaming part1 part2' }; + expect(() => rerender()).not.toThrow(); + }); +});