mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-13 04:03:30 +00:00
test(client): support panel selection in slot runtime
This commit is contained in:
@@ -54,6 +54,7 @@
|
||||
"@deepseek-ai/dsh-client-store": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-chat": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
|
||||
@@ -29,8 +29,10 @@ import {
|
||||
apply as applyUiSession, inject as uiSessionInject,
|
||||
} from '@deepseek-ai/dsh-client-ui-session/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { PanelInfo } from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import type {
|
||||
ChildrenDecl, ComposedProps, HostObservable, OwnerOf, SlotComponent, SlotMap, SlotRenderer,
|
||||
ChildrenDecl, ComposedProps, HostObservable, OwnerOf, RenderOpts, SlotComponent, SlotMap, SlotRenderer,
|
||||
SlotRendererHost, SnapshotSelectorHook, StoreInstanceLike,
|
||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { registerDomSnapshotSerializer } from './snapshot.ts'
|
||||
@@ -125,7 +127,7 @@ export interface TestFileUpload {
|
||||
* {@link SlotView.update} drive React through the standard uSES boundary.
|
||||
*/
|
||||
class OwnerPropsCell {
|
||||
private readonly owners = new Map<string, object>()
|
||||
private readonly owners = new Map<string, { owner: object; opts: RenderOpts | undefined }>()
|
||||
private readonly listeners = new Set<() => void>()
|
||||
private version = 0
|
||||
|
||||
@@ -147,15 +149,16 @@ class OwnerPropsCell {
|
||||
* caller wraps in act).
|
||||
* @param key - slot key.
|
||||
* @param owner - owner props share.
|
||||
* @param opts - explicit keyed or list selection for the render site.
|
||||
*/
|
||||
set(key: string, owner: object): void {
|
||||
this.owners.set(key, owner)
|
||||
set(key: string, owner: object, opts?: RenderOpts): void {
|
||||
this.owners.set(key, { owner, opts })
|
||||
this.version += 1
|
||||
for (const fn of [...this.listeners]) fn()
|
||||
}
|
||||
|
||||
/** Keys with supplied owner props, in first-supply order. */
|
||||
entries(): readonly (readonly [string, object])[] {
|
||||
entries(): readonly (readonly [string, { owner: object; opts: RenderOpts | undefined }])[] {
|
||||
return [...this.owners.entries()]
|
||||
}
|
||||
}
|
||||
@@ -217,6 +220,8 @@ export class SlotTestRuntime {
|
||||
readonly sessions: TestSessions
|
||||
/** Workspaces double (list observable, recorded intent actions). */
|
||||
readonly workspaces: TestWorkspaces
|
||||
/** Test-owned panel selection used by the framework's usePanelInfo hook. */
|
||||
readonly panelInfo = createSnapshotStore<PanelInfo>({ activePanelId: null })
|
||||
/** Mutable file-upload stub; replace `upload` in suites that exercise the capability. */
|
||||
readonly fileUpload: TestFileUpload
|
||||
|
||||
@@ -233,6 +238,7 @@ export class SlotTestRuntime {
|
||||
private readonly autoDeclared = new Set<string>()
|
||||
private autoRootView: RenderResult | undefined
|
||||
private readonly disposeWorkspaceSource: () => void
|
||||
private readonly disposePanelInfoSource: () => void
|
||||
|
||||
private constructor(ctx: Context, slots: SlotRegistry) {
|
||||
this.ctx = ctx
|
||||
@@ -248,6 +254,7 @@ export class SlotTestRuntime {
|
||||
ctx.provide('workspaces', this.workspaces)
|
||||
ctx.provide('fileUpload', this.fileUpload as never)
|
||||
this.disposeWorkspaceSource = slots.provideRoot({ hooks: { workspaces: this.workspaces.list } })
|
||||
this.disposePanelInfoSource = slots.provideRoot({ hooks: { panelInfo: this.panelInfo } })
|
||||
// Capturing install: the production renderer does the rendering; the
|
||||
// wrapper only takes the host face for storeOf (no machinery copied).
|
||||
const renderer = createSlotRenderer()
|
||||
@@ -309,6 +316,11 @@ export class SlotTestRuntime {
|
||||
this.disposeWorkspaceSource()
|
||||
}
|
||||
|
||||
/** Release the default panel hook before mounting the production Layout owner. */
|
||||
releasePanelInfoSource(): void {
|
||||
this.disposePanelInfoSource()
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the root slot tree through the ctx-level entry (the shell's own
|
||||
* entry point): `ctx.slots.renderSlot('root', {})` under Testing Library.
|
||||
@@ -333,13 +345,13 @@ export class SlotTestRuntime {
|
||||
async declare(children: ChildrenDecl): Promise<void> {
|
||||
for (const key of Object.keys(children)) this.autoDeclared.add(key)
|
||||
const cell = this.ownerCell
|
||||
const AutoFrame = (props: { renderSlot: (key: string, owner: object) => ReactNode }) => {
|
||||
const AutoFrame = (props: { renderSlot: (key: string, owner: object, opts?: RenderOpts) => ReactNode }) => {
|
||||
useSyncExternalStore(cell.subscribe, cell.getVersion)
|
||||
// Keyed Fragments only: the renderer's outlet anchor is the one
|
||||
// `[data-slot]` element — the frame adding its own would nest
|
||||
// duplicate anchors under the same key.
|
||||
return createElement(Fragment, null, cell.entries().map(([key, owner]) =>
|
||||
createElement(Fragment, { key }, props.renderSlot(key, owner))))
|
||||
return createElement(Fragment, null, cell.entries().map(([key, { owner, opts }]) =>
|
||||
createElement(Fragment, { key }, props.renderSlot(key, owner, opts))))
|
||||
}
|
||||
await this.root.declare(children as never, AutoFrame as never)
|
||||
}
|
||||
@@ -352,16 +364,17 @@ export class SlotTestRuntime {
|
||||
* slot of the same tree.
|
||||
* @param key - a key declared through {@link SlotTestRuntime.declare}.
|
||||
* @param owner - owner props share for the render site.
|
||||
* @param opts - explicit keyed or list selection; retained by view updates.
|
||||
* @returns the slot-local view (snapshot container, scoped queries, owner updates).
|
||||
*/
|
||||
renderSlot<K extends keyof SlotMap & string>(key: K, owner: OwnerOf<K>): SlotView<K> {
|
||||
renderSlot<K extends keyof SlotMap & string>(key: K, owner: OwnerOf<K>, opts?: RenderOpts): SlotView<K> {
|
||||
if (!this.autoDeclared.has(key)) {
|
||||
throw new Error(`renderSlot('${key}') without declare() — declare the key first (or use root.declare for a custom frame)`)
|
||||
}
|
||||
const install = (next: object): void => {
|
||||
// Synchronous cell write inside act: the frame re-renders through uSES.
|
||||
act(() => {
|
||||
this.ownerCell.set(key, next)
|
||||
this.ownerCell.set(key, next, opts)
|
||||
})
|
||||
}
|
||||
install(owner)
|
||||
@@ -421,6 +434,7 @@ export class SlotTestRuntime {
|
||||
for (const view of this.views.splice(0)) view.unmount()
|
||||
for (const handle of this.handles.splice(0)) await handle.dispose()
|
||||
this.root.release()
|
||||
this.disposePanelInfoSource()
|
||||
await this.sessions.disposeScopes()
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-ui-chat/client'
|
||||
import { EMPTY_CONVERSATION_SNAPSHOT } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { MainPanelId, PanelInfo } from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
bindSnapshotSelector,
|
||||
@@ -17,6 +19,12 @@ import {
|
||||
const originalLanguages = [...navigator.languages]
|
||||
const originalLanguage = navigator.language
|
||||
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface SlotMap {
|
||||
'trt.panel-info': { kind: 'keyed'; scope: 'root'; owner: { label: string } }
|
||||
}
|
||||
}
|
||||
|
||||
usePinnedBrowserLanguages('zh-CN', 'en-US')
|
||||
afterEach(cleanup)
|
||||
afterAll(() => {
|
||||
@@ -38,6 +46,34 @@ function entry(seq: number): SessionLiveEventEntry {
|
||||
}
|
||||
|
||||
describe('fixture helpers', () => {
|
||||
it('drives panel hooks, retains keyed selection on owner updates, and releases the default source', async () => {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
try {
|
||||
await runtime.declare({ 'trt.panel-info': { kind: 'keyed', scope: 'root' } })
|
||||
runtime.slots.register({ name: 'trt.panel-info', key: 'probe' },
|
||||
({ usePanelInfo, label }: PropsRuntime<'trt.panel-info'>) => (
|
||||
<span>{label}:{usePanelInfo(info => info.activePanelId) ?? 'conversation'}</span>
|
||||
))
|
||||
const view = runtime.renderSlot('trt.panel-info', { label: 'first' }, { entryKey: 'probe' })
|
||||
expect(view.container.textContent).toBe('first:conversation')
|
||||
act(() => { runtime.panelInfo.set({ activePanelId: 'custom' as MainPanelId }) })
|
||||
expect(view.container.textContent).toBe('first:custom')
|
||||
view.update({ label: 'next' })
|
||||
expect(view.container.textContent).toBe('next:custom')
|
||||
const replacement = createSnapshotStore<PanelInfo>({ activePanelId: null })
|
||||
await act(async () => {
|
||||
runtime.releasePanelInfoSource()
|
||||
await runtime.mount({
|
||||
inject: ['slots'],
|
||||
apply(ctx) { ctx.slots.provideRoot({ hooks: { panelInfo: replacement } }) },
|
||||
})
|
||||
})
|
||||
expect(view.container.textContent).toBe('next:conversation')
|
||||
} finally {
|
||||
await runtime.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an upload until a suite replaces the default stub', async () => {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
expect(runtime.fileUpload.available).toBe(false)
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
{
|
||||
"path": "../../client/ui-conversation"
|
||||
},
|
||||
{
|
||||
"path": "../../client/ui-layout"
|
||||
},
|
||||
{
|
||||
"path": "../../client/ui-slots"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -9900,6 +9900,9 @@ importers:
|
||||
'@deepseek-ai/dsh-client-ui-conversation':
|
||||
specifier: workspace:^
|
||||
version: link:../../client/ui-conversation
|
||||
'@deepseek-ai/dsh-client-ui-layout':
|
||||
specifier: workspace:^
|
||||
version: link:../../client/ui-layout
|
||||
'@deepseek-ai/dsh-client-ui-renderer':
|
||||
specifier: workspace:^
|
||||
version: link:../../client/ui-renderer
|
||||
|
||||
Reference in New Issue
Block a user