test(client): cover menu dismiss, goal pause, and permission label paths

Close the per-file coverage gaps the new UI behavior introduced: MenuView
pointer-outside dismiss (all guard branches), the GoalBar pause action,
the ui-slash injected onDismiss face, and the non-kebab permission name
passthrough.
This commit is contained in:
Yif
2026-07-29 19:59:42 +08:00
parent 45c9205caf
commit 82a43a7240
4 changed files with 57 additions and 0 deletions

View File

@@ -104,6 +104,13 @@ describe('GoalBar', () => {
expect(screen.getByRole('textbox', { name: 'Goal objective' })).toBeTruthy()
})
it('active goal: the pause action pauses', () => {
const actions = makeActions()
render(<GoalBar goal={makeGoal()} {...actions} />)
fireEvent.click(screen.getByRole('button', { name: 'Pause goal' }))
expect(actions.onPause).toHaveBeenCalledTimes(1)
})
it('paused goal: "Paused Goal" with a resume action before edit', () => {
const actions = makeActions()
render(<GoalBar goal={makeGoal({ phase: 'paused' })} {...actions} />)

View File

@@ -85,6 +85,11 @@ describe('ui-permission browser plugin', () => {
const again = await c.ui.options(proj, new AbortController().signal)
expect(again.find(option => option.id === 'workspace-write')?.active).toBe(true)
expect(again.find(option => option.id === 'read-only')?.detail).toBe('Reads only.')
// Kebab-case names title-case; non-kebab host-configured names pass through.
expect(again.map(option => option.label)).toEqual(['Read Only', 'Workspace Write', 'Danger Full Access'])
b.values.set(sid('s1'), { ...SELECT, options: [{ value: 'plain', name: 'Ask Every Time' }] })
const passthrough = await c.ui.options(proj, new AbortController().signal)
expect(passthrough[0]?.label).toBe('Ask Every Time')
// A projection that vanished between availability and open throws.
expect(() => c.ui.options({ sessionId: sid('ghost') }, new AbortController().signal))
.toThrow(/not available on this host/)

View File

@@ -84,6 +84,9 @@ describe('apply', () => {
// The pick face routes into the controller pipeline (closed menu → no-op).
injected.onPick('command', 0)
expect(controller.menu.getSnapshot().open).toBe(false)
// The dismiss face routes into the controller too (closed menu → no-op).
injected.onDismiss()
expect(controller.menu.getSnapshot().open).toBe(false)
// An unknown session id fails loud (no silent scope miss).
expect(() => injectEntry(sid('ghost'))).toThrow(/resolved no scope/)
})

View File

@@ -142,6 +142,48 @@ describe('MenuView', () => {
expect(screen.getByRole('listbox').style.maxHeight).toBe('88px')
})
it('pointerdown outside the menu (no composer card ancestor) dismisses', () => {
const { onDismiss } = mount(openState())
fireEvent.pointerDown(document.body)
expect(onDismiss).toHaveBeenCalledTimes(1)
})
it('pointerdown inside the list does not dismiss', () => {
const { onDismiss } = mount(openState())
fireEvent.pointerDown(screen.getAllByRole('option')[0]!)
expect(onDismiss).not.toHaveBeenCalled()
})
it('pointerdown inside the surrounding composer card does not dismiss; outside it does', () => {
const menu = createSnapshotStore<MenuState>(openState())
const onDismiss = vi.fn()
render(
<div data-composer-card="">
<MenuView menu={menu} onPick={vi.fn()} onDismiss={onDismiss} t={t} />
<button type="button" data-testid="composer-button" />
</div>,
)
fireEvent.pointerDown(screen.getByTestId('composer-button'))
expect(onDismiss).not.toHaveBeenCalled()
fireEvent.pointerDown(document.body)
expect(onDismiss).toHaveBeenCalledTimes(1)
})
it('ignores a pointerdown whose target is not a DOM node', () => {
const { onDismiss } = mount(openState())
const ev = new Event('pointerdown', { bubbles: true })
Object.defineProperty(ev, 'target', { value: {} })
document.dispatchEvent(ev)
expect(onDismiss).not.toHaveBeenCalled()
})
it('closing the menu removes the dismiss listener', () => {
const { menu, onDismiss } = mount(openState())
act(() => { menu.set(CLOSED) })
fireEvent.pointerDown(document.body)
expect(onDismiss).not.toHaveBeenCalled()
})
it('mousedown on a row picks (source, index) and prevents the focus steal', () => {
const { onPick } = mount(openState())
const options = screen.getAllByRole('option')