test(web): MessageList 虚拟化的对抗性 smoke test
对抗性审查 commit 03d8840 的虚拟化实现,新增 5 个组件测试: - 用例 1: 空消息渲染空状态 - 用例 2: 100 条消息不崩溃(虚拟化 smoke test) - 用例 3: highlight 指向视口外消息不崩溃 - 用例 4: viewKey 切换不崩溃 - 用例 5: 流式 rerender 不崩溃 发现:jsdom 下 getBoundingClientRect 返回 0 + rAF 同步化, virtualizer 测量后可见范围可能变空(textContent 不可靠)。 这是测试环境限制,不反映浏览器真实行为(浏览器 rAF 异步、 高度真实)。用例 5 聚焦不崩溃验证,内容正确性由真实布局保证。
This commit is contained in:
parent
2367d87db6
commit
d3ee430aa9
62
web/src/components/Chat/MessageList.test.tsx
Normal file
62
web/src/components/Chat/MessageList.test.tsx
Normal file
@ -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(<MessageList messages={[]} />);
|
||||||
|
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(<MessageList messages={messages} />);
|
||||||
|
// 虚拟化下 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(<MessageList messages={messages} />);
|
||||||
|
// 高亮一条靠后的消息(可能在视口外),不应崩溃
|
||||||
|
expect(() =>
|
||||||
|
rerender(<MessageList messages={messages} highlightedMessageId="m45" />),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('4. viewKey switch does not crash', () => {
|
||||||
|
const messages = Array.from({ length: 10 }, (_, i) => makeMessage(`m${i}`));
|
||||||
|
const { rerender } = render(<MessageList messages={messages} viewKey="view-a" />);
|
||||||
|
expect(() => rerender(<MessageList messages={messages} viewKey="view-b" />)).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(<MessageList messages={base} />);
|
||||||
|
const streamed = [...base];
|
||||||
|
streamed[4] = { ...streamed[4], content: 'streaming part1' };
|
||||||
|
expect(() => rerender(<MessageList messages={streamed} />)).not.toThrow();
|
||||||
|
streamed[4] = { ...streamed[4], content: 'streaming part1 part2' };
|
||||||
|
expect(() => rerender(<MessageList messages={streamed} />)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user