Files
makelore/tests/unit/coding-conversation-timeline.test.tsx

163 lines
5.5 KiB
TypeScript

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(
<CodingConversationTimeline conversationId="conversation-attachment" />,
);
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(<CodingConversationTimeline conversationId="conversation-window" />);
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(
<Profiler id="selected-timeline" onRender={(_id, _phase, duration) => commits.push(duration)}>
<CodingConversationTimeline conversationId="conversation-selected" />
</Profiler>,
);
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([]);
});
});