// ── 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. That is still // true, and MODULE_API.md §7.7's browser smoke is still what proves the module // works. But it left a gap worth closing, and slice 3 is when it started to // matter: nothing checked *what* the chunk registers. // // It can be checked, because registration is the one thing this 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 read back what it asked for. No DOM is needed // because nothing renders — `` is `jsx(Shard)`, an object, and the // route table is full of them by design. // // What this catches that review does not: a page that silently stops being // routed, a nav row whose `to` drifts from its route's path, a slot fill that // was renamed on one side, and the whole registration surface disappearing // because an exception was thrown halfway down entry.jsx. // // What it deliberately does NOT do is re-assert the paths as a literal list. // The interesting property is that the nav and the routes AGREE, and a test that // restates both is a second copy of the thing it is checking. 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') // A component, as far as the registry cares. The kit's real members are core's; // nothing here renders, 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: '1.3.0', 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 routes in all three areas, namespaced under the module id', () => { const { routes } = registered assert.equal(routes.public.length, 12) assert.equal(routes.admin.length, 7) assert.equal(routes.player.length, 2) for (const area of ['public', 'admin', 'player']) { for (const r of routes[area]) { assert.match(r.path, /^uo\//, `${area} route "${r.path}" is not under the module 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 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 (`/uo/shard`), routes carry the namespaced one // (`uo/shard`), and reconciling them 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 render a glyph on every core row, so a row without one // reads as breakage rather than as a design. The PUBLIC header is text // buttons and is deliberately excluded. // // The player half of this assertion is not symmetry for its own sake. Core's // PlayerPortalLayout rendered `` UNGUARDED — fine for as long as // every row in it was core's own and had one, and React error #130 with a // blank portal the moment a module registered one without. Core is guarded // now, but a missing icon there is still a visible defect and this is the // cheap place to catch it. 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 is gated by a namespace this module provides', () => { // Resolution is by the REGISTERING module (§3.3), so a `feature` on a row from // a module that registered no provider resolves against nothing — and // everything fails open, which would re-advertise surfaces an operator hid. const gated = Object.values(registered.nav).flat().filter((r) => r.feature) assert.ok(gated.length > 0) assert.ok(registered.providers.has('uo'), 'rows carry feature gates but no provider was registered') }) it('fills the three extension slots, each with a component', () => { const { extensions } = registered assert.deepEqual( [...extensions.keys()].sort(), ['admin.users.detail', 'player.invite.accepted', 'site.footer.status'], ) for (const [slot, { id, Component }] of extensions) { assert.equal(id, 'uo', `${slot} was filled under the wrong owner id`) assert.equal(typeof Component, 'function', `${slot} was not filled with a component`) } }) it('the manifest\'s declared server slot is one this module fills', () => { // module.json declares SERVER slots and the loader validates them before the // chunk is ever served. Client slots cannot be declared there — the server has // no knowledge of them — so this is the one place the two halves are compared. const manifest = JSON.parse(fs.readFileSync(path.resolve(HERE, '..', '..', 'module.json'), 'utf8')) 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 manifest = JSON.parse(fs.readFileSync(path.resolve(HERE, '..', '..', 'module.json'), 'utf8')) 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]) })