// ── What the chunk registers, checked without a browser ───────────────────
//
// `build.test.js` says the honest thing about this half: its real failures are
// timing and resolution, and a DOM-less runner cannot see either. MODULE_API.md
// §7.7's browser smoke is what proves the client half works, and nothing here
// replaces it.
//
// What a test CAN do is read back what the chunk asked for. Registration is the
// one thing the chunk does at evaluation time, and it does it through an object
// core hands it — so: stand up a fake `window.__rg` with a recording registry and
// the real React behind it, import the BUILT artifact, and inspect the result. No
// DOM is needed because nothing renders; `` is `jsx(WorldStatus)`,
// an object, and the route table is full of them by design.
//
// It catches a page that silently stops being routed, a nav row whose `to` drifts
// from its route's path, and the whole registration surface disappearing because
// something threw halfway down entry.jsx.
//
// **It runs against `dist/entry.js`, so build before you test.** The skip below
// is deliberate — `npm test` has to be runnable before `npm run build` — which
// means a CI job that tests without building is a job asking nothing at all. Ours
// builds first, on purpose.
import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import * as react from 'react'
import * as jsxRuntime from 'react/jsx-runtime'
import * as router from 'react-router-dom'
const HERE = path.dirname(fileURLToPath(import.meta.url))
const CHUNK = path.resolve(HERE, '..', 'dist', 'entry.js')
const manifest = JSON.parse(fs.readFileSync(path.resolve(HERE, '..', '..', 'module.json'), 'utf8'))
// A component, as far as the registry cares. The kit's real members are core's;
// nothing renders here, so a named stub is enough to be imported and passed on.
const stub = (name) => Object.assign(() => null, { displayName: name })
function fakeRg() {
const routes = { public: [], admin: [], player: [] }
const nav = { public: [], admin: [], player: [] }
const providers = new Map()
const extensions = new Map()
return {
version: manifest.coreApi.replace(/^\D+/, ''),
react,
jsxRuntime,
router,
// `react-dom/client` is imported for the identity check in core.js and never
// called — `createRoot` in a DOM-less process would throw. The shim reads
// this object, so the check compares against whatever is here.
reactDom: { createRoot: () => { throw new Error('not in a browser') } },
ui: Object.fromEntries(
['PublicLayout', 'PageHeader', 'Loading', 'ErrorState', 'EmptyState', 'useAsync', 'useAuth', 'useSite']
.map((n) => [n, stub(n)]),
),
api: { request: async () => ({}), ApiError: Error, BASE: '/api/v1' },
registry: {
registerRoutes(id, byArea) {
for (const [area, list] of Object.entries(byArea || {})) {
for (const r of list || []) routes[area].push({ ...r, path: `${id}/${r.path}`, moduleId: id })
}
},
registerNav(id, { area, items }) {
for (const item of items || []) nav[area].push({ ...item, moduleId: id })
},
registerFeatureProvider(id, namespace, hook) { providers.set(namespace, { id, hook }) },
registerExtension(id, slot, Component) {
if (extensions.has(slot)) throw new Error(`slot "${slot}" already filled`)
extensions.set(slot, { id, Component })
},
routesFor: (area) => routes[area],
navFor: (area) => nav[area],
},
_read: () => ({ routes, nav, providers, extensions }),
}
}
// Loaded once: an ES module is evaluated a single time per process however many
// times it is imported, so every test below reads the same registration pass —
// which is also how it behaves in a browser.
let registered = null
let skip = false
if (!fs.existsSync(CHUNK)) {
skip = true
} else {
const rg = fakeRg()
globalThis.window = { __rg: rg }
await import(`${new URL(`file://${CHUNK.split(path.sep).join('/')}`)}`)
registered = rg._read()
}
const it = (name, fn) => test(name, { skip: skip && 'no dist/entry.js — run npm run build' }, fn)
it('registers at least one route, namespaced under the module id', () => {
const all = Object.values(registered.routes).flat()
assert.ok(all.length > 0, 'the chunk registered no routes at all')
for (const [area, list] of Object.entries(registered.routes)) {
for (const r of list) {
assert.ok(r.path.startsWith(`${manifest.id}/`), `${area} route "${r.path}" is not under the namespace`)
assert.ok(r.element, `${area} route "${r.path}" has no element`)
}
}
})
it('every route path is distinct within its area', () => {
// Two routes on one path is a page that can never be reached, and React
// renders the first one without complaint.
for (const [area, list] of Object.entries(registered.routes)) {
const paths = list.map((r) => r.path)
assert.equal(new Set(paths).size, paths.length, `duplicate path in ${area}`)
}
})
it('every nav row points at a route this module actually registered', () => {
// The agreement that matters, and the one that rots quietly: a row survives a
// route rename and becomes a link to core's catch-all redirect. Nav rows carry
// the FULL rendered path (`/examplegame/status`); routes carry the namespaced
// one (`examplegame/status`). Reconciling the two is the whole test.
const rendered = {
public: (p) => `/${p}`,
admin: (p) => `/admin/${p}`,
player: (p) => `/player/${p}`,
}
for (const [area, rows] of Object.entries(registered.nav)) {
const reachable = new Set(registered.routes[area].map((r) => rendered[area](r.path)))
for (const row of rows) {
assert.ok(reachable.has(row.to), `${area} nav row "${row.label}" links to ${row.to}, which no route serves`)
}
}
})
it('every admin and player nav row carries an icon', () => {
// Both of those navs draw a glyph on every core row, so a row without one reads
// as breakage rather than as a design — and core's player portal used to render
// `` unguarded, which blanked the entire portal with React error #130
// the first time a module registered a row without one. Core guards it now; a
// missing icon there is still a visible defect and this is the cheap place to
// catch it. The PUBLIC header is text buttons and is deliberately excluded.
for (const area of ['admin', 'player']) {
for (const row of registered.nav[area]) {
assert.equal(typeof row.icon, 'function', `${area} nav row "${row.label}" has no icon`)
}
}
})
it('a nav row that gates on a feature has a provider to resolve it', () => {
// Resolution is by the REGISTERING module (§3.3), and every unknown fails OPEN.
// So a row carrying a `feature` from a module that registered no provider is a
// row that always shows — which re-advertises a surface an operator hid.
const gated = Object.values(registered.nav).flat().filter((r) => r.feature)
if (gated.length === 0) return
assert.ok(registered.providers.size > 0, 'rows carry feature gates but no provider was registered')
})
it('every slot module.json declares is one the chunk fills', () => {
// `module.json` declares SERVER slots, and the loader validates those before
// the chunk is ever served. Client slots cannot be declared there — the server
// knows nothing about them — so this is the one place the two halves meet.
for (const slot of manifest.extensions || []) {
assert.ok(registered.extensions.has(slot), `module.json declares "${slot}" and the chunk does not fill it`)
}
})
it('registers under exactly one module id, matching the manifest', () => {
const owners = new Set([
...Object.values(registered.routes).flat().map((r) => r.moduleId),
...Object.values(registered.nav).flat().map((r) => r.moduleId),
...[...registered.extensions.values()].map((e) => e.id),
...[...registered.providers.values()].map((p) => p.id),
])
assert.deepEqual([...owners], [manifest.id])
})