mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
181 lines
6.9 KiB
TypeScript
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()
|
|
})
|
|
})
|