import { test, beforeEach } 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 { registry, registerRoutes, registerNav, registerFeatureProvider, routesFor, navFor, featureProviderFor, featureProviders, registeredIds, _reset, } from '../src/modules/registry.js' import { MODULE_API_VERSION } from '../src/modules/version.js' // The client-side module registry (docs/website/MODULE_API.md ยง3.3). Tested in // isolation from React, like the nav-override merge next door, because the // property worth proving has nothing to do with rendering: a module gets exactly // the URL namespace core gave it, however it spells the paths it registers. // // window.__rg itself (modules/shared.js) is not tested here โ€” it imports .jsx and // there is no DOM in this runner. What it publishes is React, the router and // core components: a wiring test would assert that an import statement imported // something. Phase 1's spike proved the half that can actually fail, which is a // real chunk resolving its externals against the global in a browser under an // enforced CSP. beforeEach(() => _reset()) test('a module route is namespaced under the module id', () => { registerRoutes('uo', { public: [{ path: 'atlas', element: 'ATLAS' }] }) assert.deepEqual( routesFor('public').map((r) => r.path), ['uo/atlas'], ) }) test('a module cannot spell its way out of its namespace', () => { // Whatever the module writes, the segment it lands under is core's to choose: // leading slashes, several of them, a trailing one, or nothing at all. registerRoutes('uo', { public: [ { path: '/atlas' }, { path: '//atlas/creatures' }, { path: 'atlas/' }, { path: '' }, ], }) assert.deepEqual( routesFor('public').map((r) => r.path), ['uo/atlas', 'uo/atlas/creatures', 'uo/atlas', 'uo'], ) }) test('a path is namespaced, not sanitised โ€” traversal stays a literal segment', () => { // `..` is not stripped, and does not need to be: React Router matches path // patterns literally, so `/uo/../admin` is a route nothing navigates to rather // than a route that resolves somewhere else. Asserted so that a future // "cleanup" that starts resolving these knows it changed a behaviour. registerRoutes('uo', { public: [{ path: '../admin' }] }) assert.deepEqual(routesFor('public')[0].path, 'uo/../admin') }) test('routes keep their gate and carry the owning module id', () => { registerRoutes('uo', { admin: [{ path: 'shard-ops', element: 'OPS', gate: { roles: ['admin', 'moderator'] } }], }) const [route] = routesFor('admin') assert.deepEqual(route.gate, { roles: ['admin', 'moderator'] }) assert.equal(route.moduleId, 'uo') assert.equal(route.element, 'OPS') }) test('the three areas are kept apart', () => { registerRoutes('uo', { public: [{ path: 'atlas' }], admin: [{ path: 'link' }], player: [{ path: 'chars' }], }) assert.equal(routesFor('public').length, 1) assert.equal(routesFor('admin').length, 1) assert.equal(routesFor('player').length, 1) // An area nobody registered is an empty list, never undefined: App.jsx maps // over all three unconditionally. _reset() for (const area of ['public', 'admin', 'player']) assert.deepEqual(routesFor(area), []) }) test('an unknown area throws rather than being dropped', () => { // Loudly, because the alternative is a module whose pages simply never appear // and no indication anywhere of why. assert.throws(() => registerRoutes('uo', { publik: [{ path: 'atlas' }] }), /unknown area/) assert.throws(() => registerNav('uo', { area: 'sidebar', items: [] }), /unknown area/) assert.equal(registeredIds().length, 0) }) test('nav items sort by order, and equal orders keep load order', () => { registerNav('aa', { area: 'admin', items: [{ label: 'Second', to: '/a', order: 30 }] }) registerNav('zz', { area: 'admin', items: [{ label: 'Third', to: '/z', order: 30 }] }) registerNav('mm', { area: 'admin', items: [{ label: 'First', to: '/m', order: 10 }] }) assert.deepEqual( navFor('admin').map((i) => i.label), ['First', 'Second', 'Third'], ) }) test('a nav item with no order sorts after the ones that asked for a place', () => { registerNav('uo', { area: 'public', items: [{ label: 'Unordered', to: '/u' }, { label: 'Early', to: '/e', order: 5 }], }) assert.deepEqual( navFor('public').map((i) => i.label), ['Early', 'Unordered'], ) }) test('a feature provider is stored under its namespace, with its owner', () => { const hook = () => ({ atlas: true }) registerFeatureProvider('uo', 'shard', hook) assert.deepEqual(featureProviderFor('shard'), { id: 'uo', hook }) assert.equal(featureProviderFor('nothing'), undefined) }) test('providers can be enumerated in registration order, with their owner', () => { // Core's feature context has to CALL each of these, as a hook, in a fixed // order โ€” so it needs the list, and it needs the owner id to resolve a nav // row whose `moduleId` says who it belongs to (modules/features.jsx). const uo = () => null const rust = () => null registerFeatureProvider('uo', 'shard', uo) registerFeatureProvider('rust', 'server', rust) assert.deepEqual(featureProviders(), [ { id: 'uo', namespace: 'shard', hook: uo }, { id: 'rust', namespace: 'server', hook: rust }, ]) }) test('enumerating providers is NOT part of the module-facing surface', () => { // A module asks for a namespace it knows the name of; enumerating what // everyone else registered is core's business, so `featureProviders` is a // module export and not a member of window.__rg.registry. assert.equal(registry.featureProviders, undefined) assert.equal(typeof featureProviders, 'function') }) test('every registration marks the module registered', () => { registerRoutes('a', { public: [{ path: 'x' }] }) registerNav('b', { area: 'public', items: [] }) registerFeatureProvider('c', 'ns', () => {}) assert.deepEqual(registeredIds().sort(), ['a', 'b', 'c']) }) test('the registry object handed to modules exposes the whole surface', () => { // window.__rg.registry is the ONLY way a module reaches any of this, so a // member missing from the object is a member that does not exist. assert.deepEqual(Object.keys(registry).sort(), [ 'featureProviderFor', 'navFor', 'registerFeatureProvider', 'registerNav', 'registerRoutes', 'registeredIds', 'routesFor', ]) }) test('the client and server halves declare the same MODULE_API_VERSION', () => { // The value is duplicated because it has to be on window.__rg before the first // module chunk evaluates, which is earlier than a fetch could answer. This is // the test that pays for the copy: a bump that edits one file fails here // instead of shipping a core whose two halves disagree about the contract they // implement. const here = path.dirname(fileURLToPath(import.meta.url)) const server = fs.readFileSync( path.join(here, '..', '..', 'server', 'src', 'modules', 'version.js'), 'utf8', ) const match = server.match(/MODULE_API_VERSION\s*=\s*'([^']+)'/) assert.ok(match, 'server/src/modules/version.js no longer declares MODULE_API_VERSION as a literal') assert.equal(MODULE_API_VERSION, match[1]) })