80 lines
4.7 KiB
TypeScript
80 lines
4.7 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
import {
|
|
CapabilityPicker,
|
|
capabilitySelectionKey,
|
|
detectCapabilitySlashTrigger,
|
|
filterCapabilityItems,
|
|
resolveCapabilityPickerKey,
|
|
type CapabilityPickerItem
|
|
} from '../src/renderer/src/workbuddy/composer/CapabilityPicker'
|
|
|
|
const items: CapabilityPickerItem[] = [
|
|
{ id: 'skill:review', name: '代码审查', description: '检查实现缺陷与风险', kind: 'skill' },
|
|
{ id: 'mcp-server', name: 'postgres', description: '查询本地数据库', kind: 'mcp' },
|
|
{ id: 'compact', name: 'compact', description: '压缩当前上下文', kind: 'command' }
|
|
]
|
|
const checks: string[] = []
|
|
const check = (name: string, actual: unknown, expected: unknown): void => {
|
|
assert.deepEqual(actual, expected, name)
|
|
checks.push(name)
|
|
}
|
|
|
|
check('empty query preserves registration order', filterCapabilityItems(items, ''), items)
|
|
check('filter matches names and IDs case-insensitively',
|
|
filterCapabilityItems(items, '/POST').map(item => item.id), ['mcp-server'])
|
|
check('filter matches descriptions', filterCapabilityItems(items, '上下文').map(item => item.id), ['compact'])
|
|
check('filter matches kind labels', filterCapabilityItems(items, '命令').map(item => item.id), ['compact'])
|
|
check('multiple terms must all match', filterCapabilityItems(items, '数据库 本地').map(item => item.id), ['mcp-server'])
|
|
check('selection identity includes kind', capabilitySelectionKey({ id: 'compact', kind: 'command' }), 'command:compact')
|
|
|
|
check('slash at input start is detected',
|
|
detectCapabilitySlashTrigger('/comp', 5, items), { start: 0, end: 5, query: 'comp' })
|
|
const independent = '请使用 /mcp'
|
|
check('independent slash token is detected',
|
|
detectCapabilitySlashTrigger(independent, independent.length, items), { start: 4, end: independent.length, query: 'mcp' })
|
|
check('plain slash opens the catalog when items exist',
|
|
detectCapabilitySlashTrigger('/', 1, items), { start: 0, end: 1, query: '' })
|
|
for (const value of ['https://example.com/a', 'src/renderer/file.ts', 'C:\\work\\file.ts', '/Users/me/file.txt', 'file:///tmp/a']) {
|
|
check('path or URL does not trigger: ' + value, detectCapabilitySlashTrigger(value, value.length, items), null)
|
|
}
|
|
check('unknown leading token is rejected when catalog is available',
|
|
detectCapabilitySlashTrigger('/Users', 6, items), null)
|
|
check('slash inside ordinary text is not independent',
|
|
detectCapabilitySlashTrigger('path/compact', 12, items), null)
|
|
|
|
check('IME composition suppresses Enter',
|
|
resolveCapabilityPickerKey({ key: 'Enter', isComposing: true, itemCount: 3, activeIndex: 1 }), { type: 'none' })
|
|
check('IME keyCode 229 suppresses selection',
|
|
resolveCapabilityPickerKey({ key: 'Enter', keyCode: 229, itemCount: 3, activeIndex: 1 }), { type: 'none' })
|
|
check('ArrowDown wraps', resolveCapabilityPickerKey({ key: 'ArrowDown', itemCount: 3, activeIndex: 2 }),
|
|
{ type: 'move', index: 0 })
|
|
check('ArrowUp wraps', resolveCapabilityPickerKey({ key: 'ArrowUp', itemCount: 3, activeIndex: 0 }),
|
|
{ type: 'move', index: 2 })
|
|
check('Home and End select boundaries',
|
|
[resolveCapabilityPickerKey({ key: 'Home', itemCount: 3, activeIndex: 2 }),
|
|
resolveCapabilityPickerKey({ key: 'End', itemCount: 3, activeIndex: 0 })],
|
|
[{ type: 'move', index: 0 }, { type: 'move', index: 2 }])
|
|
check('Enter selects the active item',
|
|
resolveCapabilityPickerKey({ key: 'Enter', itemCount: 3, activeIndex: 1 }), { type: 'select', index: 1 })
|
|
check('Escape closes', resolveCapabilityPickerKey({ key: 'Escape', itemCount: 3, activeIndex: 1 }), { type: 'close' })
|
|
check('Enter is ignored on an empty result', resolveCapabilityPickerKey({ key: 'Enter', itemCount: 0, activeIndex: 0 }),
|
|
{ type: 'none' })
|
|
|
|
const html = renderToStaticMarkup(
|
|
<CapabilityPicker items={items} selected={[{ id: 'skill:review', kind: 'skill' }]}
|
|
onSelect={() => undefined} onClose={() => undefined} />
|
|
)
|
|
check('SSR renders combobox and listbox', [html.includes('role="combobox"'), html.includes('role="listbox"')], [true, true])
|
|
check('SSR exposes type labels', ['技能', 'MCP', '命令'].every(label => html.includes(label)), true)
|
|
check('SSR marks selected identity', html.includes('aria-selected="true"'), true)
|
|
check('SSR renders descriptions', html.includes('检查实现缺陷与风险'), true)
|
|
check('SSR seeds external query',
|
|
renderToStaticMarkup(<CapabilityPicker items={items} selected={[]} onSelect={() => undefined}
|
|
onClose={() => undefined} query="compact" />).includes('data-kind="command"'), true)
|
|
check('SSR renders explicit empty state',
|
|
renderToStaticMarkup(<CapabilityPicker items={items} selected={[]} onSelect={() => undefined}
|
|
onClose={() => undefined} query="missing" />).includes('没有匹配的能力'), true)
|
|
|
|
console.log(JSON.stringify({ result: 'PASS', checks: checks.length }))
|