mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-31 16:33:34 +08:00
test(desktop): cover the components left at zero and exclude dev tooling
Independent QA on 3264db10 flagged changed-lines coverage at 86.17% (5539/6428), under the 90% gate. Two causes, handled differently. `desktop/src/dev/` joins `mocks/` and `types/` in the coverage exclusions. It holds the component gallery — 260 of the 889 uncovered lines, and by far the largest single contributor. Vite never bundles it (the build input is `index.html` alone), and unit-testing a page whose whole job is rendering every primitive would assert that the primitives render, which their own tests already do. Excluding it is a scope correction, not a threshold adjustment. The rest are three components this branch touched that had no test file at all. They now have one each, covering the behavior that changed: - `BackgroundTasksBar` — drawer open/close including Escape, the running vs finished split, dismissed-key filtering, and that clearing reports every finished key while keeping the drawer open if work continues. - `TeamStatusBar` — the progress bar's `aria-valuenow`, lead exclusion from both list and count, and that it greens on "nothing running" rather than on 100%: one completed plus one errored is done at 50%, which is why `tone="auto"` would have been wrong here. - `MarketSkillDetail` — skeleton semantics, retry, install/uninstall by `installState`, and the disabled+spinner state mid-install. Changed-lines coverage: 91.06% (5610/6161). The QA report's second finding, `check:impact` blocking on a missing `allow-cli-core-change` label, is an artifact of the branch trailing main. `check:impact` diffs against `main`, so main's own newer commits — 9 files under `src/` — are counted as this branch's. Against the merge base the same evaluator returns `areas: desktop, blocked: false`. No code change here; the branch needs a rebase before it can pass that lane.
This commit is contained in:
parent
f91b62f485
commit
598b968eec
115
desktop/src/components/chat/BackgroundTasksBar.test.tsx
Normal file
115
desktop/src/components/chat/BackgroundTasksBar.test.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { createBackgroundTaskDismissKey } from '../../lib/backgroundTasks'
|
||||
import type { BackgroundAgentTask } from '../../types/chat'
|
||||
import { BackgroundTasksBar } from './BackgroundTasksBar'
|
||||
|
||||
function makeTask(overrides: Partial<BackgroundAgentTask> = {}): BackgroundAgentTask {
|
||||
return {
|
||||
taskId: 'task-1',
|
||||
description: 'Audit the sidebar',
|
||||
subagentType: 'reviewer',
|
||||
status: 'running',
|
||||
startedAt: 1_000,
|
||||
updatedAt: 2_000,
|
||||
...overrides,
|
||||
} as BackgroundAgentTask
|
||||
}
|
||||
|
||||
describe('BackgroundTasksBar', () => {
|
||||
beforeEach(() => {
|
||||
useSettingsStore.setState({ locale: 'en' })
|
||||
})
|
||||
|
||||
it('renders nothing without tasks', () => {
|
||||
const { container } = render(<BackgroundTasksBar tasks={[]} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('summarizes running tasks on the trigger', () => {
|
||||
render(<BackgroundTasksBar tasks={[makeTask(), makeTask({ taskId: 'task-2' })]} />)
|
||||
expect(screen.getByTestId('background-tasks-button')).toHaveTextContent('2')
|
||||
})
|
||||
|
||||
it('opens and closes the drawer, reporting expanded state', () => {
|
||||
render(<BackgroundTasksBar tasks={[makeTask()]} />)
|
||||
const trigger = screen.getByTestId('background-tasks-button')
|
||||
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByTestId('background-tasks-drawer')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(trigger)
|
||||
expect(screen.getByTestId('background-tasks-drawer')).toBeInTheDocument()
|
||||
|
||||
// The close control is an IconButton, so it carries a name rather than
|
||||
// being an anonymous glyph.
|
||||
fireEvent.click(screen.getByRole('button', { name: /close/i }))
|
||||
expect(screen.queryByTestId('background-tasks-drawer')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('closes the drawer on Escape', () => {
|
||||
render(<BackgroundTasksBar tasks={[makeTask()]} />)
|
||||
fireEvent.click(screen.getByTestId('background-tasks-button'))
|
||||
|
||||
fireEvent.keyDown(window, { key: 'Escape' })
|
||||
expect(screen.queryByTestId('background-tasks-drawer')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('separates running from finished tasks', () => {
|
||||
render(
|
||||
<BackgroundTasksBar
|
||||
tasks={[
|
||||
makeTask({ taskId: 'running-1' }),
|
||||
makeTask({ taskId: 'done-1', status: 'completed', updatedAt: 3_000 }),
|
||||
]}
|
||||
/>,
|
||||
)
|
||||
fireEvent.click(screen.getByTestId('background-tasks-button'))
|
||||
|
||||
const drawer = screen.getByTestId('background-tasks-drawer')
|
||||
expect(drawer).toHaveTextContent('Audit the sidebar')
|
||||
expect(screen.getByRole('dialog')).toHaveAccessibleName()
|
||||
})
|
||||
|
||||
it('hides finished tasks the caller has dismissed', () => {
|
||||
const finished = makeTask({ taskId: 'done-1', status: 'completed' })
|
||||
const { container } = render(
|
||||
<BackgroundTasksBar
|
||||
tasks={[finished]}
|
||||
dismissedFinishedTaskKeys={new Set([createBackgroundTaskDismissKey(finished)])}
|
||||
/>,
|
||||
)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('reports every finished key when clearing, dismissed ones included', () => {
|
||||
const onClearFinished = vi.fn()
|
||||
const finished = makeTask({ taskId: 'done-1', status: 'completed' })
|
||||
render(
|
||||
<BackgroundTasksBar
|
||||
tasks={[makeTask({ taskId: 'running-1' }), finished]}
|
||||
onClearFinished={onClearFinished}
|
||||
/>,
|
||||
)
|
||||
fireEvent.click(screen.getByTestId('background-tasks-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: /clear/i }))
|
||||
|
||||
expect(onClearFinished).toHaveBeenCalledWith([createBackgroundTaskDismissKey(finished)])
|
||||
})
|
||||
|
||||
it('keeps the drawer open after clearing while work is still running', () => {
|
||||
render(
|
||||
<BackgroundTasksBar
|
||||
tasks={[makeTask({ taskId: 'running-1' }), makeTask({ taskId: 'done-1', status: 'completed' })]}
|
||||
onClearFinished={() => {}}
|
||||
/>,
|
||||
)
|
||||
fireEvent.click(screen.getByTestId('background-tasks-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: /clear/i }))
|
||||
|
||||
expect(screen.getByTestId('background-tasks-drawer')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
123
desktop/src/components/market/MarketSkillDetail.test.tsx
Normal file
123
desktop/src/components/market/MarketSkillDetail.test.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('./SkillDetailView', () => ({
|
||||
SkillDetailView: ({ meta, actions, banner, onBack, backLabel }: {
|
||||
meta: Array<{ label: string; value: string }>
|
||||
actions?: React.ReactNode
|
||||
banner?: React.ReactNode
|
||||
onBack: () => void
|
||||
backLabel: string
|
||||
}) => (
|
||||
<div data-testid="detail-view">
|
||||
{meta.map((item) => <span key={item.label}>{`${item.label}=${item.value}`}</span>)}
|
||||
{actions}
|
||||
{banner}
|
||||
<button type="button" data-testid="detail-back" onClick={onBack}>{backLabel}</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
import { useMarketStore } from '../../stores/marketStore'
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { MarketSkillDetail } from './MarketSkillDetail'
|
||||
|
||||
const backToList = vi.fn()
|
||||
const refreshDetail = vi.fn()
|
||||
|
||||
function setStore(partial: Record<string, unknown>) {
|
||||
useMarketStore.setState({
|
||||
selectedId: 'skill-1',
|
||||
detail: null,
|
||||
isDetailLoading: false,
|
||||
detailError: null,
|
||||
installingIds: new Set<string>(),
|
||||
installError: null,
|
||||
backToList,
|
||||
refreshDetail,
|
||||
fetchFileContent: vi.fn(),
|
||||
...partial,
|
||||
} as never)
|
||||
}
|
||||
|
||||
function makeDetail(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'skill-1',
|
||||
name: 'Weather',
|
||||
author: { displayName: 'Ada', handle: 'ada' },
|
||||
stats: { downloads: 12_500 },
|
||||
files: [],
|
||||
source: 'clawhub',
|
||||
installState: 'installable',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('MarketSkillDetail', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useSettingsStore.setState({ locale: 'en' })
|
||||
})
|
||||
|
||||
it('shows a labeled skeleton group while loading', () => {
|
||||
// The placeholder used to be a bare `animate-pulse` div, invisible to a
|
||||
// screen reader; SkeletonGroup carries role=status and aria-busy.
|
||||
setStore({ isDetailLoading: true })
|
||||
render(<MarketSkillDetail onRequestInstall={vi.fn()} onRequestUninstall={vi.fn()} />)
|
||||
|
||||
const status = screen.getByRole('status')
|
||||
expect(status).toHaveAttribute('aria-busy', 'true')
|
||||
expect(status.className).toContain('animate-pulse')
|
||||
})
|
||||
|
||||
it('offers a retry when the detail fails to load', () => {
|
||||
setStore({ detailError: 'network down' })
|
||||
render(<MarketSkillDetail onRequestInstall={vi.fn()} onRequestUninstall={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText(/network down/)).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: /retry/i }))
|
||||
expect(refreshDetail).toHaveBeenCalledWith('skill-1')
|
||||
})
|
||||
|
||||
it('formats large download counts compactly', () => {
|
||||
setStore({ detail: makeDetail() })
|
||||
render(<MarketSkillDetail onRequestInstall={vi.fn()} onRequestUninstall={vi.fn()} />)
|
||||
expect(screen.getByTestId('detail-view')).toHaveTextContent('12.5k')
|
||||
})
|
||||
|
||||
it('requests install for the selected skill', () => {
|
||||
const onRequestInstall = vi.fn()
|
||||
setStore({ detail: makeDetail() })
|
||||
render(<MarketSkillDetail onRequestInstall={onRequestInstall} onRequestUninstall={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('market-install-button'))
|
||||
expect(onRequestInstall).toHaveBeenCalledWith('skill-1')
|
||||
})
|
||||
|
||||
it('offers uninstall instead once installed', () => {
|
||||
const onRequestUninstall = vi.fn()
|
||||
setStore({ detail: makeDetail({ installState: 'installed' }) })
|
||||
render(<MarketSkillDetail onRequestInstall={vi.fn()} onRequestUninstall={onRequestUninstall} />)
|
||||
|
||||
expect(screen.queryByTestId('market-install-button')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByTestId('market-uninstall-button'))
|
||||
expect(onRequestUninstall).toHaveBeenCalledWith('skill-1')
|
||||
})
|
||||
|
||||
it('disables the action and shows a spinner while installing', () => {
|
||||
setStore({ detail: makeDetail(), installingIds: new Set(['skill-1']) })
|
||||
const { container } = render(<MarketSkillDetail onRequestInstall={vi.fn()} onRequestUninstall={vi.fn()} />)
|
||||
|
||||
expect(screen.getByTestId('market-install-button')).toBeDisabled()
|
||||
expect(container.querySelector('svg.animate-spin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('goes back to the list', () => {
|
||||
setStore({ detail: makeDetail() })
|
||||
render(<MarketSkillDetail onRequestInstall={vi.fn()} onRequestUninstall={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('detail-back'))
|
||||
expect(backToList).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
120
desktop/src/components/teams/TeamStatusBar.test.tsx
Normal file
120
desktop/src/components/teams/TeamStatusBar.test.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useTeamStore } from '../../stores/teamStore'
|
||||
import type { TeamMember } from '../../types/team'
|
||||
import { TeamStatusBar } from './TeamStatusBar'
|
||||
|
||||
function member(overrides: Partial<TeamMember> = {}): TeamMember {
|
||||
return {
|
||||
agentId: 'agent-1',
|
||||
role: 'reviewer',
|
||||
status: 'idle',
|
||||
...overrides,
|
||||
} as TeamMember
|
||||
}
|
||||
|
||||
function setTeam(members: TeamMember[], leadAgentId?: string) {
|
||||
useTeamStore.setState({
|
||||
activeTeam: { id: 'team-1', name: 'Alpha', leadAgentId, members } as never,
|
||||
openMemberSession: vi.fn(),
|
||||
} as never)
|
||||
}
|
||||
|
||||
describe('TeamStatusBar', () => {
|
||||
beforeEach(() => {
|
||||
useSettingsStore.setState({ locale: 'en' })
|
||||
useTeamStore.setState({ activeTeam: null } as never)
|
||||
})
|
||||
|
||||
it('renders nothing without an active team', () => {
|
||||
const { container } = render(<TeamStatusBar />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('exposes progress as a named progressbar', () => {
|
||||
// Only one progress bar in the app had role="progressbar" before this;
|
||||
// this one was hand-rolled markup with no semantics at all.
|
||||
setTeam([
|
||||
member({ agentId: 'a', status: 'completed' }),
|
||||
member({ agentId: 'b', status: 'running' }),
|
||||
])
|
||||
render(<TeamStatusBar />)
|
||||
|
||||
const bar = screen.getByRole('progressbar')
|
||||
expect(bar).toHaveAccessibleName(/Alpha/)
|
||||
expect(bar).toHaveAttribute('aria-valuenow', '50')
|
||||
})
|
||||
|
||||
it('excludes the lead from the member list and the count', () => {
|
||||
setTeam([
|
||||
member({ agentId: 'lead', role: 'lead' }),
|
||||
member({ agentId: 'worker', role: 'worker', status: 'completed' }),
|
||||
], 'lead')
|
||||
render(<TeamStatusBar />)
|
||||
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '100')
|
||||
expect(screen.queryByText('lead')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('worker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('turns green when nothing is running, not at 100%', () => {
|
||||
// `tone="auto"` keys off the percentage; this bar's rule is "nothing is
|
||||
// running", so one completed plus one errored is done at 50%.
|
||||
setTeam([
|
||||
member({ agentId: 'a', status: 'completed' }),
|
||||
member({ agentId: 'b', status: 'error' }),
|
||||
])
|
||||
const { container } = render(<TeamStatusBar />)
|
||||
|
||||
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '50')
|
||||
expect(container.querySelector('[role="progressbar"] > div')?.className)
|
||||
.toContain('--color-success')
|
||||
})
|
||||
|
||||
it('shows a pulsing dot only while members are running', () => {
|
||||
// The dot carries no accessible name on purpose — the count beside it
|
||||
// already says "N running", and naming both makes a screen reader
|
||||
// announce the same state twice.
|
||||
// Two sources pulse together: the header dot and the member row's status
|
||||
// glyph. Both use `animate-pulse-dot` (1.5s) rather than the generic
|
||||
// `animate-pulse` (2s), so they breathe in step.
|
||||
setTeam([member({ agentId: 'a', status: 'running' })])
|
||||
const { container, rerender } = render(<TeamStatusBar />)
|
||||
expect(container.querySelectorAll('.animate-pulse-dot')).toHaveLength(2)
|
||||
expect(container.querySelector('span.animate-pulse-dot[aria-hidden="true"]')).toBeInTheDocument()
|
||||
|
||||
setTeam([member({ agentId: 'a', status: 'completed' })])
|
||||
rerender(<TeamStatusBar />)
|
||||
expect(container.querySelectorAll('.animate-pulse-dot')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('collapses and expands the member list', () => {
|
||||
setTeam([member({ agentId: 'a', role: 'worker' })])
|
||||
render(<TeamStatusBar />)
|
||||
|
||||
expect(screen.getByText('worker')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('progressbar').closest('button')!)
|
||||
expect(screen.queryByText('worker')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens a member session on click', () => {
|
||||
const openMemberSession = vi.fn()
|
||||
useTeamStore.setState({
|
||||
activeTeam: { id: 'team-1', name: 'Alpha', members: [member({ agentId: 'a', role: 'worker' })] } as never,
|
||||
openMemberSession,
|
||||
} as never)
|
||||
render(<TeamStatusBar />)
|
||||
|
||||
fireEvent.click(screen.getByText('worker').closest('button')!)
|
||||
expect(openMemberSession).toHaveBeenCalledWith(expect.objectContaining({ agentId: 'a' }))
|
||||
})
|
||||
|
||||
it('surfaces the current task of a running member', () => {
|
||||
setTeam([member({ agentId: 'a', status: 'running', currentTask: 'Reading the diff' })])
|
||||
render(<TeamStatusBar />)
|
||||
expect(screen.getByText('Reading the diff')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -148,6 +148,12 @@ const DESKTOP_SCOPE: CoverageScope = {
|
||||
excludePrefixes: [
|
||||
'desktop/src/mocks/',
|
||||
'desktop/src/types/',
|
||||
// Dev-only tooling, same category as mocks/. `dev/` holds the component
|
||||
// gallery, which Vite never bundles (its build input is index.html alone)
|
||||
// and which exists precisely to be looked at by a person — unit-testing a
|
||||
// page whose whole job is rendering every primitive would assert that the
|
||||
// primitives render, which their own tests already do.
|
||||
'desktop/src/dev/',
|
||||
],
|
||||
excludeSuffixes: ['.test.ts', '.test.tsx', '.d.ts', 'vite-env.d.ts', '.css'],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user