cc-haha/desktop/src/components/chat/AssistantMessage.linkrouting.test.tsx
程序员阿江(Relakkes) d7d7cffddb feat(desktop): render AI-output videos inline as <video> (mp4/webm/mov)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:40:46 +08:00

181 lines
6.9 KiB
TypeScript

import '@testing-library/jest-dom'
import { fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
const { openBrowser } = vi.hoisted(() => ({ openBrowser: vi.fn() }))
vi.mock('../../stores/browserPanelStore', () => ({
useBrowserPanelStore: { getState: () => ({ open: openBrowser }) },
}))
vi.mock('../../lib/desktopRuntime', async (orig) => ({
...(await orig<Record<string, unknown>>()),
getServerBaseUrl: () => 'http://127.0.0.1:4321',
}))
// Mock openTargetStore for the open-with menu (used by the cards)
const ensureTargets = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
const openTargetFn = vi.hoisted(() => vi.fn())
vi.mock('../../stores/openTargetStore', () => ({
useOpenTargetStore: {
getState: () => ({ ensureTargets, targets: [], openTarget: openTargetFn }),
},
}))
// Mock workspacePanelStore — usable both as a hook selector and via getState().
// workDir is undefined (no active workspace) so relative paths resolve as-is.
const openPreviewFn = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
vi.mock('../../stores/workspacePanelStore', () => {
const state = { statusBySession: {} as Record<string, { workDir?: string } | undefined>, openPreview: openPreviewFn }
const useWorkspacePanelStore = Object.assign(
(selector: (s: typeof state) => unknown) => selector(state),
{ getState: () => state },
)
return { useWorkspacePanelStore }
})
// Mock tauri shell (used by openSystem inside the card's open-with)
const shellOpen = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
vi.mock('@tauri-apps/plugin-shell', () => ({ open: shellOpen }))
// Mock i18n — return the key as the label so we can assert on keys
vi.mock('../../i18n', () => ({
useTranslation: () => (k: string, v?: Record<string, string>) => (v?.target ? `${k}:${v.target}` : k),
}))
// Mock settingsStore (safety net for transitive i18n usage)
vi.mock('../../stores/settingsStore', () => ({
useSettingsStore: Object.assign((sel: (s: { locale: string }) => unknown) => sel({ locale: 'en' }), {
getState: () => ({ locale: 'en' }),
subscribe: () => () => {},
}),
}))
import { AssistantMessage } from './AssistantMessage'
afterEach(() => {
openBrowser.mockReset()
ensureTargets.mockReset().mockResolvedValue(undefined)
openTargetFn.mockReset()
openPreviewFn.mockReset().mockResolvedValue(undefined)
})
describe('AssistantMessage link routing', () => {
it('opens a localhost link in the in-app browser on left-click', () => {
render(<AssistantMessage sessionId="s1" content={'打开 [预览](http://localhost:5173/)'} />)
// The clickable element is the rendered markdown anchor (the card title is a span).
fireEvent.click(screen.getByRole('link', { name: '预览' }))
expect(openBrowser).toHaveBeenCalledWith('s1', 'http://localhost:5173/')
})
})
describe('AssistantMessage output-target cards', () => {
it('renders a card for a localhost URL after streaming ends', () => {
render(
<AssistantMessage
sessionId="s1"
content={'本地服务运行在 http://localhost:5173/ 上'}
isStreaming={false}
/>,
)
expect(screen.getAllByText('http://localhost:5173/').length).toBeGreaterThan(0)
expect(screen.getByText('assistantOutputs.kind.localhost')).toBeInTheDocument()
})
it('renders a card for a markdown link with its Markdown badge', () => {
render(
<AssistantMessage
sessionId="s1"
content={'见 [说明文档](docs/readme.md)'}
isStreaming={false}
/>,
)
// Link text appears in both the bubble anchor and the card title; the badge is unique.
expect(screen.getByRole('link', { name: '说明文档' })).toBeInTheDocument()
expect(screen.getByText('assistantOutputs.kind.markdown')).toBeInTheDocument()
})
it('renders a relative image inline (an <img>) but NOT as an image card', () => {
render(
<AssistantMessage
sessionId="s1"
content={'渲染结果见 outputs/foo/preview_frame.png'}
isStreaming={false}
/>,
)
// Image renders inline through InlineImageGallery (workDir is undefined in this
// test's mock, so the relative path resolves as-is and is served via /preview-fs).
const img = screen.getByRole('img') as HTMLImageElement
expect(img.getAttribute('src')).toBe(
'http://127.0.0.1:4321/preview-fs/s1/outputs/foo/preview_frame.png',
)
// ...and is NOT duplicated as an output-target card.
expect(screen.queryByText('assistantOutputs.kind.image')).toBeNull()
})
it('renders a relative video inline (a <video>) but NOT as a card', () => {
const { container } = render(
<AssistantMessage
sessionId="s1"
content={'生成的视频见 outputs/demo.mp4'}
isStreaming={false}
/>,
)
// Video renders inline through InlineVideoGallery (workDir is undefined in this
// test's mock, so the relative path resolves as-is and is served via /preview-fs).
const video = container.querySelector('video') as HTMLVideoElement
expect(video).not.toBeNull()
expect(video.getAttribute('src')).toBe('http://127.0.0.1:4321/preview-fs/s1/outputs/demo.mp4')
// ...and is NOT duplicated as an output-target card (no extra open/copy controls).
expect(screen.queryByText('assistantOutputs.kind.image')).toBeNull()
})
it('still renders md/html/localhost cards when those references are present', () => {
render(
<AssistantMessage
sessionId="s1"
content={[
'本地服务 http://localhost:5173/',
'见 [说明](docs/readme.md)',
'页面 [首页](out/index.html)',
].join('\n')}
isStreaming={false}
/>,
)
expect(screen.getByText('assistantOutputs.kind.localhost')).toBeInTheDocument()
expect(screen.getByText('assistantOutputs.kind.markdown')).toBeInTheDocument()
expect(screen.getByText('assistantOutputs.kind.html')).toBeInTheDocument()
})
it('does NOT render cards while streaming', () => {
render(
<AssistantMessage
sessionId="s1"
content={'本地服务运行在 http://localhost:5173/ 上'}
isStreaming={true}
/>,
)
expect(screen.queryByText('assistantOutputs.kind.localhost')).toBeNull()
})
it('does NOT render cards when there are no previewable references', () => {
render(
<AssistantMessage
sessionId="s1"
content={'装一下 `npm install` 然后看 [anchor](#section)'}
isStreaming={false}
/>,
)
expect(screen.queryByText('assistantOutputs.kind.localhost')).toBeNull()
expect(screen.queryByText('Markdown')).toBeNull()
})
it('does NOT render cards when sessionId is absent', () => {
render(
<AssistantMessage
content={'本地服务运行在 http://localhost:5173/ 上'}
isStreaming={false}
/>,
)
expect(screen.queryByText('assistantOutputs.kind.localhost')).toBeNull()
})
})