mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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
121 lines
4.8 KiB
TypeScript
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()
|
|
})
|
|
})
|