// @vitest-environment jsdom import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import type { SessionListState, WorkspaceId, WorkspaceListState, WorkspaceView, } from '@deepseek-ai/dsh-client-runtime/client' import { WorkspaceCreateError } from '@deepseek-ai/dsh-client-runtime/client' import type { DirectoryFlowOwnerProps } from '../src/client/contract/slots.ts' import { WorkspacePicker } from '../src/client/WorkspacePicker.tsx' afterEach(cleanup) const wid = (id: string) => id as WorkspaceId function workspace(id: string, title = id): WorkspaceView { return { workspaceId: wid(id), path: `/projects/${id}`, title, sessionIds: [], createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', } } function hook(snapshot: T) { return function select(selector: (state: T) => S): S { return selector(snapshot) } } const sessions: SessionListState = { ids: [], byId: {}, current: undefined, phase: 'ready', } const workspaceState = (items: readonly WorkspaceView[]): WorkspaceListState => ({ items, state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: items[0]?.workspaceId, }) function anchor(): { current: HTMLElement } { const element = document.createElement('button') element.getBoundingClientRect = () => ({ top: 10, left: 20, width: 30, height: 40, right: 50, bottom: 50, x: 20, y: 10, toJSON: () => ({}), }) return { current: element } } /** * Probe occupant of the directory-flow hole: records the latest owner * conversation so tests drive onPicked/onCancel/onError like a composed flow * package would, and renders a marker element while the flow is open. */ function flowProbe() { const probe: { owner: DirectoryFlowOwnerProps | undefined } = { owner: undefined } const renderSlot = ((_name: string, owner: DirectoryFlowOwnerProps) => { probe.owner = owner return owner.open ?
: null }) as never return { probe, renderSlot } } function mount( items: readonly WorkspaceView[] = [workspace('alpha', 'Alpha')], createWorkspace = vi.fn(), hasDirectoryFlow: () => boolean = () => true, ) { const onPick = vi.fn() const onClose = vi.fn() const anchorRef = anchor() const { probe, renderSlot } = flowProbe() const renderPicker = (nextItems: readonly WorkspaceView[]) => ( ) const view = render( renderPicker(items), ) return { view, onPick, onClose, createWorkspace, probe, rerenderItems: (nextItems: readonly WorkspaceView[]) => { view.rerender(renderPicker(nextItems)) }, } } function chooseItem(name: 'Open local folder…' | 'Create a new workspace'): void { fireEvent.click(screen.getByRole('menuitem', { name })) } describe('WorkspacePicker', () => { it('lists real Workspaces from useWorkspaces and forwards a selected id', () => { const b = mount() fireEvent.click(screen.getByRole('menuitem', { name: 'Alpha' })) expect(b.onPick).toHaveBeenCalledWith(wid('alpha')) }) it('creates a real Workspace from a name and focuses its frontend Session target', async () => { const created = workspace('new', 'New') const createWorkspace = vi.fn(async () => created) const b = mount([], createWorkspace) chooseItem('Create a new workspace') const input = screen.getByLabelText('New workspace name') fireEvent.change(input, { target: { value: 'project-one' } }) fireEvent.click(screen.getByRole('button', { name: 'Create workspace' })) expect(createWorkspace).toHaveBeenCalledWith({ name: 'project-one' }) await waitFor(() => { expect(b.onPick).toHaveBeenCalledWith(created.workspaceId) }) }) it('opens the composed directory flow, adopts its picked path, and selects the returned Workspace', async () => { const created = { ...workspace('adopted'), path: '/tmp/project', title: 'project' } const createWorkspace = vi.fn(async () => created) const b = mount([], createWorkspace) expect(screen.queryByTestId('directory-flow')).toBeNull() chooseItem('Open local folder…') expect(b.onClose).toHaveBeenCalled() expect(screen.getByTestId('directory-flow')).toBeTruthy() await act(async () => { b.probe.owner!.onPicked('/tmp/project') }) expect(createWorkspace).toHaveBeenCalledWith({ path: '/tmp/project' }) await waitFor(() => { expect(b.onPick).toHaveBeenCalledWith(created.workspaceId) }) // Successful adoption withdraws the flow request. expect(screen.queryByTestId('directory-flow')).toBeNull() }) it('treats flow cancellation as a silent no-op', () => { const b = mount([]) chooseItem('Open local folder…') act(() => { b.probe.owner!.onCancel() }) expect(screen.queryByTestId('directory-flow')).toBeNull() expect(b.createWorkspace).not.toHaveBeenCalled() expect(b.onPick).not.toHaveBeenCalled() expect(screen.queryByRole('dialog')).toBeNull() }) it('shows a name conflict and retries by reopening the flow', async () => { const createWorkspace = vi.fn(async () => { throw new WorkspaceCreateError({ code: 'workspace-name-conflict', message: 'project already exists', details: { name: 'project' }, }) }) const b = mount([], createWorkspace) chooseItem('Open local folder…') await act(async () => { b.probe.owner!.onPicked('/one/project') }) await waitFor(() => { expect(screen.getByRole('dialog', { name: 'A workspace with this name already exists' })).toBeTruthy() }) expect(screen.getByRole('alert').textContent).toBe('Choose a folder with a different name.') // The failed adoption withdrew the flow; Choose again reopens it. expect(b.probe.owner!.open).toBe(false) fireEvent.click(screen.getByRole('button', { name: 'Choose again' })) expect(b.probe.owner!.open).toBe(true) expect(b.onPick).not.toHaveBeenCalled() }) it('disables every menu action from flow open through adoption, and reports busy to the flow', async () => { let resolve!: (workspace: WorkspaceView) => void const pending = new Promise((settle) => { resolve = settle }) const created = workspace('adopted') const b = mount([workspace('alpha', 'Alpha')], vi.fn(() => pending)) chooseItem('Open local folder…') // The flow is open but nothing is picked yet: a chooser pending on the // host display must already block concurrent workspace actions. expect(screen.getByRole('menuitem', { name: 'Alpha' }).disabled).toBe(true) expect(screen.getByRole('menuitem', { name: 'Create a new workspace' }).disabled).toBe(true) act(() => { b.probe.owner!.onPicked('/tmp/project') }) expect(b.probe.owner!.busy).toBe(true) expect(screen.getByRole('menuitem', { name: 'Open local folder…' }).disabled).toBe(true) expect(screen.getByRole('menuitem', { name: 'Create a new workspace' }).disabled).toBe(true) await act(async () => { resolve(created); await pending }) expect(b.probe.owner!.busy).toBe(false) }) it('shows the flow-reported failure in the folder-error surface', () => { const b = mount([]) chooseItem('Open local folder…') act(() => { b.probe.owner!.onError('no chooser installed') }) expect(screen.getByRole('alert').textContent).toBe('no chooser installed') expect(screen.queryByTestId('directory-flow')).toBeNull() expect(b.createWorkspace).not.toHaveBeenCalled() }) it('closes a creation modal when the user cancels', () => { mount([]) chooseItem('Create a new workspace') fireEvent.click(screen.getByRole('button', { name: 'Cancel' })) expect(screen.queryByRole('dialog')).toBeNull() }) it('blocks a create-new name already present in the Workspace list', () => { const b = mount([workspace('alpha', 'Alpha')]) chooseItem('Create a new workspace') fireEvent.change(screen.getByLabelText('New workspace name'), { target: { value: ' Alpha ' } }) expect(screen.getByRole('alert').textContent).toBe('A workspace named “Alpha” already exists.') expect(screen.getByRole('button', { name: 'Create workspace' }).disabled).toBe(true) fireEvent.keyDown(screen.getByLabelText('New workspace name'), { key: 'Enter' }) expect(b.createWorkspace).not.toHaveBeenCalled() }) it('does not flash a duplicate alert when the successful create frame arrives before its unary response', async () => { let resolve!: (workspace: WorkspaceView) => void const pending = new Promise((settle) => { resolve = settle }) const created = workspace('fresh', 'same-name') const b = mount([], vi.fn(() => pending)) chooseItem('Create a new workspace') fireEvent.change(screen.getByLabelText('New workspace name'), { target: { value: 'same-name' } }) fireEvent.click(screen.getByRole('button', { name: 'Create workspace' })) b.rerenderItems([created]) expect(screen.getByRole('status').textContent).toBe('Creating workspace…') expect(screen.queryByRole('alert')).toBeNull() await act(async () => { resolve(created); await pending }) expect(b.onPick).toHaveBeenCalledWith(created.workspaceId) }) it('exposes creation phase and error text while retaining the modal for retry', async () => { let reject!: (reason: unknown) => void const pending = new Promise((_resolve, rejectPromise) => { reject = rejectPromise }) const createWorkspace = vi.fn(() => pending) const b = mount([], createWorkspace) chooseItem('Create a new workspace') const input = screen.getByLabelText('New workspace name') fireEvent.keyDown(input, { key: 'ArrowRight' }) fireEvent.change(input, { target: { value: 'broken' } }) fireEvent.click(screen.getByRole('button', { name: 'Create workspace' })) expect(screen.getByRole('status').textContent).toBe('Creating workspace…') fireEvent.keyDown(input, { key: 'Enter' }) expect(createWorkspace).toHaveBeenCalledTimes(1) fireEvent.keyDown(document, { key: 'Escape' }) expect(screen.getByRole('dialog')).toBeTruthy() await act(async () => { reject(new Error('disk unavailable')); await pending.catch(() => {}) }) expect(screen.getByRole('alert').textContent).toBe('Workspace creation failed: disk unavailable') expect(b.view.getByRole('dialog')).toBeTruthy() }) it('reports non-Error creation failures', async () => { const b = mount([], vi.fn(async () => { throw 'permission denied' })) chooseItem('Create a new workspace') // The name field starts empty (no prefill); a name is required to submit. fireEvent.change(screen.getByLabelText('New workspace name'), { target: { value: 'broken' } }) fireEvent.click(screen.getByRole('button', { name: 'Create workspace' })) await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('Workspace creation failed: permission denied') }) expect(b.onPick).not.toHaveBeenCalled() }) it('waits to show its menu until an optional anchor is available', () => { const { renderSlot } = flowProbe() render( true} renderSlot={renderSlot} />, ) expect(screen.queryByRole('menu')).toBeNull() }) it('shows list loading through a stable status surface', () => { const state: WorkspaceListState = { ...workspaceState([]), phase: 'pending', state: 'loading', baselinesReady: false, } const { renderSlot } = flowProbe() render( true} renderSlot={renderSlot} />, ) expect(screen.getByRole('status').textContent).toBe('Loading workspaces…') }) it('hides the folder entry while the directory-flow hole is empty', () => { mount([], vi.fn(), () => false) expect(screen.getByRole('menuitem', { name: 'Create a new workspace' })).toBeTruthy() expect(screen.queryByRole('menuitem', { name: 'Open local folder…' })).toBeNull() }) it('shows the folder entry once the hole reports an occupant on a later render', () => { let occupied = false const b = mount([], vi.fn(), () => occupied) expect(screen.queryByRole('menuitem', { name: 'Open local folder…' })).toBeNull() // A flow package activating after the first paint is observed on the // next render — the same cadence as reopening the menu. occupied = true b.rerenderItems([]) expect(screen.getByRole('menuitem', { name: 'Open local folder…' })).toBeTruthy() }) })