Files
deepseek-harness/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx
T

262 lines
10 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import type {
SessionId, SessionListState, WorkspaceId, WorkspaceListState, WorkspaceView,
} from '@deepseek-ai/dsh-client-runtime/client'
import type { SidebarRootComponentProps } from '../src/client/contract/slots.ts'
import { SidebarRoot } from '../src/client/SidebarRoot.tsx'
afterEach(() => {
cleanup()
vi.useRealTimers()
})
const sid = (id: string) => id as SessionId
const wid = (id: string) => id as WorkspaceId
const hook = <T,>(snapshot: T) => <S,>(selector: (state: T) => S): S => selector(snapshot)
const workspace: WorkspaceView = {
workspaceId: wid('project'), path: '/projects/project', title: 'Project', sessionIds: [sid('s1')],
createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
}
const sessions: SessionListState = {
ids: [sid('s1')],
byId: { [sid('s1')]: { id: sid('s1'), displayTitle: 'First session', running: false, updatedAt: 1 } },
current: undefined, phase: 'ready',
intent: undefined,
}
const workspaces: WorkspaceListState = {
items: [workspace], state: 'idle', phase: 'ready', error: null,
intent: undefined, baselinesReady: true, recentWorkspaceId: workspace.workspaceId,
}
function mount(sessionState: SessionListState = sessions) {
const startSession = vi.fn()
const open = vi.fn()
let pickerOwner: unknown
const view = render(
<SidebarRoot
collapsed={false} width={300}
useSessions={hook(sessionState)} useWorkspaces={hook(workspaces)}
startSession={startSession} open={open} toggleSidebar={vi.fn()}
renderSlot={((_key: string, owner: unknown) => { pickerOwner = owner; return null }) as SidebarRootComponentProps['renderSlot']}
/>,
)
return { view, startSession, open, pickerOwner: () => pickerOwner }
}
function mountSidebar({
sessionState = sessions,
workspaceState = workspaces,
collapsed = false,
width = 300,
}: {
sessionState?: SessionListState
workspaceState?: WorkspaceListState
collapsed?: boolean
width?: number
} = {}) {
const startSession = vi.fn()
const open = vi.fn()
const toggleSidebar = vi.fn()
let pickerOwner: unknown
let current = { sessionState, workspaceState, collapsed, width }
const root = () => (
<SidebarRoot
collapsed={current.collapsed} width={current.width}
useSessions={hook(current.sessionState)} useWorkspaces={hook(current.workspaceState)}
startSession={startSession} open={open} toggleSidebar={toggleSidebar}
renderSlot={((_key: string, owner: unknown) => { pickerOwner = owner; return null }) as SidebarRootComponentProps['renderSlot']}
/>
)
const view = render(root())
return {
startSession,
open,
toggleSidebar,
pickerOwner: () => pickerOwner,
rerender(next: Partial<typeof current>) {
current = { ...current, ...next }
view.rerender(root())
},
}
}
describe('SidebarRoot', () => {
it('renders real Workspaces from useWorkspaces and routes New Session', () => {
const b = mount()
expect(screen.getByText('Project')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'New session' }))
expect(b.startSession).toHaveBeenCalledWith()
})
it('shows a frontend Session under its real Workspace and routes its row plus', () => {
const intent = { sessionId: sid('intent'), target: { kind: 'workspace' as const, workspaceId: workspace.workspaceId }, prompt: '', phase: 'connecting' as const }
const b = mount({
...sessions,
current: intent.sessionId,
intent,
})
expect(screen.getByText('New session')).toBeTruthy()
expect(screen.getByText('2 sessions')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'New session in Project' }))
expect(b.startSession).toHaveBeenCalledWith(workspace.workspaceId)
})
it('forwards Workspace picker selection and closes the picker', () => {
const b = mount()
fireEvent.click(screen.getByRole('button', { name: 'Create workspace' }))
const owner = b.pickerOwner() as { open: boolean; onPick(id: WorkspaceId): void }
expect(owner.open).toBe(true)
owner.onPick(workspace.workspaceId)
expect(b.startSession).toHaveBeenCalledWith(workspace.workspaceId)
})
it('opens a real Session through the owner action', () => {
const b = mount({ ...sessions, current: sid('intent'), intent: {
sessionId: sid('intent'), target: { kind: 'workspace', workspaceId: workspace.workspaceId }, prompt: '', phase: 'ready',
} })
fireEvent.click(screen.getByText('Project'))
fireEvent.click(screen.getByText('First session'))
expect(b.open).toHaveBeenCalledWith(sid('s1'))
})
it('opens, selects, dismisses, and toggles the group-by menu', () => {
mount()
const button = screen.getByRole('button', { name: 'Group by' })
fireEvent.click(button)
fireEvent.click(screen.getByRole('menuitem', { name: 'Workspace' }))
expect(screen.queryByRole('menu')).toBeNull()
fireEvent.click(button)
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryByRole('menu')).toBeNull()
fireEvent.click(button)
fireEvent.click(button)
expect(screen.queryByRole('menu')).toBeNull()
})
it('routes every Workspace picker close path', () => {
const b = mount()
fireEvent.click(screen.getByRole('button', { name: 'Create workspace' }))
const owner = b.pickerOwner() as { open: boolean; onClose(): void }
expect(owner.open).toBe(true)
act(() => { owner.onClose() })
expect((b.pickerOwner() as { open: boolean }).open).toBe(false)
})
it('focuses, filters, and clears search while distinguishing both empty states', () => {
mount()
const input = screen.getByPlaceholderText('Search name, keywords...')
fireEvent.click(input.parentElement!)
expect(document.activeElement).toBe(input)
fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
fireEvent.change(input, { target: { value: 'missing' } })
expect(screen.getByText('No matches')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Clear search' }))
expect(screen.queryByText('No matches')).toBeNull()
cleanup()
const emptySessions = listState()
const emptyWorkspaces: WorkspaceListState = { ...workspaces, items: [], recentWorkspaceId: undefined }
mountSidebar({ sessionState: emptySessions, workspaceState: emptyWorkspaces })
expect(screen.getByText('No sessions yet')).toBeTruthy()
})
it('toggles Workspace and nested Session expansion in both directions', () => {
const parent = sid('parent')
const child = sid('child')
const nestedSessions: SessionListState = {
...sessions,
ids: [parent, child],
byId: {
[parent]: { id: parent, displayTitle: 'Parent', running: false, updatedAt: 2 },
[child]: { id: child, displayTitle: 'Child', running: false, updatedAt: 1, parentId: parent },
},
}
const nestedWorkspace: WorkspaceListState = {
...workspaces,
items: [{ ...workspace, sessionIds: [parent, child] }],
}
mountSidebar({ sessionState: nestedSessions, workspaceState: nestedWorkspace })
fireEvent.click(screen.getByText('Project'))
fireEvent.click(screen.getByRole('button', { name: 'Expand' }))
expect(screen.getByText('Child')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Collapse' }))
expect(screen.queryByText('Child')).toBeNull()
fireEvent.click(screen.getByText('Project'))
expect(screen.queryByText('Parent')).toBeNull()
})
it('does not start a Session from an Ungrouped row create action', () => {
const loose = sid('loose')
const looseSessions: SessionListState = {
...listState(),
ids: [loose],
byId: { [loose]: { id: loose, displayTitle: 'Loose', running: false, updatedAt: 1 } },
current: loose,
}
const b = mountSidebar({
sessionState: looseSessions,
workspaceState: { ...workspaces, items: [], recentWorkspaceId: undefined },
})
fireEvent.click(screen.getByRole('button', { name: 'New session in Ungrouped' }))
expect(b.startSession).not.toHaveBeenCalled()
})
it('keeps an already expanded selected Workspace open and resolves later Workspace matches', () => {
const b = mountSidebar()
fireEvent.click(screen.getByText('Project'))
const other = { ...workspace, workspaceId: wid('other'), title: 'Other', sessionIds: [] }
b.rerender({
sessionState: { ...sessions, current: sid('s1') },
workspaceState: { ...workspaces, items: [other, workspace] },
})
expect(screen.getByText('First session')).toBeTruthy()
b.rerender({
sessionState: {
...sessions,
current: sid('draft'),
intent: { sessionId: sid('draft'), target: { kind: 'workspace-intent' }, prompt: '', phase: 'ready' },
},
})
expect(screen.getByText('Project')).toBeTruthy()
})
it('renders the static collapsed rail and expands rail search into focused input', () => {
vi.useFakeTimers()
const b = mountSidebar({ collapsed: true })
expect(screen.getByRole('button', { name: 'Open sidebar' })).toBeTruthy()
expect(screen.queryByPlaceholderText('Search name, keywords...')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Open sidebar' }))
expect(b.toggleSidebar).toHaveBeenCalledOnce()
fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
expect(b.toggleSidebar).toHaveBeenCalledTimes(2)
b.rerender({ collapsed: false })
const input = screen.getByPlaceholderText('Search name, keywords...')
act(() => { vi.advanceTimersByTime(300) })
expect(document.activeElement).toBe(input)
})
it('keeps wide content during live collapse, then settles to the rail', () => {
vi.useFakeTimers()
const b = mountSidebar({ width: 320 })
fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' }))
expect(b.toggleSidebar).toHaveBeenCalledOnce()
b.rerender({ collapsed: true, width: 56 })
expect(screen.getByPlaceholderText('Search name, keywords...')).toBeTruthy()
act(() => { vi.advanceTimersByTime(150) })
expect(screen.queryByPlaceholderText('Search name, keywords...')).toBeNull()
expect(screen.getByRole('button', { name: 'Open sidebar' })).toBeTruthy()
})
})
function listState(): SessionListState {
return { ids: [], byId: {}, current: undefined, phase: 'ready', intent: undefined }
}