// The built artifact, exercised the way the page exercises it: the module table // calls `window.__ModuleLoader__.load`, materializes the factory, and hands the // result to Cordis. This runs under Node with stubs, which is exactly the point: // the bundle may not need a DOM before its components render, and `require` may // only ever ask for modules the module table actually holds. // // The freshness case is the guard against the one real maintenance hazard of // this layout: editing src/client/** and committing without rebuilding. import { test } from 'node:test' import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') const artifactPath = resolve(repoRoot, 'client.js') const source = await readFile(artifactPath, 'utf8') /** The smallest React the bundle can be handed: property access only. */ const reactStub = { createElement: () => null, Fragment: Symbol('Fragment'), useCallback: (callback) => callback, useEffect: () => {}, useSyncExternalStore: () => undefined, } /** Load the artifact against a stub module table and return its registration. */ function materialize(moduleTable = { react: reactStub }) { let registration const previous = globalThis.window globalThis.window = { __ModuleLoader__: { load: (value) => { registration = value } } } try { // eslint-disable-next-line no-new-func -- executing the artifact IS the test new Function(source)() } finally { globalThis.window = previous } assert.ok(registration, 'the bundle never registered a factory') const requested = [] const exports = registration.factory((specifier) => { requested.push(specifier) if (!Object.hasOwn(moduleTable, specifier)) { throw new Error(`the bundle asked the module table for "${specifier}"`) } return moduleTable[specifier] }) return { registration, exports, requested } } /** The smallest client root context that lets `apply` run without a page. */ function fakeContext() { const effects = [] const slots = [] const registrations = [] return { effects, slots, registrations, ctx: { get: () => undefined, effect: (callback, label) => { effects.push(label) return () => {} }, slots: { inject: (name, setup) => { slots.push(name) setup() }, register: (definition, component) => { registrations.push({ definition, component }) return { dispose() {} } }, }, }, } } test('the artifact is a generated, self-contained script', () => { assert.match(source.split('\n')[0], /^\/\/ Generated by scripts\/build-client\.mjs/) assert.ok(!source.includes('\r'), 'the artifact must be LF-only (see .gitattributes)') assert.ok(source.includes('window.__ModuleLoader__.load({'), 'missing the registration wrapper') assert.ok(source.includes("id: '@dsh-plugin/session-notify',"), 'the registration id must be the package name') assert.ok(source.trimEnd().endsWith('})'), 'the registration wrapper is not closed') const factoryAt = source.indexOf('factory(require) {') assert.ok(factoryAt > 0, 'the factory is missing') const requires = [...source.matchAll(/\brequire\(\s*"([^"]+)"\s*\)/g)] assert.ok(requires.length > 0, 'the bundle never requires react') for (const [whole, specifier] of requires) { assert.equal(specifier, 'react', 'only platform seeds may be requested synchronously') assert.ok(source.indexOf(whole) > factoryAt, 'require must sit inside the factory, or it would load eagerly') } assert.ok(!/^\s*(?:import|export)\s/m.test(source), 'a self-contained bundle has no top-level import/export') }) test('materializing the factory yields the plugin the module table expects', () => { const { registration, exports, requested } = materialize() assert.equal(registration.id, '@dsh-plugin/session-notify') assert.deepEqual(exports.inject, ['slots', 'locale']) assert.equal(typeof exports.apply, 'function') assert.deepEqual([...new Set(requested)], ['react']) }) test('applying the plugin registers its three seats without touching the DOM', () => { const { exports } = materialize() const { ctx, effects, slots, registrations } = fakeContext() exports.apply(ctx) assert.deepEqual(effects, [ 'session-notify: dictionaries', 'session-notify: window focus', 'session-notify: disposal', ]) assert.deepEqual(slots, ['shell.overlay', 'plugins.bundle.config', 'shell.overlay']) assert.deepEqual(registrations.map(({ definition }) => definition.id ?? definition.key), [ 'session-notify-toasts', '@dsh-plugin/session-notify', 'session-notify-watch', ]) assert.deepEqual(registrations.map(({ definition }) => definition.order), [100, undefined, 101]) for (const { component } of registrations) assert.equal(typeof component, 'function') const [toasts, config] = registrations const toastProps = toasts.definition.inject() assert.deepEqual(toastProps.store.getSnapshot().kinds, { completion: true, approval: true, question: true }) assert.equal(toastProps.store.getSnapshot().permission, 'unsupported') assert.equal(toastProps.platform, undefined) for (const operation of ['dismiss', 'arm', 'hold', 'open']) { assert.equal(typeof toastProps[operation], 'function', `${operation} is not a function`) } assert.equal(typeof toastProps.fallbackT, 'function') const configProps = config.definition.inject() assert.equal(typeof configProps.ask, 'function') assert.equal(typeof configProps.testAny, 'function') assert.equal(typeof configProps.testSystem, 'function') assert.equal(typeof configProps.setKind, 'function') assert.equal(configProps.replayCount(), 0) // No locale service in this fake context, so the plugin's own translator falls // back to its English dictionary. assert.equal(configProps.copy.deliveryFacts(null, configProps.fallbackT), null) assert.equal(configProps.copy.replayValue(0, configProps.fallbackT), 'None') }) test('the committed artifact is exactly what src/client builds', async (t) => { const { buildClient } = await import('../scripts/build-client.mjs') let built try { built = await buildClient({ write: false }) } catch (error) { t.skip(`cannot rebuild without devDependencies: ${error.message}`) return } assert.equal(built, source, 'client.js is stale — run `pnpm run build` and commit the result') })