test(desktop): guard the mock paths and update suites for the new markup

`mockPaths.test.ts` asserts every `vi.mock` specifier resolves. A mock
pointing at a moved module is not an error — Vitest registers the factory
against a specifier nothing imports, the real module loads, and the test
keeps passing with its isolation quietly gone. That is what happened to
`RepositoryLaunchControls.test.tsx`, which kept mocking `./DirectoryPicker`
after the component moved to `composite/`, so the real picker and its API
calls rendered in a test that believed they were stubbed.

Suite updates for markup that legitimately changed shape:

- Dropdown entries are `role="option"` now, not buttons. `<button>` is
  invalid inside a `role="listbox"` and left the dropdown without arrow
  keys; the assertions follow the corrected semantics.
- `PetSettings`' toggles report `role="switch"`, the ARIA role for a
  toggle. They were bare checkboxes while `McpSettings`' equivalent was
  already a switch — the two are now consistent.
- `pages.test.tsx` drops its references to the two deleted mock pages.
This commit is contained in:
程序员阿江(Relakkes) 2026-07-26 16:03:35 +08:00
parent f64745e569
commit 59b93f9bca
4 changed files with 63 additions and 31 deletions

View File

@ -68,7 +68,7 @@ vi.mock('../api/providers', () => ({
}))
vi.mock('../lib/desktopNotifications', () => desktopNotificationsMock)
vi.mock('../components/chat/clipboard', () => clipboardMock)
vi.mock('@/lib/clipboard', () => clipboardMock)
vi.mock('@tauri-apps/api/core', () => tauriCoreMock)
vi.mock('@tauri-apps/plugin-dialog', () => tauriDialogMock)
vi.mock('@tauri-apps/plugin-process', () => tauriProcessMock)
@ -883,7 +883,7 @@ describe('Settings > General tab', () => {
const trigger = screen.getByRole('button', { name: 'Response Language' })
expect(trigger).toHaveTextContent('Default (English)')
fireEvent.click(trigger)
fireEvent.click(screen.getByRole('button', { name: '中文 (Chinese)' }))
fireEvent.click(screen.getByRole('option', { name: '中文 (Chinese)' }))
expect(useSettingsStore.getState().setResponseLanguage).toHaveBeenCalledWith('chinese')
})
@ -1724,8 +1724,9 @@ describe('Settings > Providers tab', () => {
expect(within(dialog).queryByRole('combobox')).not.toBeInTheDocument()
fireEvent.click(within(dialog).getByRole('button', { name: /Anthropic Messages \(native\)/i }))
fireEvent.click(within(dialog).getByRole('button', { name: /OpenAI Responses API \(proxy\)/i }))
fireEvent.click(within(dialog).getByRole('option', { name: /OpenAI Responses API \(proxy\)/i }))
// The panel is closed now; this finds the trigger, which reflects the pick.
expect(within(dialog).getByRole('button', { name: /OpenAI Responses API \(proxy\)/i })).toBeInTheDocument()
expect(within(dialog).getByText('Requests will be translated via the local proxy')).toBeInTheDocument()
})

View File

@ -0,0 +1,57 @@
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
/**
* A `vi.mock` whose path does not resolve is not an error Vitest registers
* the factory against a specifier nothing imports, the real module loads, and
* the test keeps passing with its isolation quietly gone.
*
* That is exactly what happened when components moved out of `shared/`:
* `RepositoryLaunchControls.test.tsx` kept mocking `./DirectoryPicker` while
* the component had moved to `@/components/composite/DirectoryPicker`, so the
* real picker (and its API calls) rendered in a test that believed it was
* stubbed.
*/
const SRC = join(process.cwd(), 'src')
function collectTests(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
if (entry === 'node_modules' || entry.startsWith('.')) continue
const path = join(dir, entry)
if (statSync(path).isDirectory()) collectTests(path, out)
else if (/\.test\.tsx?$/.test(entry)) out.push(path)
}
return out
}
/** Mirrors the resolution in vite.config.ts: `@/` maps to `src/`. */
function resolvesToFile(specifier: string, fromFile: string): boolean {
let base: string
if (specifier.startsWith('@/')) base = join(SRC, specifier.slice(2))
else if (specifier.startsWith('.')) base = resolve(dirname(fromFile), specifier)
else return true // bare package specifier — node_modules, not our concern
return ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '']
.some((suffix) => existsSync(base + suffix))
}
describe('vi.mock specifiers', () => {
it('all resolve to a real module', () => {
const broken: string[] = []
for (const file of collectTests(SRC)) {
readFileSync(file, 'utf8').split('\n').forEach((line, index) => {
const match = line.match(/vi\.mock\(\s*['"]([^'"]+)['"]/)
if (!match) return
if (!resolvesToFile(match[1]!, file)) {
broken.push(`${file.replace(SRC, 'src')}:${index + 1} vi.mock('${match[1]}')`)
}
})
}
expect(broken).toEqual([])
})
})

View File

@ -75,9 +75,7 @@ vi.mock('../api/sessions', async (importOriginal) => {
// Import all pages
import { EmptySession } from '../pages/EmptySession'
import { ActiveSession } from '../pages/ActiveSession'
import { AgentTeams } from '../pages/AgentTeams'
import { ScheduledTasks } from '../pages/ScheduledTasks'
import { ToolInspection } from '../pages/ToolInspection'
// Layout components (chrome is now here, not in pages)
import { Sidebar } from '../components/layout/Sidebar'
@ -1243,25 +1241,11 @@ describe('Content-only pages render without errors', () => {
useSessionRuntimeStore.setState({ selections: {} })
})
it('AgentTeams renders team strip and members', () => {
const { container } = render(<AgentTeams />)
expect(container.innerHTML).toContain('Architect')
expect(container.innerHTML).toContain('session-dev')
expect(container.innerHTML).toContain('groups')
})
it('ScheduledTasks renders (store-connected)', async () => {
const { container } = render(<ScheduledTasks />)
await screen.findByText('Scheduled tasks')
expect(container.innerHTML).toContain('Scheduled tasks')
})
it('ToolInspection renders diff viewer', () => {
const { container } = render(<ToolInspection />)
expect(container.innerHTML).toContain('edit_file')
expect(container.innerHTML).toContain('Split')
expect(container.innerHTML).toContain('Unified')
})
})
describe('Chat attachments', () => {
@ -1301,7 +1285,7 @@ describe('AppShell layout renders chrome', () => {
describe('Design system compliance', () => {
it('Pages use Material Symbols Outlined icons', () => {
const pages = [EmptySession, AgentTeams, ToolInspection]
const pages = [EmptySession]
for (const Page of pages) {
const { container, unmount } = render(<Page />)
const icons = container.querySelectorAll('.material-symbols-outlined')
@ -1326,13 +1310,3 @@ describe('Design system compliance', () => {
}
})
})
describe('Mock data integration', () => {
it('AgentTeams shows team members from mock data', () => {
const { container } = render(<AgentTeams />)
expect(container.innerHTML).toContain('Architect')
expect(container.innerHTML).toContain('Frontend Dev')
expect(container.innerHTML).toContain('Backend Dev')
expect(container.innerHTML).toContain('Tester')
})
})

View File

@ -6,7 +6,7 @@ import 'katex/dist/katex.min.css'
import { marked, type Tokens } from 'marked'
import { CodeViewer } from '../chat/CodeViewer'
import { MermaidRenderer } from '../chat/MermaidRenderer'
import { copyTextToClipboard } from '../chat/clipboard'
import { copyTextToClipboard } from '@/lib/clipboard'
type Props = {
content: string