Every client file reached hooks and element types through the central
require seam (src/client/react.ts) and spelled them React.useState /
ReactNS.ReactElement. Import the members directly instead — named value
imports for hooks, inline type-only imports for element types — and let
the bundler own the single require('react'), which is exactly what the
platform module table answers.
This needs JSX off the classic runtime (it required a React identifier
in every tsx scope): tsconfig moves to jsx: react-jsx, so the bundle
sources jsx from 'react/jsx-runtime', already seeded by every supported
harness (verified against the dsh-v0.1.2-rc.1 platform.ts). The bundle
smoke's fake module table gains the specifier to stay a faithful shell
stand-in. h() survives as a local createElement alias at the slot
render-prop seams and the lightbox portal imports createPortal from
react-dom directly.
Verified: the built client.js requires react, react/jsx-runtime, and
react-dom exactly once each; the compat lane (bundle smoke + the
real-baseline matrix) passes against the fresh artifacts; the full gate
stays green with per-file coverage at 100%.
157 lines
8.4 KiB
TypeScript
157 lines
8.4 KiB
TypeScript
// The bundle smoke, as part of the `compat` project: the only check that
|
|
// exercises the BUILT artifacts (lib/client.js) — the packaging glue (the
|
|
// `window.__ModuleLoader__.load({id, factory})` handoff, the CSS channel's
|
|
// data-plugin style tags, platform-module requires) only exists after tsdown
|
|
// runs. Skipped cleanly when lib/ is absent (run `pnpm run build` first);
|
|
// the release workflow builds before `pnpm test`, so it always runs there.
|
|
//
|
|
// Everything here runs against the REAL bundle in its own JSDOM with REAL
|
|
// React and the REAL ui-primitives — only the harness services
|
|
// (locale/slots/effects) are in-memory implementations of their documented
|
|
// contracts.
|
|
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { beforeAll, describe, test } from 'vitest'
|
|
import { JSDOM } from 'jsdom'
|
|
import React from 'react'
|
|
import ReactDOM from 'react-dom'
|
|
import * as jsxRuntime from 'react/jsx-runtime'
|
|
import * as staging from './staging'
|
|
|
|
describe.skipIf(staging.artifactsMissing())('bundle smoke — the built lib/client.js', () => {
|
|
const dom = new JSDOM('<!doctype html><html><head></head><body></body></html>', { pretendToBeVisual: true })
|
|
// The bundle evaluates against window-scoped globals; this spec owns its
|
|
// worker (per-file fork), so the assignment stays file-local.
|
|
const globals = globalThis as { window?: unknown; document?: unknown }
|
|
|
|
/** The factory handoff the bundle registers through. */
|
|
let handoff: { id: string; factory: (require: (spec: string) => unknown) => unknown } | null = null
|
|
/** Everything the assertions below look at, captured by the setup. */
|
|
const state: {
|
|
plugin?: { name: string; inject: string[]; apply(ctx: unknown): void }
|
|
dicts: Map<string, Record<string, Record<string, string>>>
|
|
slots: [string, { order?: number; id?: string; label?: () => string; inject?: (sessionId?: string) => unknown }][]
|
|
sources: { trigger: string }[]
|
|
disposers: (() => void)[]
|
|
} = { dicts: new Map(), slots: [], sources: [], disposers: [] }
|
|
|
|
const styleTags = (): HTMLElement[] =>
|
|
[...dom.window.document.head.querySelectorAll<HTMLElement>('style[data-plugin="dsh-context"]')]
|
|
|
|
beforeAll(() => {
|
|
globals.window = dom.window
|
|
globals.document = dom.window.document
|
|
|
|
// The module table: the shell seeds exactly these specifiers. The bundle
|
|
// must require nothing else (the tsdown purity gate pins this too). The
|
|
// primitives dist ships CSS modules Node cannot parse — the bundle only
|
|
// references its components as React leaves, so inert stand-ins play the
|
|
// shell's role here (their real rendering is covered by the vitest
|
|
// jsdom lane).
|
|
const inert = (name: string) => {
|
|
const C = (): null => null
|
|
;(C as { displayName?: string }).displayName = name
|
|
return C
|
|
}
|
|
const moduleTable = (spec: string): unknown => {
|
|
if (spec === 'react') return React
|
|
if (spec === 'react/jsx-runtime') return jsxRuntime
|
|
if (spec === 'react-dom') return ReactDOM
|
|
if (spec === '@deepseek-ai/dsh-client-ui-primitives') {
|
|
return new Proxy({}, { get: (_t, key) => (typeof key === 'string' ? inert(key) : undefined) })
|
|
}
|
|
throw new Error(`bundle required a non-platform module: ${spec}`)
|
|
}
|
|
|
|
;(dom.window as { __ModuleLoader__: unknown }).__ModuleLoader__ = { load: (h: typeof handoff) => { handoff = h } }
|
|
new Function(readFileSync(join(staging.REPO, 'lib', 'client.js'), 'utf8'))()
|
|
assert.ok(handoff !== null, 'bundle must register through __ModuleLoader__.load')
|
|
|
|
// Faithful harness services (documented contracts, in-memory).
|
|
const removeFrom = <T,>(list: T[]) => (item: T) => () => {
|
|
const i = list.indexOf(item)
|
|
if (i >= 0) list.splice(i, 1)
|
|
}
|
|
const ctx: Record<string, unknown> = {
|
|
get(name: string) {
|
|
if (name === 'inputTriggers') return { registerSource: (s: { trigger: string }) => { state.sources.push(s); return removeFrom(state.sources)(s) } }
|
|
if (name === 'sessions') return { scope: () => undefined }
|
|
return undefined
|
|
},
|
|
inject(_deps: string[], cb: (c: unknown) => void) {
|
|
// Cordis fires the callback once every requested service exists; this
|
|
// fake answers every name it knows up front, so it fires immediately.
|
|
// The settingsScope wiring guards on the absent binder itself.
|
|
cb(ctx)
|
|
},
|
|
effect(fn: () => (() => void) | void) {
|
|
const d = typeof fn === 'function' ? fn() : undefined
|
|
if (typeof d === 'function') state.disposers.push(d)
|
|
return d
|
|
},
|
|
locale: {
|
|
register: (ns: string, d: Record<string, Record<string, string>>) => { state.dicts.set(ns, d); return () => state.dicts.delete(ns) },
|
|
bind: (ns: string) => (key: string, vars?: Record<string, string | number>) => {
|
|
let s = state.dicts.get(ns)?.zh[key] ?? key
|
|
if (vars) for (const k in vars) s = s.replace('{' + k + '}', String(vars[k]))
|
|
return s
|
|
},
|
|
},
|
|
slots: {
|
|
inject: (name: string, fn: () => unknown) => { state.slots.push([name, fn() as (typeof state.slots)[number][1]]) },
|
|
register: (opts: (typeof state.slots)[number][1]) => opts,
|
|
},
|
|
}
|
|
state.plugin = handoff.factory(moduleTable) as typeof state.plugin
|
|
state.plugin.apply(ctx)
|
|
})
|
|
|
|
test('handoff: the bundle registers as dsh-context with the factory closure', () => {
|
|
assert.ok(handoff !== null, 'bundle must register through __ModuleLoader__.load')
|
|
assert.equal(handoff.id, 'dsh-context', 'handoff id is the package name')
|
|
assert.equal(typeof handoff.factory, 'function')
|
|
assert.equal(state.plugin?.name, 'dsh-context')
|
|
assert.deepEqual(state.plugin?.inject, ['slots', 'locale'])
|
|
})
|
|
|
|
test('CSS channel: plugin-owned style tags, one per sheet, minified', () => {
|
|
assert.ok(styleTags().length > 1, 'plugin-owned <style data-plugin> tags injected at factory execution')
|
|
const tagIds = styleTags().map(tag => tag.dataset.pluginCss)
|
|
assert.ok(tagIds.every(id => typeof id === 'string' && id.startsWith('dsh-context/')), 'official data-plugin-css tag ids')
|
|
assert.equal(new Set(tagIds).size, tagIds.length, 'one tag per stylesheet, no duplicate tag ids')
|
|
const css = styleTags().map(tag => tag.textContent).join('\n')
|
|
for (const marker of ['.lc-root', '.lc-br-elem-row', '.lc-stacked-seg', '.lc-bar-tip-on', '.lc-stat-tip', '.lc-occupied-box-on']) {
|
|
assert.ok(css.includes(marker), `styles carry ${marker}`)
|
|
}
|
|
assert.ok(!css.includes('120ms ease'), 'lightningcss minified the sheet')
|
|
})
|
|
|
|
test('registrations: bilingual dictionaries, four slots, the /context trigger source', () => {
|
|
assert.ok(state.dicts.get('dsh-context')?.zh && state.dicts.get('dsh-context')?.en, 'bilingual dictionaries registered')
|
|
assert.equal(state.slots.length, 4, 'view tab + assistant action + composer dock + input overlay slots')
|
|
assert.equal(state.slots[0]?.[0], 'conversation.view')
|
|
assert.equal(state.slots[0]?.[1].order, 20)
|
|
assert.equal(state.slots[0]?.[1].label?.(), '上下文', 'tab label localized')
|
|
assert.equal(state.slots[1]?.[0], 'conversation.chat.assistant-actions')
|
|
assert.equal(state.slots[1]?.[1].id, 'context-jump', 'jump action rides its own slot id')
|
|
assert.equal(state.slots[2]?.[0], 'conversation.composer.dock')
|
|
assert.equal(state.slots[2]?.[1].id, 'stats-jump', 'stats-line jump rides its own slot id')
|
|
assert.equal(state.slots[2]?.[1].order, 10, 'below the shipped stats entry (order 0)')
|
|
assert.equal(state.slots[3]?.[0], 'conversation.input.overlay')
|
|
const overlayInject = state.slots[3]?.[1].inject
|
|
const overlayHooks = overlayInject?.('s1') as { hooks: { contextModal: { getSnapshot: unknown } } } | undefined
|
|
assert.equal(typeof overlayHooks?.hooks.contextModal.getSnapshot, 'function', 'overlay hooks carry the modal store')
|
|
assert.equal(state.sources.length, 1, '/context trigger source registered')
|
|
assert.equal(state.sources[0]?.trigger, '/', 'trigger is the slash')
|
|
})
|
|
|
|
test('HMR safety: fiber dispose removes every registration; the style tags survive it', () => {
|
|
for (const d of state.disposers) d()
|
|
assert.equal(state.dicts.size, 0, 'dictionaries unregistered on dispose')
|
|
assert.equal(state.sources.length, 0, 'trigger source removed on dispose')
|
|
assert.ok(styleTags().length > 0, 'style tags survive fiber dispose (the HMR receiver claims them)')
|
|
})
|
|
})
|