// @vitest-environment jsdom
/**
* Integration against the real ui-slots SlotCore (T1): the outlet's uSES
* pairing rides the real subscribe/getVersion/entries surfaces, and the
* whitelist narrows at compile time (expect-error negative samples).
*/
import { describe, expect, it, vi } from 'vitest'
import { act, render } from '@testing-library/react'
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
import { scopedSlots } from '@deepseek-ai/dsh-client-web-react'
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
'spec.single': { kind: 'single'; scope: 'root'; props: { label?: string } }
'spec.list': { kind: 'list'; scope: 'root'; props: object }
'spec.off-limits': { kind: 'single'; scope: 'root'; props: object }
}
}
describe('scopedSlots over the real SlotCore', () => {
it('renders registrations live: define, register, dispose back to fallback', async () => {
const core = new SlotCore()
core.define('spec.single', { kind: 'single', scope: 'root' })
const slots = scopedSlots(core, 'spec.single')
const view = render(<>{slots.renderSlot('spec.single', {}, { fallback: none })}>)
expect(view.container.textContent).toBe('none')
let dispose = () => {}
// The real core batches subscriber notification per microtask: async act.
await act(async () => { dispose = core.register('spec.single', ({ label }) => {label ?? 'on'}) })
expect(view.container.textContent).toBe('on')
await act(async () => { dispose(); dispose() }) // disposer is idempotent in the real core
expect(view.container.textContent).toBe('none')
})
it('passes owner props through and orders list entries', () => {
const core = new SlotCore()
core.define('spec.single', { kind: 'single', scope: 'root' })
core.define('spec.list', { kind: 'list', scope: 'root' })
core.register('spec.single', ({ label }) => {label})
core.register('spec.list', () => 2, { id: 'two', order: 2 })
core.register('spec.list', () => 1, { id: 'one', order: 1 })
const slots = scopedSlots(core, 'spec.single', 'spec.list')
const view = render(
<>
{slots.renderSlot('spec.single', { label: 'owner' })}
{slots.renderSlot('spec.list', {})}
>,
)
expect(view.container.textContent).toBe('owner12')
})
it('fails loud when rendering a key that was never defined', () => {
const core = new SlotCore()
const slots = scopedSlots(core, 'spec.single')
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
expect(() => render(<>{slots.renderSlot('spec.single', {})}>)).toThrow(/before define/)
spy.mockRestore()
})
it('narrows the whitelist at compile time and backstops at runtime', () => {
const core = new SlotCore()
core.define('spec.single', { kind: 'single', scope: 'root' })
core.define('spec.off-limits', { kind: 'single', scope: 'root' })
const slots = scopedSlots(core, 'spec.single')
// @ts-expect-error spec.off-limits is outside this ScopedSlots whitelist
expect(() => slots.renderSlot('spec.off-limits', {})).toThrow(/whitelist/)
// @ts-expect-error unknown keys are rejected even before whitelist narrowing
expect(() => slots.renderSlot('spec.nonexistent', {})).toThrow(/whitelist/)
})
})