cc-haha/desktop/src/components/chat/AssistantOutputTargetCard.test.tsx
程序员阿江(Relakkes) 7ca702a253 fix(desktop): simplify changed-file open-with menus
Changed-file rows are easier to scan when previewable files are grouped before source-only files, and the file open-with menu should only expose useful preview, editor, and reveal actions. Output target cards also no longer need a separate copy control now that open/open-with are the primary actions.

Constraint: Keep the existing open-with API and menu components shared across changed files, workspace context menus, and output target cards.
Rejected: Keep system-default open for files | it duplicated less predictable platform behavior and added a low-value final menu item.
Confidence: high
Scope-risk: narrow
Directive: File open-with menus should stay ordered by immediate preview/open usefulness before editor and reveal actions.
Tested: cd desktop && bun run test src/lib/openWithItems.test.ts src/components/workspace/WorkspaceFileOpenWith.test.tsx src/components/chat/AssistantOutputTargetCard.test.tsx src/components/chat/CurrentTurnChangeCard.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: git diff --check
2026-06-01 01:43:36 +08:00

121 lines
4.8 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',
}))
const ensureTargets = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
const openTargetFn = vi.hoisted(() => vi.fn())
vi.mock('../../stores/openTargetStore', () => ({
useOpenTargetStore: {
getState: () => ({ ensureTargets, targets: [], openTarget: openTargetFn }),
},
}))
const openPreviewFn = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
vi.mock('../../stores/workspacePanelStore', () => ({
useWorkspacePanelStore: {
getState: () => ({ statusBySession: {}, openPreview: openPreviewFn }),
},
}))
const shellOpen = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
vi.mock('@tauri-apps/plugin-shell', () => ({ open: shellOpen }))
// Mock i18n — return the key (plus interpolation) so we can assert on keys
vi.mock('../../i18n', () => ({
useTranslation: () => (k: string, v?: Record<string, string>) => (v?.target ? `${k}:${v.target}` : k),
}))
import { AssistantOutputTargetCard } from './AssistantOutputTargetCard'
import type { AssistantOutputTarget } from '../../lib/assistantOutputTargets'
const markdownTarget: AssistantOutputTarget = {
id: 'markdown:docs/readme.md',
kind: 'markdown',
title: 'readme.md',
subtitle: 'docs/readme.md',
href: 'docs/readme.md',
normalizedPath: 'docs/readme.md',
confidence: 'high',
source: 'markdown-link',
}
const localhostTarget: AssistantOutputTarget = {
id: 'localhost-url:http://localhost:5173/',
kind: 'localhost-url',
title: 'http://localhost:5173/',
href: 'http://localhost:5173/',
confidence: 'high',
source: 'plain-url',
}
afterEach(() => {
openBrowser.mockReset()
ensureTargets.mockReset().mockResolvedValue(undefined)
openTargetFn.mockReset()
openPreviewFn.mockReset().mockResolvedValue(undefined)
shellOpen.mockReset().mockResolvedValue(undefined)
})
describe('AssistantOutputTargetCard', () => {
it('renders a markdown target title + Markdown badge', () => {
render(<AssistantOutputTargetCard target={markdownTarget} sessionId="s1" />)
expect(screen.getByText('readme.md')).toBeInTheDocument()
expect(screen.getByText('assistantOutputs.kind.markdown')).toBeInTheDocument()
expect(screen.getByText('docs/readme.md')).toBeInTheDocument()
})
it('renders a localhost target title + Localhost badge (URL not duplicated)', () => {
render(<AssistantOutputTargetCard target={localhostTarget} sessionId="s1" />)
// subtitle equals the title for localhost, so the URL renders exactly once.
expect(screen.getAllByText('http://localhost:5173/')).toHaveLength(1)
expect(screen.getByText('assistantOutputs.kind.localhost')).toBeInTheDocument()
})
it('routes Open to workspace preview for a markdown target', () => {
render(<AssistantOutputTargetCard target={markdownTarget} sessionId="s1" />)
fireEvent.click(screen.getByLabelText('assistantOutputs.open'))
expect(openPreviewFn).toHaveBeenCalledWith('s1', 'docs/readme.md', 'file')
})
it('routes Open to the in-app browser for a localhost target', () => {
render(<AssistantOutputTargetCard target={localhostTarget} sessionId="s1" />)
fireEvent.click(screen.getByLabelText('assistantOutputs.open'))
expect(openBrowser).toHaveBeenCalledWith('s1', 'http://localhost:5173/')
})
it('does not render a copy button for output target cards', () => {
render(<AssistantOutputTargetCard target={markdownTarget} sessionId="s1" />)
expect(screen.queryByLabelText('assistantOutputs.copy')).not.toBeInTheDocument()
})
it('opens the open-with menu with URL items for a localhost target', async () => {
render(<AssistantOutputTargetCard target={localhostTarget} sessionId="s1" />)
fireEvent.click(screen.getByLabelText('openWith.title'))
expect(await screen.findByText('openWith.inAppBrowser')).toBeInTheDocument()
expect(screen.getByText('openWith.systemBrowser')).toBeInTheDocument()
})
it('re-clicking the same open-with trigger TOGGLES the menu closed', async () => {
render(<AssistantOutputTargetCard target={localhostTarget} sessionId="s1" />)
const trigger = screen.getByLabelText('openWith.title')
// 1st click → opens
fireEvent.click(trigger)
expect(await screen.findByText('openWith.inAppBrowser')).toBeInTheDocument()
// 2nd click on the SAME trigger → closes (toggle)
fireEvent.click(trigger)
expect(screen.queryByText('openWith.inAppBrowser')).not.toBeInTheDocument()
})
})