cc-haha/desktop/src/components/chat/ConversationNavigator.test.tsx
2026-07-11 14:48:23 +08:00

138 lines
4.7 KiB
TypeScript

import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useSettingsStore } from '../../stores/settingsStore'
import type { UIMessage } from '../../types/chat'
import {
buildConversationNavigationItems,
ConversationNavigator,
type ConversationNavigationSource,
} from './ConversationNavigator'
function source(message: UIMessage, renderIndex: number): ConversationNavigationSource {
return {
message,
renderIndex,
renderItemKey: message.id,
}
}
describe('buildConversationNavigationItems', () => {
it('keeps only visible user and assistant messages in transcript order', () => {
const items = buildConversationNavigationItems([
source({ id: 'user-1', type: 'user_text', content: ' Review the API ', timestamp: 1 }, 0),
source({ id: 'thinking-1', type: 'thinking', content: 'hidden', timestamp: 2 }, 1),
source({ id: 'assistant-empty', type: 'assistant_text', content: ' ', timestamp: 3 }, 2),
source({ id: 'assistant-1', type: 'assistant_text', content: '**API** review complete', timestamp: 4 }, 3),
source({ id: 'system-1', type: 'system', content: 'hidden', timestamp: 5 }, 4),
])
expect(items).toEqual([
{
id: 'user-1',
renderItemKey: 'user-1',
renderIndex: 0,
role: 'user',
preview: 'Review the API',
attachmentCount: 0,
},
{
id: 'assistant-1',
renderItemKey: 'assistant-1',
renderIndex: 3,
role: 'assistant',
preview: 'API review complete',
attachmentCount: 0,
},
])
})
it('counts user attachments and flattens markdown into preview text', () => {
const items = buildConversationNavigationItems([
source({
id: 'user-files',
type: 'user_text',
content: '> Please inspect [`MessageList`](https://example.com)\n\n```ts\nconst ready = true\n```',
timestamp: 1,
attachments: [
{ type: 'file', name: 'one.ts', mimeType: 'text/plain' },
{ type: 'file', name: 'two.ts', mimeType: 'text/plain' },
],
}, 0),
])
expect(items[0]).toMatchObject({
preview: 'Please inspect MessageList const ready = true',
attachmentCount: 2,
})
})
it('bounds previews for very long messages', () => {
const items = buildConversationNavigationItems([
source({ id: 'long', type: 'assistant_text', content: 'long answer '.repeat(200), timestamp: 1 }, 0),
])
expect(items[0]?.preview.length).toBeLessThanOrEqual(280)
expect(items[0]?.preview.endsWith('…')).toBe(true)
})
})
describe('ConversationNavigator', () => {
beforeEach(() => {
useSettingsStore.setState({ locale: 'en' })
})
it('renders ordered role markers and identifies the active target', () => {
render(
<ConversationNavigator
items={[
{ id: 'user-1', renderItemKey: 'user-1', renderIndex: 0, role: 'user', preview: 'First prompt', attachmentCount: 0 },
{ id: 'assistant-1', renderItemKey: 'assistant-1', renderIndex: 1, role: 'assistant', preview: 'First answer', attachmentCount: 0 },
]}
activeItemId="assistant-1"
onNavigate={vi.fn()}
/>,
)
const markers = screen.getAllByRole('button')
expect(markers.map((marker) => marker.getAttribute('data-role'))).toEqual(['user', 'assistant'])
expect(markers[0]?.getAttribute('aria-current')).toBeNull()
expect(markers[1]?.getAttribute('aria-current')).toBe('location')
})
it('shows the preview on hover or focus and navigates on click', () => {
const onNavigate = vi.fn()
const item = {
id: 'user-1',
renderItemKey: 'user-1',
renderIndex: 0,
role: 'user' as const,
preview: 'Inspect the virtual transcript',
attachmentCount: 2,
}
render(
<ConversationNavigator
items={[item]}
activeItemId="user-1"
onNavigate={onNavigate}
/>,
)
const marker = screen.getByRole('button', { name: /User message.*Inspect the virtual transcript/ })
expect(screen.queryByTestId('conversation-navigation-preview')).toBeNull()
fireEvent.mouseEnter(marker)
const preview = screen.getByTestId('conversation-navigation-preview')
expect(preview.parentElement).toBe(document.body)
expect(preview.textContent).toContain('User message')
expect(preview.textContent).toContain('Inspect the virtual transcript')
expect(preview.textContent).toContain('2')
fireEvent.mouseLeave(marker)
fireEvent.focus(marker)
expect(screen.getByTestId('conversation-navigation-preview')).toBeTruthy()
fireEvent.click(marker)
expect(onNavigate).toHaveBeenCalledWith(item)
})
})