Merge remote-tracking branch 'origin/master' into codex/status-bar-token-metrics
# Conflicts: # packages/client/ui-conversation/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/host/apiproxy/src/api-proxy.ts
This commit is contained in:
@@ -107,6 +107,7 @@ async function bench() {
|
||||
const workspacesFake = {
|
||||
list: workspaceStore,
|
||||
connectWorkspace: vi.fn(async () => ROOT),
|
||||
openPath: vi.fn(async () => {}),
|
||||
}
|
||||
ctx.provide('workspaces', workspacesFake)
|
||||
const layoutFake = { openDetails: vi.fn(), closeDetails: vi.fn() }
|
||||
@@ -259,6 +260,15 @@ describe('conversation slot inject surface', () => {
|
||||
expect(conv.instance).toBe(instance)
|
||||
})
|
||||
|
||||
it('openFile (chat view face) resolves against session cwd and calls workspaces.openPath', async () => {
|
||||
const b = await bench()
|
||||
const { injected } = b.chatViewSurface(ROOT)
|
||||
injected.openFile('src/a.ts')
|
||||
await vi.waitFor(() => {
|
||||
expect(b.workspacesFake.openPath).toHaveBeenCalledWith('/proj/src/a.ts')
|
||||
})
|
||||
})
|
||||
|
||||
it('routes navigation and workspace switching through the runtime owners, carrying the draft', async () => {
|
||||
const b = await bench()
|
||||
const { injected } = b.conversationSurface(ROOT)
|
||||
|
||||
@@ -48,6 +48,7 @@ async function bench() {
|
||||
ctx.provide('workspaces', {
|
||||
startSession: vi.fn(),
|
||||
sendSession: vi.fn(),
|
||||
openPath: vi.fn(async () => {}),
|
||||
})
|
||||
ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
||||
ctx.provide('locale', { bind: () => (key: string) => key })
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// always-visible nested rows through the SAME keyed toolview hole — the bash
|
||||
// sub-call lands in the bash sample plugin's registration exactly like a
|
||||
// top-level bash row, unregistered sub-tools fall back to GenericToolCard —
|
||||
// and a sub-row click opens details for the sub-callId. Running parents
|
||||
// and a file sub-row click opens the host path. Running parents
|
||||
// (runningCalls) nest their so-far dispatches the same way.
|
||||
|
||||
import { Context } from 'cordis'
|
||||
@@ -114,14 +114,16 @@ async function bench(snapshot: ConversationSnapshot) {
|
||||
open: vi.fn(),
|
||||
}
|
||||
ctx.provide('sessions', sessionsFake)
|
||||
ctx.provide('workspaces', {
|
||||
const workspaces = {
|
||||
list: createSnapshotStore<WorkspaceListState>({
|
||||
items: [], state: 'idle', phase: 'ready', error: null,
|
||||
baselinesReady: true, recentWorkspaceId: undefined,
|
||||
}),
|
||||
startSession: vi.fn(),
|
||||
sendSession: vi.fn(),
|
||||
})
|
||||
openPath: vi.fn(async () => {}),
|
||||
}
|
||||
ctx.provide('workspaces', workspaces)
|
||||
ctx.provide('layout', layout)
|
||||
ctx.provide('i18n', { bind: () => (key: string) => key })
|
||||
|
||||
@@ -136,7 +138,7 @@ async function bench(snapshot: ConversationSnapshot) {
|
||||
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
return { ctx, slots, fiber, session, layout }
|
||||
return { ctx, slots, fiber, session, layout, workspaces }
|
||||
}
|
||||
|
||||
function mountApp(slots: SlotsService) {
|
||||
@@ -220,15 +222,21 @@ describe('run_code sub-calls through the real chat machinery', () => {
|
||||
expect(nested).not.toBeNull()
|
||||
})
|
||||
|
||||
it('a sub-row click opens details for the sub-callId', async () => {
|
||||
it('a file sub-row click opens the host path; bash sub-rows do not open details', async () => {
|
||||
const parent = 'call-64'
|
||||
const dispatches = new Map([[parent, [
|
||||
subCall(11, parent, 1, 'bash', { command: 'ls notes', description: 'List notes' }, 'demo.txt'),
|
||||
subCall(11, parent, 1, 'read', { path: 'notes/demo.txt' }, 'ok'),
|
||||
subCall(12, parent, 2, 'bash', { command: 'ls notes', description: 'List notes' }, 'demo.txt'),
|
||||
]]])
|
||||
const b = await bench(snapshotWith([codeResult(10, parent)], dispatches))
|
||||
const view = mountApp(b.slots)
|
||||
view.getByText('notes/demo.txt').click()
|
||||
expect(b.layout.openDetails).not.toHaveBeenCalled()
|
||||
await vi.waitFor(() => {
|
||||
expect(b.workspaces.openPath).toHaveBeenCalledWith('notes/demo.txt')
|
||||
})
|
||||
view.getByText('List notes').click()
|
||||
expect(b.layout.openDetails).toHaveBeenCalledTimes(1)
|
||||
expect(b.layout.openDetails).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('a RUNNING run_code call nests its so-far dispatches under the spinner row', async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// standard useSessions kit (no registry predicates — tool ring dissolved).
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { act, cleanup, render } from '@testing-library/react'
|
||||
import type {
|
||||
AssistantMessageNode, ConversationSnapshot, SessionId, SessionListState, ToolResultNode,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -204,10 +204,9 @@ describe('bash sample row', () => {
|
||||
|
||||
const rowProps = (sessionId: SessionId, over?: {
|
||||
store?: ReturnType<typeof listStore>
|
||||
openDetails?: () => void
|
||||
}): ToolRowProps => ({
|
||||
callId: 'c1', toolName: 'bash', block: result('c1'),
|
||||
openDetails: over?.openDetails ?? vi.fn(),
|
||||
openFile: vi.fn(),
|
||||
sessionId,
|
||||
useSessions: bindSnapshotSelector(over?.store ?? listStore()),
|
||||
} as unknown as ToolRowProps)
|
||||
@@ -240,21 +239,17 @@ describe('bash sample row', () => {
|
||||
expect(view.container.querySelector('[data-sample="bash-scoped"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('summarizes as Bash · description and hands clicks to openDetails on both arms', () => {
|
||||
const openGlobal = vi.fn()
|
||||
const global = render(<BashRow {...rowProps(ROOT, { openDetails: openGlobal })} />)
|
||||
it('summarizes as Bash · description on both arms without row click targets', () => {
|
||||
const global = render(<BashRow {...rowProps(ROOT)} />)
|
||||
// Two renders share document.body: query inside each container.
|
||||
const globalRow = global.container.querySelector('[data-sample="bash-global"]')!
|
||||
expect(globalRow.textContent).toContain('Bash')
|
||||
expect(globalRow.textContent).toContain('Build')
|
||||
fireEvent.click(globalRow)
|
||||
expect(openGlobal).toHaveBeenCalledTimes(1)
|
||||
const openScoped = vi.fn()
|
||||
const scoped = render(<BashRow {...rowProps(CHILD, { openDetails: openScoped })} />)
|
||||
expect(globalRow.getAttribute('data-clickable')).toBeNull()
|
||||
const scoped = render(<BashRow {...rowProps(CHILD)} />)
|
||||
const scopedRow = scoped.container.querySelector('[data-sample="bash-scoped"]')!
|
||||
expect(scopedRow.textContent).toContain('Bash')
|
||||
expect(scopedRow.textContent).toContain('Build')
|
||||
fireEvent.click(scopedRow)
|
||||
expect(openScoped).toHaveBeenCalledTimes(1)
|
||||
expect(scopedRow.getAttribute('data-clickable')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
|
||||
afterEach(cleanup)
|
||||
import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { classifyTool, toolRowModel } from '../src/client/contract/tool-call-model.ts'
|
||||
import { classifyTool, resolveToolPath, toolRowModel } from '../src/client/contract/tool-call-model.ts'
|
||||
import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
|
||||
import { ToolRow } from '../src/client/chat/ToolRow.tsx'
|
||||
import { GenericToolCard } from '../src/client/chat/GenericToolCard.tsx'
|
||||
@@ -64,6 +64,22 @@ describe('tool-call-model', () => {
|
||||
expect(toolRowModel('', running({ argsRaw: '' })).summary).toBe('c1')
|
||||
})
|
||||
|
||||
it('exposes filePath for path/file_path args and skips URL-only reads', () => {
|
||||
expect(toolRowModel('read', running({ name: 'read', argsRaw: '{"path":"src/a.ts"}' })).filePath).toBe('src/a.ts')
|
||||
expect(toolRowModel('write', running({ name: 'write', argsRaw: '{"file_path":"src/a.ts"}' })).filePath).toBe('src/a.ts')
|
||||
expect(toolRowModel('edit', running({ name: 'edit', argsRaw: '{"file_path":"src/a.ts"}' })).filePath).toBe('src/a.ts')
|
||||
expect(toolRowModel('web_fetch', running({ name: 'web_fetch', argsRaw: '{"url":"https://example.com"}' })).filePath)
|
||||
.toBeUndefined()
|
||||
expect(toolRowModel('bash', running()).filePath).toBeUndefined()
|
||||
})
|
||||
|
||||
it('resolveToolPath joins relative paths under cwd and passes absolute through', () => {
|
||||
expect(resolveToolPath('/w', 'src/a.ts')).toBe('/w/src/a.ts')
|
||||
expect(resolveToolPath('/w/', '/abs/a.ts')).toBe('/abs/a.ts')
|
||||
expect(resolveToolPath(undefined, 'src/a.ts')).toBe('src/a.ts')
|
||||
expect(resolveToolPath('/w', 'C:\\x\\a.ts')).toBe('C:\\x\\a.ts')
|
||||
})
|
||||
|
||||
it('displays workspace-rooted paths relative to the session cwd', () => {
|
||||
const cwd = '/Users/u/ws/'
|
||||
expect(toolRowModel('edit', running({ name: 'edit', argsRaw: '{"file_path":"/Users/u/ws/src/x.ts"}' }), cwd).summary).toBe('src/x.ts')
|
||||
@@ -149,13 +165,34 @@ describe('ToolRow', () => {
|
||||
expect(view.queryByTestId('tool-icon')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('row click hands off to onOpenDetails; the expand toggle does not', () => {
|
||||
it('file-path summary opens through onOpenFile; the leading slot is not an expand control', () => {
|
||||
const open = vi.fn()
|
||||
const view = render(<ToolRow {...rowProps} onOpenDetails={open} />)
|
||||
const view = render(
|
||||
<ToolRow {...rowProps} variant="read" title="Read" summary="src/a.ts" filePath="src/a.ts" onOpenFile={open} />,
|
||||
)
|
||||
fireEvent.click(view.getByText('src/a.ts'))
|
||||
expect(open).toHaveBeenCalledWith('src/a.ts')
|
||||
// Only the path link is a button — no args-expand affordance on file rows.
|
||||
expect(view.container.querySelectorAll('button')).toHaveLength(1)
|
||||
expect(view.container.querySelector('[aria-expanded]')).toBeNull()
|
||||
expect(view.queryByText(/"a": 1/)).toBeNull()
|
||||
})
|
||||
|
||||
it('a single-file path disables expand even when onOpenFile is absent', () => {
|
||||
const view = render(
|
||||
<ToolRow {...rowProps} variant="write" title="Write" summary="作文.md" filePath="作文.md" />,
|
||||
)
|
||||
expect(view.container.querySelector('button')).toBeNull()
|
||||
expect(view.container.querySelector('[aria-expanded]')).toBeNull()
|
||||
fireEvent.click(view.getByText('作文.md'))
|
||||
expect(view.queryByText(/"a": 1/)).toBeNull()
|
||||
})
|
||||
|
||||
it('non-file rows do not open anything when the summary is clicked', () => {
|
||||
const open = vi.fn()
|
||||
const view = render(<ToolRow {...rowProps} onOpenFile={open} />)
|
||||
fireEvent.click(view.getByText('List files'))
|
||||
expect(open).toHaveBeenCalledTimes(1)
|
||||
fireEvent.click(view.container.querySelector('button')!)
|
||||
expect(open).toHaveBeenCalledTimes(1)
|
||||
expect(open).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -180,7 +217,7 @@ describe('ThinkRow', () => {
|
||||
|
||||
describe('GenericToolCard', () => {
|
||||
const props = (toolName: string, block: RunningToolCall | ToolResultNode): ToolRowOwnerProps => ({
|
||||
callId: 'c1', toolName, block, openDetails: vi.fn(),
|
||||
callId: 'c1', toolName, block, openFile: vi.fn(),
|
||||
})
|
||||
|
||||
it('renders the classified variant row from the frozen slice', () => {
|
||||
@@ -225,10 +262,15 @@ describe('GenericToolCard', () => {
|
||||
expect(view.container.querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('row click reaches openDetails', () => {
|
||||
const p = props('bash', result())
|
||||
const view = render(<GenericToolCard {...p} />)
|
||||
fireEvent.click(view.getByText('List files'))
|
||||
expect(p.openDetails).toHaveBeenCalledTimes(1)
|
||||
it('file-path summary click reaches openFile; bash summary does not', () => {
|
||||
const file = props('read', running({ name: 'read', argsRaw: '{"path":"src/x.ts"}' }))
|
||||
const fileView = render(<GenericToolCard {...file} />)
|
||||
fireEvent.click(fileView.getByText('src/x.ts'))
|
||||
expect(file.openFile).toHaveBeenCalledWith('src/x.ts')
|
||||
|
||||
const bash = props('bash', result())
|
||||
const bashView = render(<GenericToolCard {...bash} />)
|
||||
fireEvent.click(bashView.getByText('List files'))
|
||||
expect(bash.openFile).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,14 +121,16 @@ async function bench(nodes: ToolResultNode[]) {
|
||||
open: vi.fn(),
|
||||
updateIntent: vi.fn(),
|
||||
})
|
||||
ctx.provide('workspaces', {
|
||||
const workspaces = {
|
||||
list: createSnapshotStore<WorkspaceListState>({
|
||||
items: [], state: 'idle', phase: 'ready', error: null,
|
||||
baselinesReady: true, recentWorkspaceId: undefined,
|
||||
}),
|
||||
startSession: vi.fn(),
|
||||
sendSession: vi.fn(),
|
||||
})
|
||||
openPath: vi.fn(async () => {}),
|
||||
}
|
||||
ctx.provide('workspaces', workspaces)
|
||||
ctx.provide('layout', layout)
|
||||
ctx.provide('locale', { bind: () => (key: string) => key })
|
||||
|
||||
@@ -143,7 +145,7 @@ async function bench(nodes: ToolResultNode[]) {
|
||||
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
return { ctx, slots, fiber, session, list, layout }
|
||||
return { ctx, slots, fiber, session, list, layout, workspaces }
|
||||
}
|
||||
|
||||
/** Render the whole tree through the ctx-level root seam (the shell's own entry). */
|
||||
@@ -186,11 +188,22 @@ describe('keyed toolview hole through the real machinery', () => {
|
||||
expect(mounted!.querySelector('pre.shiki')?.textContent).toBe(code)
|
||||
})
|
||||
|
||||
it('row clicks travel owner openDetails → chat inject → layout orchestration', async () => {
|
||||
it('file-path clicks travel owner openFile → chat inject → workspaces.openPath', async () => {
|
||||
const b = await bench([toolResult(3, 'c1', 'read', '{"path":"src/a.ts"}')])
|
||||
const view = mountApp(b.slots)
|
||||
view.getByText('src/a.ts').click()
|
||||
expect(b.layout.openDetails).not.toHaveBeenCalled()
|
||||
await vi.waitFor(() => {
|
||||
expect(b.workspaces.openPath).toHaveBeenCalledWith('src/a.ts')
|
||||
})
|
||||
})
|
||||
|
||||
it('bash summary clicks do not open details or host paths', async () => {
|
||||
const b = await bench([toolResult(3, 'c1', 'bash')])
|
||||
const view = mountApp(b.slots)
|
||||
view.getByText('Build').click()
|
||||
expect(b.layout.openDetails).toHaveBeenCalledTimes(1)
|
||||
expect(b.layout.openDetails).not.toHaveBeenCalled()
|
||||
expect(b.workspaces.openPath).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('a live keyed registration takes over its tool row and unload reverts to the fallback', async () => {
|
||||
@@ -271,6 +284,7 @@ describe('registrant load-order seam', () => {
|
||||
}),
|
||||
startSession: vi.fn(),
|
||||
sendSession: vi.fn(),
|
||||
openPath: vi.fn(async () => {}),
|
||||
})
|
||||
ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
||||
ctx.provide('locale', { bind: () => (key: string) => key })
|
||||
|
||||
@@ -88,6 +88,7 @@ function emptyWorkspaces() {
|
||||
function makeHarness(init?: Partial<ConversationSnapshot>) {
|
||||
const { set, source } = makeSource(init)
|
||||
const openDetails = vi.fn<(t: SelectionTarget) => void>()
|
||||
const openFile = vi.fn<(path: string) => void>()
|
||||
const loadOlder = vi.fn()
|
||||
// Selection rides the REAL chat store (same construction path as
|
||||
// production; the view reads it through the PropsStore useStore share).
|
||||
@@ -112,10 +113,11 @@ function makeHarness(init?: Partial<ConversationSnapshot>) {
|
||||
renderSlot,
|
||||
SessionProvider: SessionProviderStub,
|
||||
openDetails,
|
||||
openFile,
|
||||
loadOlder,
|
||||
}
|
||||
const setSelection = (next: SelectionTarget | null): void => { chat.actions.select(next) }
|
||||
return { set, ChatView, props, openDetails, loadOlder, setSelection }
|
||||
return { set, ChatView, props, openDetails, openFile, loadOlder, setSelection }
|
||||
}
|
||||
|
||||
describe('chat-flow derivation', () => {
|
||||
@@ -281,16 +283,31 @@ describe('ChatView', () => {
|
||||
expect(view.getByText(/"command": "cmd-a"/)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('clicking a tool row opens details with callId and toolName; selection marks data-selected', () => {
|
||||
it('clicking a bash summary does not open details; selection still marks data-selected', () => {
|
||||
const h = makeHarness({ nodes: [toolResult(3, 'a')] })
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
fireEvent.click(view.getByText('run a'))
|
||||
expect(h.openDetails).toHaveBeenCalledWith({ turnSeq: 3, callId: 'a', toolName: 'bash' })
|
||||
expect(h.openDetails).not.toHaveBeenCalled()
|
||||
expect(h.openFile).not.toHaveBeenCalled()
|
||||
expect(view.container.querySelector('[data-selected]')).toBeNull()
|
||||
act(() => { h.setSelection({ turnSeq: 3, callId: 'a', toolName: 'bash' }) })
|
||||
expect(view.container.querySelector('[data-selected]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('clicking a file-tool path summary opens the host file, not details', () => {
|
||||
const h = makeHarness({
|
||||
nodes: [{
|
||||
kind: 'tool-result', seq: 3, time: 3_000, callId: 'r1',
|
||||
call: { name: 'read', argsRaw: '{"path":"src/a.ts"}' },
|
||||
callTime: 2_500, content: [], isError: false, callView: null, resultView: null,
|
||||
}],
|
||||
})
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
fireEvent.click(view.getByText('src/a.ts'))
|
||||
expect(h.openFile).toHaveBeenCalledWith('src/a.ts')
|
||||
expect(h.openDetails).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('running calls render as a live tool group with the running state', () => {
|
||||
const h = makeHarness({ runningCalls: [runningCall('r1')], running: true })
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('tails', () => {
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
}
|
||||
const props: ToolRowOwnerProps = {
|
||||
callId: 'c5', toolName: 'todo_write', block: settled, openDetails: vi.fn(),
|
||||
callId: 'c5', toolName: 'todo_write', block: settled, openFile: vi.fn(),
|
||||
}
|
||||
const view = render(<GenericToolCard {...props} />)
|
||||
// Settled ok state keeps the variant icon (sparkle) instead of a StateDot.
|
||||
@@ -99,7 +99,7 @@ describe('tails', () => {
|
||||
phase: 'ready',
|
||||
})
|
||||
const props = (block: RunningToolCall | ToolResultNode) => ({
|
||||
callId: 'c1', toolName: 'bash', block, openDetails: vi.fn(),
|
||||
callId: 'c1', toolName: 'bash', block, openFile: vi.fn(),
|
||||
sessionId: sid, useSessions: bindSnapshotSelector(list),
|
||||
} as unknown as ToolRowProps)
|
||||
|
||||
|
||||
@@ -96,10 +96,10 @@ const resultNode = (argsRaw: string, over?: Partial<ToolResultNode>): ToolResult
|
||||
content: [], isError: false, callView: null, resultView: null, ...over,
|
||||
})
|
||||
|
||||
function rowProps(block: unknown, openDetails = vi.fn()): ToolRowProps {
|
||||
function rowProps(block: unknown): ToolRowProps {
|
||||
return {
|
||||
callId: 'c1', toolName: 'todo_write', block,
|
||||
openDetails,
|
||||
openFile: vi.fn(),
|
||||
sessionId: 's1',
|
||||
useSessions: () => undefined,
|
||||
} as unknown as ToolRowProps
|
||||
@@ -140,27 +140,10 @@ describe('TodoRow', () => {
|
||||
expect(screen.getByText('todo_write · not json')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('falls back when parsed args carry no todos array, and click opens details', () => {
|
||||
const openDetails = vi.fn()
|
||||
render(<TodoRow {...rowProps(resultNode('{"other":1}'), openDetails)} />)
|
||||
it('falls back when parsed args carry no todos array and stays non-interactive', () => {
|
||||
render(<TodoRow {...rowProps(resultNode('{"other":1}'))} />)
|
||||
expect(screen.getByText('todo_write · {"other":1}')).toBeTruthy()
|
||||
fireEvent.click(screen.getByText('更新任务清单'))
|
||||
expect(openDetails).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('opens details from the keyboard on Enter and Space, ignoring other keys', () => {
|
||||
const openDetails = vi.fn()
|
||||
render(<TodoRow {...rowProps(resultNode(ARGS), openDetails)} />)
|
||||
const row = screen.getByRole('button')
|
||||
expect(row.getAttribute('tabindex')).toBe('0')
|
||||
fireEvent.keyDown(row, { key: 'Enter' })
|
||||
fireEvent.keyDown(row, { key: ' ' })
|
||||
expect(openDetails).toHaveBeenCalledTimes(2)
|
||||
// Space must not also scroll the flow: the handler claims the event.
|
||||
expect(fireEvent.keyDown(row, { key: ' ' })).toBe(false)
|
||||
fireEvent.keyDown(row, { key: 'a' })
|
||||
fireEvent.keyDown(row, { key: 'ArrowDown' })
|
||||
expect(openDetails).toHaveBeenCalledTimes(3)
|
||||
expect(screen.queryByRole('button')).toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -49,6 +49,8 @@ describe('view-ring type negatives (compile-time; body never runs)', () => {
|
||||
const chatProps = (props: ChatViewSlotProps): ReactNode => {
|
||||
// @ts-expect-error openDetails takes a SelectionTarget, not a string
|
||||
props.openDetails('nope')
|
||||
// @ts-expect-error openFile takes a path string, not a SelectionTarget
|
||||
props.openFile({ turnSeq: 1, callId: 'c' })
|
||||
return null
|
||||
}
|
||||
void chatProps
|
||||
|
||||
Reference in New Issue
Block a user