Files
deepseek-harness/packages/client/ui-permission/tests/permission-row.client.spec.tsx
imccyu 7ad54e7791 refactor(client): name the compile face in every client test filename
A test file under packages/client now says which face it covers:
`*.client.spec.{ts,tsx}` and its `*.client.{ts,tsx}` helpers belong to the
Client aggregate, `*.host.spec.ts` to the host aggregate. The carrier's four
node-half specs take the Host suffix.

The two suffixes are mutually exclusive, so each aggregate excludes the
other's and both keep one broad test glob: `exclude` wins over `include`, and
`packages/client/**` no longer has to be excluded wholesale from the host
program with per-file `files` entries carved back out of it. A Host-face spec
that reaches only Host source therefore needs no cross-face project
reference, which the split-project rule rejects.

vitest still discovers every file through `**/*.spec.{ts,tsx}`.
2026-08-12 01:41:40 +08:00

158 lines
6.2 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import type { SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
import { PermissionRow, type PermissionRowProps } from '../src/client/PermissionRow.tsx'
import { en } from '../src/client/locales.ts'
import { PermissionSettingsController } from '../src/client/settings-store.ts'
afterEach(cleanup)
const SCHEMA = {
uid: 5,
refs: {
1: { type: 'const', value: 'read-only' },
2: { type: 'const', value: 'workspace-write' },
3: { type: 'const', value: 'danger-full-access' },
4: { type: 'union', list: [1, 2, 3] },
5: { type: 'object', dict: { defaultPreset: 4 } },
},
}
function view(defaultPreset: string, revision = 0): SettingsNamespaceView {
return {
ns: 'permission',
schema: SCHEMA,
value: { defaultPreset },
base: { defaultPreset: 'read-only' },
applies: 'live',
secrets: [],
revision,
}
}
function ok<T>(value: T) {
return { rpcId: 'test', result: { ok: true as const, value } }
}
const dictionary: Record<string, string> = en
const t: PermissionRowProps['t'] = key => dictionary[key] ?? key
const runtime = {
useSessions: (() => { throw new Error('unused') }) as never,
useWorkspaces: (() => { throw new Error('unused') }) as never,
}
function mount(controller: PermissionSettingsController) {
return render(
<PermissionRow
{...runtime}
load={() => controller.load()}
select={preset => controller.select(preset)}
usePermission={bindSnapshotSelector(controller.store)}
t={t}
/>,
)
}
describe('PermissionRow', () => {
it('loads the descriptor, opens the menu, and selects a new default', async () => {
const mutate = vi.fn(() => Promise.resolve(ok(view('workspace-write', 1))))
const controller = new PermissionSettingsController({
settings: {
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
mutate,
} as never,
})
mount(controller)
const button = await screen.findByRole('button', { name: 'Read Only' })
expect(button.getAttribute('aria-expanded')).toBe('false')
fireEvent.click(button)
expect(button.getAttribute('aria-expanded')).toBe('true')
fireEvent.keyDown(document, { key: 'Escape' })
await waitFor(() => { expect(button.getAttribute('aria-expanded')).toBe('false') })
fireEvent.click(button)
fireEvent.click(button)
expect(button.getAttribute('aria-expanded')).toBe('false')
fireEvent.click(button)
fireEvent.click(screen.getByRole('menuitem', { name: 'Read Only' }))
expect(mutate).not.toHaveBeenCalled()
fireEvent.click(button)
fireEvent.click(screen.getByRole('menuitem', { name: 'Workspace Write' }))
await screen.findByRole('button', { name: 'Workspace Write' })
expect(mutate).toHaveBeenCalledOnce()
})
it('requires explicit acknowledgement before saving Full access', async () => {
const mutate = vi.fn(() => Promise.resolve(ok(view('danger-full-access', 1))))
const controller = new PermissionSettingsController({
settings: {
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
mutate,
} as never,
})
mount(controller)
fireEvent.click(await screen.findByRole('button', { name: 'Read Only' }))
fireEvent.click(screen.getByRole('menuitem', { name: 'Full access' }))
expect(mutate).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
expect(screen.queryByRole('dialog', { name: 'Enable Full access?' })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Read Only' }))
fireEvent.click(screen.getByRole('menuitem', { name: 'Full access' }))
const dialog = screen.getByRole('dialog', { name: 'Enable Full access?' })
const enable = screen.getByRole('button', { name: 'Enable Full access' })
expect((enable as HTMLButtonElement).disabled).toBe(true)
fireEvent.click(screen.getByRole('checkbox'))
fireEvent.click(enable)
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
expect(dialog.isConnected).toBe(false)
})
it('hides an unavailable namespace and disables a read-only provider', async () => {
const absent = new PermissionSettingsController({
settings: {
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] })),
mutate: vi.fn(),
} as never,
})
const rendered = mount(absent)
await waitFor(() => { expect(rendered.container.textContent).toBe('') })
rendered.unmount()
const readonly = new PermissionSettingsController({
settings: {
describe: () => Promise.resolve(ok({ writable: false, hasDocument: false, namespaces: [view('read-only')] })),
mutate: vi.fn(),
} as never,
})
mount(readonly)
expect((await screen.findByRole('button', { name: 'Read Only' })).hasAttribute('disabled')).toBe(true)
})
it('shows loading and a contained write error', async () => {
const describe = Promise.withResolvers<ReturnType<typeof ok<{
writable: boolean
namespaces: SettingsNamespaceView[]
}>>>()
const controller = new PermissionSettingsController({
settings: {
describe: () => describe.promise,
mutate: () => Promise.resolve({
rpcId: 'test',
result: {
ok: false as const,
error: { code: 'settings-conflict', message: 'changed elsewhere', details: {} },
},
}),
} as never,
})
mount(controller)
expect((await screen.findByRole('button', { name: 'Loading' })).hasAttribute('disabled')).toBe(true)
describe.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] }))
const button = await screen.findByRole('button', { name: 'Read Only' })
fireEvent.click(button)
fireEvent.click(screen.getByRole('menuitem', { name: 'Workspace Write' }))
expect((await screen.findByRole('alert')).textContent).toBe('changed elsewhere')
})
})