// @vitest-environment jsdom import { Context } from 'cordis' import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen } from '@testing-library/react' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import type { DirectoryListing } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { DirectoryFlowOwnerProps } from '@deepseek-ai/dsh-client-ui-workspace/client' import { apply, BrowseDirectoryFlow, inject } from '../src/client/index.ts' afterEach(cleanup) const HOLES = ['conversation.hero.workspace.directoryFlow', 'sidebar.workspaces.directoryFlow'] as const const HOME = '/home/u' const homeListing: DirectoryListing = { path: HOME, home: HOME, crumbs: [{ name: '/', path: '/', hidden: false }, { name: 'u', path: HOME, hidden: false }], entries: [{ name: 'Documents', path: `${HOME}/Documents`, hidden: false }], } async function bench() { const ctx = new Context() await ctx.plugin(SlotsService).await() ctx.provide('locale', new LocaleService(ctx)) const listDirectory = vi.fn(async (): Promise => homeListing) const createDirectory = vi.fn(async (path: string, name: string) => `${path}/${name}`) ctx.provide('workspaces', { listDirectory, createDirectory } as never) const slots = ctx.get('slots') as SlotsService const declare = () => slots.register({ name: 'root', children: Object.fromEntries(HOLES.map(name => [name, { kind: 'single', scope: 'root' }])), } as never, () => null) return { ctx, slots, listDirectory, createDirectory, declare } } function owner(overrides: Partial = {}): DirectoryFlowOwnerProps { return { open: true, busy: false, onPicked: vi.fn(), onCancel: vi.fn(), onError: vi.fn(), ...overrides, } } describe('directory-picker-browse client half', () => { it('declares the services it drives', () => { expect(inject).toEqual(['slots', 'workspaces', 'locale']) }) it('fills both directory-flow holes for declarations before or after apply, and leaves with its fiber', async () => { const before = await bench() before.declare() const fiber = before.ctx.plugin({ inject: [...inject], apply }) await fiber.await() for (const hole of HOLES) expect(before.slots.entries(hole)).toHaveLength(1) // Registry-contribution disposal proof: the fiber going down empties the holes. await fiber.dispose() for (const hole of HOLES) expect(before.slots.entries(hole)).toHaveLength(0) const after = await bench() await after.ctx.plugin({ inject: [...inject], apply }).await() for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(0) after.declare() await Promise.resolve() for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(1) }) it('registers the dialog dictionaries and binds this package namespace', async () => { const b = await bench() b.declare() await b.ctx.plugin({ inject: [...inject], apply }).await() const entry = b.slots.entries(HOLES[0])[0]! const injected = (entry.inject as () => { t: (key: string) => string })() // zh is the shipped default locale. expect(injected.t('browser.title')).toBe('选择工作区目录') expect(injected.t('browser.newFolder')).toBe('新建文件夹') }) it('drives the injected browse calls through the hole entry', async () => { const b = await bench() b.declare() await b.ctx.plugin({ inject: [...inject], apply }).await() const entry = b.slots.entries(HOLES[1])[0]! const injected = (entry.inject as () => { listDirectory: (path?: string) => Promise createDirectory: (path: string, name: string) => Promise })() await expect(injected.listDirectory()).resolves.toBe(homeListing) await expect(injected.createDirectory(HOME, 'fresh')).resolves.toBe(`${HOME}/fresh`) expect(b.listDirectory).toHaveBeenCalledOnce() expect(b.createDirectory).toHaveBeenCalledWith(HOME, 'fresh') }) it('adapts the owner conversation onto the dialog: confirm picks, dismissal cancels', async () => { const props = owner() const listDirectory = vi.fn(async (): Promise => homeListing) const t = (key: string): string => key render( '')} t={t} />, ) // The dialog opened at home; its confirm (browser.open) adopts the listed level. const openButton = await screen.findByRole('button', { name: 'browser.open' }) openButton.click() expect(props.onPicked).toHaveBeenCalledWith(HOME) screen.getByRole('button', { name: 'browser.cancel' }).click() expect(props.onCancel).toHaveBeenCalled() expect(props.onError).not.toHaveBeenCalled() }) it('renders nothing while the flow is closed', () => { const view = render( homeListing)} createDirectory={vi.fn(async () => '')} t={key => key} />, ) expect(view.container.innerHTML).toBe('') }) })