import { test } from 'node:test' import assert from 'node:assert/strict' import { applyNavOverrides } from '../src/lib/navOverrides.js' // The nav-override merge (docs/website/THEMING_AND_NAV.md §7.1) — the one piece // of this feature with real correctness risk, so it is tested in isolation from // React. Two properties matter above all others: // // 1. No override, or a useless one, renders the coded nav untouched. // 2. The override cannot add a route, cannot touch a role/feature gate, and // cannot un-hide anything. It is presentation only. const FLAT = [ { label: 'Home', to: '/', end: true }, { label: 'News', to: '/site/news' }, { label: 'Wiki', to: '/wiki' }, { label: 'Shard', to: '/site/shard', feature: 'status' }, ] const GROUPED = [ { items: [{ to: '/admin', label: 'Dashboard', end: true, roles: ['admin', 'editor', 'moderator'] }] }, { title: 'Content', items: [ { to: '/admin/posts', label: 'Posts', roles: ['admin', 'editor'] }, { to: '/admin/wiki', label: 'Wiki', roles: ['admin', 'editor'] }, ], }, { title: 'System', items: [ { to: '/admin/settings', label: 'Settings', roles: ['admin'] }, { to: '/admin/users', label: 'Users', roles: ['admin'] }, ], }, ] const labels = (nav) => nav.map((i) => i.label) const groupLabels = (nav) => nav.map((g) => [g.title ?? null, g.items.map((i) => i.label)]) // ── The untouched path ──────────────────────────────────────────────────── // Most instances will never set these keys. Absence must be a true no-op, and // cheap: the same array reference back means no needless re-render either. test('no override returns the base nav unchanged', () => { for (const overrides of [null, undefined, '', 0, [], 'not an object']) { assert.equal(applyNavOverrides(FLAT, overrides), FLAT) } }) test('an override with nothing usable in it returns the base nav unchanged', () => { assert.equal(applyNavOverrides(FLAT, {}), FLAT) // Every field here is unusable: unknown route, blank label, non-numeric order, // hidden as a string rather than the boolean true. assert.equal( applyNavOverrides(FLAT, { '/does/not/exist': { label: 'Ghost', hidden: true }, '/wiki': { label: ' ', order: 'first', hidden: 'yes' }, }), FLAT, ) }) // ── The security boundary ───────────────────────────────────────────────── // The single most important negative case: the override layer must never be a // way to introduce a route into a nav. test('an unknown `to` is ignored, never added', () => { const out = applyNavOverrides(FLAT, { '/admin/secret': { label: 'Secret', order: 0 } }) assert.equal(out.length, FLAT.length) assert.ok(!out.some((i) => i.to === '/admin/secret')) }) test('roles, feature, icon, end and to survive the merge verbatim', () => { const out = applyNavOverrides(FLAT, { '/site/shard': { label: 'Server Status', roles: ['player'], feature: null, to: '/evil' }, }) const shard = out.find((i) => i.to === '/site/shard') assert.equal(shard.label, 'Server Status') // the one thing an override may set assert.equal(shard.feature, 'status') // gate untouched assert.equal(shard.roles, undefined) // and not invented assert.ok(!out.some((i) => i.to === '/evil')) }) test('hidden:false cannot un-hide anything — hiding is subtractive only', () => { // The item is still present after the merge; whether it renders is decided by // the caller's own role/feature filter, which this layer cannot reach. const out = applyNavOverrides(GROUPED, { '/admin/settings': { hidden: false } }) assert.equal(out, GROUPED, 'a no-op override leaves the base nav alone') }) // ── Flat navs: label, order, hidden ─────────────────────────────────────── test('label overrides only the labelled item', () => { const out = applyNavOverrides(FLAT, { '/site/news': { label: 'Announcements' } }) assert.deepEqual(labels(out), ['Home', 'Announcements', 'Wiki', 'Shard']) }) test('hidden drops the item', () => { const out = applyNavOverrides(FLAT, { '/wiki': { hidden: true } }) assert.deepEqual(labels(out), ['Home', 'News', 'Shard']) }) // An item the admin never reordered keeps its position in the coded array, so // setting one order does not scramble the rest. test('order moves one item and leaves the others in code order', () => { const out = applyNavOverrides(FLAT, { '/wiki': { order: -1 } }) assert.deepEqual(labels(out), ['Wiki', 'Home', 'News', 'Shard']) }) test('two items given the same order keep their code order (stable sort)', () => { const out = applyNavOverrides(FLAT, { '/site/news': { order: 0 }, '/wiki': { order: 0 } }) // News before Wiki — the tie resolves to the coded order, not to insertion // order in the settings JSON. Both precede Home, whose 0 is only its index. assert.deepEqual(labels(out), ['News', 'Wiki', 'Home', 'Shard']) }) // An explicit order and an untouched item's index share one number line, so // they can collide. "Put this first" has to actually mean first. test('an explicit order beats an untouched item that merely sits at that index', () => { const out = applyNavOverrides(FLAT, { '/wiki': { order: 0 } }) assert.deepEqual(labels(out), ['Wiki', 'Home', 'News', 'Shard']) }) test('the merge does not mutate the base nav', () => { const before = JSON.stringify(FLAT) applyNavOverrides(FLAT, { '/wiki': { label: 'Library', order: 0, hidden: false } }) assert.equal(JSON.stringify(FLAT), before) }) test('no internal sort key leaks into the returned items', () => { const out = applyNavOverrides(FLAT, { '/wiki': { order: 1 } }) for (const item of out) assert.ok(!('__order' in item), 'sort key must not be rendered') }) // ── Grouped (admin) navs ────────────────────────────────────────────────── test('label and order apply within a group', () => { const out = applyNavOverrides(GROUPED, { '/admin/wiki': { label: 'Knowledge Base', order: 0 }, }) assert.deepEqual(groupLabels(out), [ [null, ['Dashboard']], ['Content', ['Knowledge Base', 'Posts']], ['System', ['Settings', 'Users']], ]) }) test('group moves an item into another existing section', () => { const out = applyNavOverrides(GROUPED, { '/admin/users': { group: 'Content' } }) assert.deepEqual(groupLabels(out), [ [null, ['Dashboard']], ['Content', ['Posts', 'Wiki', 'Users']], ['System', ['Settings']], ]) }) // A group that does not exist must not conjure a header. Groups are chosen from // a dropdown of existing titles in the editor; this is the stale-row guard. test('a group that is not an existing title is ignored', () => { const out = applyNavOverrides(GROUPED, { '/admin/users': { group: 'Danger Zone' } }) assert.deepEqual(groupLabels(out), [ [null, ['Dashboard']], ['Content', ['Posts', 'Wiki']], ['System', ['Settings', 'Users']], ]) }) test('a moved item can be ordered in its new group', () => { const out = applyNavOverrides(GROUPED, { '/admin/users': { group: 'Content', order: -1 } }) assert.deepEqual(groupLabels(out)[1], ['Content', ['Users', 'Posts', 'Wiki']]) }) test('hiding every item in a group leaves no orphaned header', () => { const out = applyNavOverrides(GROUPED, { '/admin/settings': { hidden: true }, '/admin/users': { hidden: true }, }) assert.deepEqual(groupLabels(out), [ [null, ['Dashboard']], ['Content', ['Posts', 'Wiki']], ]) }) test('group ordering itself is not overridable — sections stay in code order', () => { const out = applyNavOverrides(GROUPED, { '/admin/settings': { order: -99 } }) assert.deepEqual( out.map((g) => g.title ?? null), [null, 'Content', 'System'], ) }) // ── Degenerate input ────────────────────────────────────────────────────── test('a non-array base nav yields an empty nav rather than throwing', () => { assert.deepEqual(applyNavOverrides(null, { '/': { hidden: true } }), []) assert.deepEqual(applyNavOverrides(undefined, null), []) }) test('an empty base nav stays empty', () => { assert.deepEqual(applyNavOverrides([], { '/': { label: 'Home' } }), []) })