86 lines
4.0 KiB
TypeScript
86 lines
4.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { composeNav, type NavNode } from "./nav.ts";
|
|
|
|
// Two plugin fragments; ids let the override target nodes, `permission` gates per role.
|
|
const fragments: NavNode[][] = [
|
|
[{
|
|
icon: "i-cal", id: "sched", label: "Scheduling",
|
|
children: [
|
|
{ href: "/scheduling/shifts", id: "shifts", label: "Shifts", permission: "scheduling:read" },
|
|
{ href: "/scheduling/manage", id: "manage", label: "Manage", permission: "scheduling:admin" },
|
|
],
|
|
}],
|
|
[{ href: "/reports", id: "reports", label: "Reports", permission: "reports:read" }],
|
|
];
|
|
|
|
test("composeNav merges fragments, filters by role, and emits clean render nodes", () => {
|
|
const tree = composeNav(fragments, {}, ["scheduling:read"]);
|
|
|
|
// Reports gone (no reports:read), Manage gone (no scheduling:admin), header kept with Shifts.
|
|
// Output carries no `id`/`permission` and omits absent fields — ready for nav-tree.ejs.
|
|
assert.deepEqual(tree, [
|
|
{ icon: "i-cal", label: "Scheduling", children: [{ href: "/scheduling/shifts", label: "Shifts" }] },
|
|
]);
|
|
});
|
|
|
|
test("composeNav drops gated subtrees, empty headers, and (with no roles) all gated nodes", () => {
|
|
// A header the user can't reach takes its whole subtree, even visible children.
|
|
const gatedHeader: NavNode[][] = [[
|
|
{ id: "admin", label: "Admin", permission: "admin", children: [{ href: "/u", id: "u", label: "Users" }] },
|
|
{ id: "free", label: "Free", children: [{ href: "/d", id: "d", label: "Docs" }] },
|
|
]];
|
|
assert.deepEqual(composeNav(gatedHeader, {}, []), [
|
|
{ label: "Free", children: [{ href: "/d", label: "Docs" }] },
|
|
]);
|
|
|
|
// A pure header whose children are all filtered is dropped; a header with an href survives as a leaf.
|
|
const emptyHeader: NavNode[][] = [[
|
|
{ id: "sec", label: "Section", children: [{ href: "/x", id: "x", label: "X", permission: "x" }] },
|
|
{ href: "/hub", id: "hub", label: "Hub", children: [{ href: "/y", id: "y", label: "Y", permission: "y" }] },
|
|
]];
|
|
assert.deepEqual(composeNav(emptyHeader, {}, []), [{ href: "/hub", label: "Hub" }]);
|
|
|
|
// No fragments / no roles → empty tree, never throws.
|
|
assert.deepEqual(composeNav(), []);
|
|
});
|
|
|
|
test("composeNav keeps a node marked public for everyone — the blessed public alias", () => {
|
|
// A header with one public child + one gated child: with no roles, the public child keeps the
|
|
// header alive (the gated child is filtered out) — so a plugin can show a public menu option to all.
|
|
const frag: NavNode[][] = [[{
|
|
icon: "i-cal", id: "sched", label: "Scheduling",
|
|
children: [
|
|
{ href: "/scheduling", id: "overview", label: "Overview", public: true },
|
|
{ href: "/scheduling/shifts", id: "shifts", label: "Shifts", permission: "scheduling:read" },
|
|
],
|
|
}]];
|
|
// `public` is filter-only (like id/permission) — never rendered into the output node.
|
|
assert.deepEqual(composeNav(frag, {}, []), [
|
|
{ icon: "i-cal", label: "Scheduling", children: [{ href: "/scheduling", label: "Overview" }] },
|
|
]);
|
|
});
|
|
|
|
test("composeNav applies the override: rename, group, order, hide (then filters)", () => {
|
|
const base: NavNode[][] = [[
|
|
{ href: "/a", id: "a", label: "Alpha" },
|
|
{ href: "/b", id: "b", label: "Beta" },
|
|
{ href: "/c", id: "c", label: "Gamma" },
|
|
{ href: "/secret", id: "secret", label: "Secret", permission: "root" },
|
|
]];
|
|
|
|
const tree = composeNav(base, {
|
|
rename: { a: "First" }, // relabel by id
|
|
groups: [{ icon: "i-box", id: "grp", label: "Group", open: true, children: ["b", "c"] }], // wrap b+c
|
|
order: ["grp", "a"], // grp before the lone a
|
|
hide: ["c"], // remove c from inside the group
|
|
}, ["root"]);
|
|
|
|
// grp emitted (b only, c hidden), reordered before a; Secret kept now that role "root" is present.
|
|
assert.deepEqual(tree, [
|
|
{ icon: "i-box", label: "Group", open: true, children: [{ href: "/b", label: "Beta" }] },
|
|
{ href: "/a", label: "First" },
|
|
{ href: "/secret", label: "Secret" },
|
|
]);
|
|
});
|