import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { Profiler } from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { createProductSnapshot } from '../fixtures/coding-conversation-product-fixtures'; const attachmentApi = vi.hoisted(() => ({ load: vi.fn() })); vi.mock('@/lib/coding-attachments', () => ({ loadCodingAttachmentPreview: attachmentApi.load, })); describe('CodingConversationTimeline', () => { afterEach(() => vi.unstubAllGlobals()); it('resolves attachment refs into temporary object URLs without base64 state', async () => { const createObjectURL = vi.fn(() => 'blob:authenticated-preview'); const revokeObjectURL = vi.fn(); class TestURL extends URL {} TestURL.createObjectURL = createObjectURL; TestURL.revokeObjectURL = revokeObjectURL; vi.stubGlobal('URL', TestURL); attachmentApi.load.mockResolvedValue({ bytes: new Uint8Array([1, 2, 3]), mime: 'image/png', }); const { codingConversationStore } = await import('@/stores/coding-conversations'); const { CodingConversationTimeline } = await import( '@/pages/Chat/CodingConversationTimeline' ); const base = createProductSnapshot('conversation-attachment', 1); const snapshot = { ...base, nodes: [{ kind: 'message' as const, id: 'message-1', role: 'user' as const, status: 'complete' as const, blocks: [{ kind: 'image' as const, id: 'image-1', attachmentId: 'attachment-1', mime: 'image/png', }], }], }; codingConversationStore.getState().applySnapshotEvent({ type: 'snapshot', conversationId: 'conversation-attachment', workerGeneration: 1, seq: snapshot.cursor.seq, snapshot, }); const view = render( , ); await waitFor(() => expect(screen.getByRole('img', { name: '对话图片附件' })) .toHaveAttribute('src', 'blob:authenticated-preview')); expect(attachmentApi.load).toHaveBeenCalledWith('attachment-1'); expect(createObjectURL).toHaveBeenCalledOnce(); const blob = createObjectURL.mock.calls[0][0] as Blob; expect(blob.type).toBe('image/png'); expect(JSON.stringify(codingConversationStore.getState())).not.toContain('base64'); view.unmount(); expect(revokeObjectURL).toHaveBeenCalledWith('blob:authenticated-preview'); }); it('windows long timelines and loads earlier nodes only on demand', async () => { const { codingConversationStore } = await import('@/stores/coding-conversations'); const { CodingConversationTimeline } = await import( '@/pages/Chat/CodingConversationTimeline' ); const base = createProductSnapshot('conversation-window', 1); const snapshot = { ...base, nodes: Array.from({ length: 150 }, (_, index) => ({ kind: 'notice' as const, id: `notice-${index}`, code: `NOTICE_${index}`, level: 'info' as const, message: `Notice ${index}`, })), }; codingConversationStore.getState().applySnapshotEvent({ type: 'snapshot', conversationId: 'conversation-window', workerGeneration: 1, seq: snapshot.cursor.seq, snapshot, }); render(); expect(screen.queryByText('Notice 0')).not.toBeInTheDocument(); expect(screen.getByText('Notice 30')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '加载更早内容' })); expect(screen.getByText('Notice 0')).toBeInTheDocument(); }); it('does not commit the selected timeline when a hidden Conversation streams', async () => { const { codingConversationStore } = await import('@/stores/coding-conversations'); const { CodingConversationTimeline } = await import( '@/pages/Chat/CodingConversationTimeline' ); const selected = createProductSnapshot('conversation-selected', 1); const hidden = createProductSnapshot('conversation-hidden', 1); codingConversationStore.getState().applySnapshotEvent({ type: 'snapshot', conversationId: 'conversation-selected', workerGeneration: 1, seq: selected.cursor.seq, snapshot: selected, }); codingConversationStore.getState().applySnapshotEvent({ type: 'snapshot', conversationId: 'conversation-hidden', workerGeneration: 1, seq: hidden.cursor.seq, snapshot: hidden, }); const commits: number[] = []; render( commits.push(duration)}> , ); commits.length = 0; act(() => { codingConversationStore.getState().applyPatchBatchEvent({ type: 'patch-batch', conversationId: 'conversation-hidden', workerGeneration: 1, fromSeq: 1, toSeq: 1, items: [{ seq: 1, at: 1, patch: { op: 'message.upsert', node: { kind: 'message', id: 'hidden-message', role: 'assistant', status: 'streaming', blocks: [{ kind: 'text', id: 'hidden-text', text: 'hidden token', status: 'streaming', }], }, }, }], }); }); expect(commits).toEqual([]); }); });