| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- // Shared scaffolding for the assembled-jsdom snapshots: the real built
- // workspace `lib/client.js` artifacts booted through AppWebEntry's
- // ModuleLoader path (loadBundle) against the keyless FixtureApiClient
- // transport. Every file that mounts this graph needs the same boot entry list,
- // the same bundle map, the same jsdom globals, and the same mount call, and
- // differs only in what it asserts afterwards, so the scaffolding lives here.
- //
- // Keyless and deterministic: the fixture is the fake server, so nothing here
- // reaches a model or the network.
- import { readFileSync } from 'node:fs'
- import { createRequire } from 'node:module'
- import { dirname, join, resolve } from 'node:path'
- import { pathToFileURL } from 'node:url'
- import { act, cleanup } from '@testing-library/react'
- import { afterEach, beforeEach, vi } from 'vitest'
- import { bootInjections, orderByModuleGraph } from '@deepseek-ai/dsh-client-modules'
- import type { ClientModuleLoaderTarget, WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
- import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
- interface AssembledPlugin extends WebBootEntry {
- /** Absolute path to the built client artifact declared by this package. */
- bundlePath: string
- }
- interface AssembledBootOptions {
- /** Package ids omitted from this mounted composition. */
- readonly exclude?: readonly string[]
- }
- interface ClientPackageManifest {
- name?: string
- exports?: Record<string, string | { default?: string }>
- dsh?: {
- client?: {
- platform?: string
- inject?: string[]
- external?: string[]
- immediately?: boolean
- }
- }
- }
- interface ComposedEntry {
- name?: unknown
- disabled?: unknown
- }
- interface BootComposition {
- loadOverlayPatches(binName: string, file: string): unknown[]
- composeEntries(layers: readonly unknown[][]): ComposedEntry[]
- }
- const REPO_ROOT = process.cwd()
- const BUNDLE_LAYERS = [
- {
- manifest: join(REPO_ROOT, 'packages/bundle/base/package.json'),
- patch: join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml'),
- },
- {
- manifest: join(REPO_ROOT, 'packages/bundle/web-app/package.json'),
- patch: join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml'),
- },
- ] as const
- const bundleResolvers = BUNDLE_LAYERS.map(layer => createRequire(layer.manifest))
- const webBundleResolver = bundleResolvers[1]
- if (webBundleResolver === undefined) throw new Error('assembled boot: web bundle resolver missing')
- const appBoot = await import(pathToFileURL(webBundleResolver.resolve('@deepseek-ai/dsh-app-boot')).href) as unknown as BootComposition
- function resolvePackageManifest(specifier: string): string | undefined {
- for (const require of bundleResolvers) {
- try {
- return require.resolve(`${specifier}/package.json`)
- } catch {
- continue
- }
- }
- return undefined
- }
- function resolveClientExport(packagePath: string, pkg: ClientPackageManifest): string {
- const declared = pkg.exports?.['./client']
- const relative = typeof declared === 'string' ? declared : declared?.default
- if (relative === undefined) {
- throw new Error(`assembled boot: ${pkg.name ?? packagePath} declares dsh.client without a ./client export`)
- }
- return resolve(dirname(packagePath), relative)
- }
- /** Derive the assembled browser graph from the same bundle patches and package declarations as `dsh web`. */
- function loadAssembledPlugins(): readonly AssembledPlugin[] {
- const entries = appBoot.composeEntries(BUNDLE_LAYERS.map(layer =>
- appBoot.loadOverlayPatches('assembled boot', layer.patch)))
- const plugins = new Map<string, AssembledPlugin>()
- for (const entry of entries) {
- if (entry.disabled === true || typeof entry.name !== 'string') continue
- const packagePath = resolvePackageManifest(entry.name)
- if (packagePath === undefined) continue
- const pkg = JSON.parse(readFileSync(packagePath, 'utf8')) as ClientPackageManifest
- const declaration = pkg.dsh?.client
- if (declaration?.platform !== 'web') continue
- if (pkg.name !== entry.name) {
- throw new Error(`assembled boot: ${entry.name} resolved package ${pkg.name ?? '<unnamed>'}`)
- }
- plugins.set(entry.name, {
- id: entry.name,
- bundlePath: resolveClientExport(packagePath, pkg),
- url: `/plugins/${entry.name}/client.js?rev=fx`,
- rev: 'fx',
- ...(declaration.inject === undefined ? {} : { inject: declaration.inject }),
- ...(declaration.external === undefined ? {} : { external: declaration.external }),
- ...(declaration.immediately === true ? { immediately: true } : {}),
- })
- }
- return orderByModuleGraph([...plugins.values()]).map(({ id }) => {
- const plugin = plugins.get(id)
- /* v8 ignore next -- orderByModuleGraph returns the input row identities */
- if (plugin === undefined) throw new Error(`assembled boot: ordered unknown client package ${id}`)
- return plugin
- })
- }
- const PLUGINS = loadAssembledPlugins()
- const bundles = new Map(PLUGINS.map(plugin => [
- plugin.url,
- readFileSync(plugin.bundlePath, 'utf8'),
- ]))
- interface FixtureWindow extends Window {
- __DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
- __ModuleLoader__?: ClientModuleLoaderTarget
- }
- class ResizeObserverStub {
- observe(): void {}
- disconnect(): void {}
- unobserve(): void {}
- }
- class EventSourceStub {
- addEventListener(): void {}
- close(): void {}
- }
- const win = window as FixtureWindow
- let unmount: (() => Promise<void>) | undefined
- /**
- * Register the per-test jsdom setup and teardown the assembled boot needs:
- * English pinned before boot so role/text locators stay deterministic across
- * localized component migrations (the newEnglishPage e2e convention), the
- * observers and frame callbacks jsdom lacks, and a full reset of the document,
- * the boot globals, and the injected plugin styles afterwards.
- */
- export function installAssembledBootEnv(): void {
- beforeEach(() => {
- localStorage.clear()
- // The locale service derives its provisional locale from the browser and
- // takes an explicit choice only from Host settings, which this lane's
- // fixture transport does not serve; pinning the navigator is what selects
- // English here.
- Object.defineProperty(navigator, 'languages', { value: ['en-US'], configurable: true })
- Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true })
- document.title = 'DeepSeek Harness'
- vi.stubGlobal('ResizeObserver', ResizeObserverStub)
- vi.stubGlobal('EventSource', EventSourceStub)
- vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
- setTimeout(() => { callback(0) }, 0) as unknown as number)
- vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
- })
- afterEach(async () => {
- await act(async () => { await unmount?.() })
- unmount = undefined
- cleanup()
- delete win.__DSH_BOOT__
- delete win.__ModuleLoader__
- document.body.innerHTML = ''
- document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
- document.title = ''
- history.replaceState(null, '', '/')
- // Deleting the own properties uncovers jsdom's own accessors again
- // (Navigator declares both readonly, hence the erased receiver).
- const ownNavigator = navigator as unknown as Record<string, unknown>
- delete ownNavigator.languages
- delete ownNavigator.language
- vi.unstubAllGlobals()
- })
- }
- /**
- * Mount the assembled application on the fixture transport; the teardown
- * registered by installAssembledBootEnv disposes it.
- * @param search - fixture query string used to select deterministic host behavior.
- * @param options - composition changes applied to this mount.
- */
- export function mountAssembledApp(search = '?fixture', options: AssembledBootOptions = {}): void {
- const excluded = new Set(options.exclude)
- const plugins = PLUGINS.filter(plugin => !excluded.has(plugin.id))
- history.replaceState(null, '', `/${search}`)
- const root = document.createElement('div')
- root.id = 'root'
- document.body.appendChild(root)
- win.__DSH_BOOT__ = { rev: 'fx', entries: plugins.map(({ bundlePath: _bundlePath, ...plugin }) => plugin) }
- const [facadeRow] = bootInjections(win.__DSH_BOOT__)
- if (facadeRow?.kind !== 'script') throw new Error('missing injected ModuleLoader facade row')
- ;(0, eval)(facadeRow.text)
- // Mirror the blocking Host-injected modules script before the Vite entry calls create().
- const modules = plugins.find(candidate => candidate.id === '@deepseek-ai/dsh-client-modules')
- if (modules === undefined) throw new Error('missing parser-preloaded fixture row @deepseek-ai/dsh-client-modules')
- const modulesCode = bundles.get(modules.url)
- if (modulesCode === undefined) throw new Error(`missing built bundle ${modules.url}`)
- ;(0, eval)(modulesCode)
- act(() => {
- const entry = new AppWebEntry(root, {
- loadBundle: async (url) => {
- const code = bundles.get(url)
- if (code === undefined) throw new Error(`missing built bundle ${url}`)
- ;(0, eval)(code)
- },
- })
- void entry.run()
- unmount = () => entry.dispose()
- })
- }
- /**
- * Match a CSS-module class by its logical name.
- * Module class names carry a per-build hash in one of two schemes —
- * ui-primitives emits `_<name>_<hash>` (name bounded by underscores),
- * feature bundles emit `<hash>_<name>` (name at the end) — and a longer name
- * containing this one must not match (`line` must not hit `lineNumber`).
- * @param el - element whose class list is inspected.
- * @param name - logical (unhashed) module class name.
- * @returns whether the element carries that module class.
- */
- export function hasClass(el: Element, name: string): boolean {
- return [...el.classList].some(cls => cls === name || cls.endsWith(`_${name}`) || cls.startsWith(`_${name}_`) || cls.includes(`_${name}_`))
- }
- /**
- * Whether this run rewrites its golden instead of comparing against it, set by
- * the snapshot gate's `DSH_SNAPSHOT` mode (`record` re-runs the scenarios from
- * scratch, `refresh` re-derives the expected text from the existing ones).
- */
- export const REFRESHING_GOLDEN = process.env.DSH_SNAPSHOT === 'record' || process.env.DSH_SNAPSHOT === 'refresh'
|