update 2026-09-20 01:22:33

This commit is contained in:
action committed 2026-09-20 01:22:33 +08:00
1 parent 42e0cfbec4
commit deb27aee9d
864 files changed
+180566 -42

No files matched your search

@@ -0,0 +1,176 @@
import assert from "node:assert/strict";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import test from "node:test";
const projectRoot = resolve(import.meta.dirname, "../..");
const output = resolve(projectRoot, "htdocs/luci-static");
const asset = (path) => resolve(output, path);
const bytes = (path) => statSync(asset(path)).size;
test("production assets stay within raw-transfer budgets", () => {
const main = bytes("aurora/main.css");
const login = bytes("aurora/login.css");
const menu = bytes("resources/menu-aurora.js");
const router = bytes("resources/router-aurora.js");
const font = bytes("aurora/fonts/lato-v24-latin-regular.woff2");
const logo = bytes("aurora/images/logo.svg");
// 187K: pseudo-elements are nested rules, one content declaration each.
assert.ok(main <= 187_000, "main.css exceeds 187 KB");
assert.ok(login <= 12_000, "login.css exceeds 12 KB");
// 22K: palette recents (record on pick, pure-LRU browse order, storage
// validation) added ~0.8 KB and the ">" logout command ~0.2 KB. The total
// budget below moved by the same amount.
// 23K: palette tabs (third-level nodes, redirect-parent folding, legacy
// recents mapping, parent-aware scoring, per-segment path words) added
// ~1.3 KB; the total budget below still holds.
assert.ok(menu <= 23_000, "menu-aurora.js exceeds 23 KB");
// 15K: the expiry gate, readonly folding, menu.d node css, wildcard
// actions, progress bar, visibility gate and contract check added ~3 KB
// over the first cut; the total-transfer budget below moved by the same
// amount. Mirrored in the perf skill's aurora-budgets.md.
// 16K: the router now ships from @eamonxg/luci-theme-devkit — theme hooks
// as document events, the title format read off the document, and the
// inner-scroller restoration shared with shadcn (+0.8 KB raw, +0.3 KB gz).
assert.ok(router <= 16_000, "router-aurora.js exceeds 16 KB");
assert.ok(logo <= 16_000, "logo.svg exceeds 16 KB");
assert.ok(
main + menu + router + font + logo <= 264_000,
"admin assets exceed 264 KB",
);
assert.ok(login + font + logo <= 55_000, "login assets exceed 55 KB");
});
test("compressed LuCI JS preserves its loader directives", () => {
const js = readFileSync(asset("resources/menu-aurora.js"), "utf8");
assert.match(js, /["']require baseclass["'];["']require ui["'];/);
const module = new Function("baseclass", "ui", js)(
{ extend: (value) => value },
{},
);
assert.equal(typeof module.__init__, "function");
});
test("compiled CSS does not duplicate SVG payloads for every mask property", () => {
const css = readFileSync(asset("aurora/main.css"), "utf8");
const dataUrls = css.match(/data:image\/svg\+xml,[^"]+/g) ?? [];
const encodedBytes = dataUrls.reduce((sum, value) => sum + value.length, 0);
assert.equal(
dataUrls.length,
new Set(dataUrls).size,
"duplicate SVG data URL",
);
assert.ok(encodedBytes <= 17_000, `SVG data URLs occupy ${encodedBytes} B`);
});
test("pruned login.css keeps every consumed variable resolvable", () => {
const css = readFileSync(asset("aurora/login.css"), "utf8");
const declared = new Set(
[...css.matchAll(/[{;](--[\w-]+):/g)].map((m) => m[1]),
);
const registered = new Set(
[...css.matchAll(/@property\s+(--[\w-]+)/g)].map((m) => m[1]),
);
// header.ut injects these from UCI at render time — consumed here, never
// declared here.
const injected = new Set(["--login-bg", "--login-bg-lqip"]);
const unresolvable = [];
for (const [, name, delim] of css.matchAll(/var\(\s*(--[\w-]+)\s*([,)])/g)) {
const hasFallback = delim === ",";
if (
!declared.has(name) &&
!registered.has(name) &&
!injected.has(name) &&
!hasFallback
) {
unresolvable.push(name);
}
}
assert.deepEqual(unresolvable, []);
// The prune must actually strip admin-only tokens from the shared sheet.
for (const adminOnly of ["--mega-menu-bg", "--icon-", "--sidebar"]) {
assert.ok(!css.includes(adminOnly), `${adminOnly} should be pruned`);
}
// And the login page's own consumed tokens must survive, light and dark.
for (const kept of ["--surface:", "--brand:", "--control-bg:"]) {
const count = css.split(kept).length - 1;
assert.ok(count >= 2, `${kept} should be declared for both modes`);
}
});
test("package roots contain no macOS metadata", () => {
const offenders = [];
const visit = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.name === ".DS_Store") offenders.push(path);
else if (entry.isDirectory()) visit(path);
}
};
visit(resolve(projectRoot, "htdocs"));
visit(resolve(projectRoot, "ucode"));
assert.deepEqual(offenders, []);
});
test("the web manifest's icons exist in the package", () => {
const manifest = JSON.parse(readFileSync(asset("aurora/images/app.webmanifest"), "utf8"));
assert.ok(manifest.icons.length > 0);
for (const { src } of manifest.icons) {
assert.match(src, /^\/luci-static\//);
assert.ok(statSync(asset(src.replace(/^\/luci-static\//, ""))).isFile(), src);
}
});
test("login.css keeps the Safari 16.4 backdrop-filter prefix", () => {
const css = readFileSync(asset("aurora/login.css"), "utf8");
const count = (needle) => css.split(needle).length - 1;
const prefixed = count("-webkit-backdrop-filter:");
assert.ok(prefixed > 0);
assert.equal(count("backdrop-filter:") - prefixed, prefixed);
});
test("no rule repeats a declaration, vendor-prefixed ones aside", () => {
for (const sheet of ["aurora/main.css", "aurora/login.css"]) {
const css = readFileSync(asset(sheet), "utf8");
const repeated = [];
const blocks = [[]];
let quote = "";
let parens = 0;
let start = 0;
const close = (end) => {
const declaration = css.slice(start, end).trim();
const seen = blocks.at(-1);
if (declaration && !declaration.startsWith("-webkit-")) {
if (seen.includes(declaration)) repeated.push(declaration);
seen.push(declaration);
}
start = end + 1;
};
for (let i = 0; i < css.length; i++) {
const char = css[i];
if (quote) {
if (char === "\\") i++;
else if (char === quote) quote = "";
} else if (char === '"' || char === "'") quote = char;
else if (char === "(") parens++;
else if (char === ")") parens--;
else if (parens) continue;
else if (char === ";") close(i);
else if (char === "{") {
blocks.push([]);
start = i + 1;
} else if (char === "}") {
close(i);
blocks.pop();
}
}
assert.deepEqual(repeated, [], sheet);
}
});
@@ -0,0 +1,792 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
const source = await readFile(
new URL("../src/resource/menu-aurora.js", import.meta.url),
"utf8",
);
const E = (tagName, attributes, children) => ({
tagName,
attributes,
children,
});
const loadMenuModule = (
localStorage = {},
{
document = {},
navigator = { platform: "" },
ui = { menu: { getChildren: () => [] } },
} = {},
) => {
const baseclass = {
extend(module) {
return module;
},
};
const L = {
env: { dispatchpath: [], requestpath: [] },
url: (...segments) => `/${segments.join("/")}`,
};
return new Function(
"baseclass",
"ui",
"E",
"L",
"_",
"document",
"window",
"localStorage",
"navigator",
source,
)(baseclass, ui, E, L, (value) => value, document, {}, localStorage, navigator);
};
const menu = loadMenuModule();
const score = (query, text) => menu.fuzzyMatch(query, text)?.score ?? null;
const sliced = (text, ranges) =>
ranges.map(([from, to]) => text.slice(from, to));
test("matches a scattered subsequence and reports its ranges", () => {
const match = menu.fuzzyMatch("dns", "DHCP and DNS");
assert.ok(match);
assert.deepEqual(sliced("DHCP and DNS", match.ranges), ["DNS"]);
});
test("returns null when the query is not a subsequence", () => {
assert.equal(menu.fuzzyMatch("zzz", "Firewall"), null);
});
test("retries from every first-character occurrence instead of staying greedy", () => {
// A single greedy pass anchors "dns" to the leading D of "DHCP and DNS" and
// scatters, scoring the real hit below shorter near-misses.
assert.ok(score("dns", "DHCP and DNS") > score("dns", "Diagnostics"));
});
test("scores an adjacency run above the same characters scattered", () => {
assert.ok(score("fir", "Firewall") > score("fir", "Failover in Routing"));
});
test("rewards word starts", () => {
assert.ok(score("s", "Status") > score("s", "Wireless"));
});
test("penalises longer targets so the tighter title wins", () => {
assert.ok(score("log", "Logout") > score("log", "Logging and reporting"));
});
test("matches CJK titles", () => {
const match = menu.fuzzyMatch("防火墙", "防火墙设置");
assert.ok(match);
assert.deepEqual(sliced("防火墙设置", match.ranges), ["防火墙"]);
});
test("keeps astral characters whole in the reported ranges", () => {
// `at` is a UTF-16 offset while for...of yields code points: advancing by 1
// would end the range inside the surrogate pair and slice it into mojibake.
const title = "🚀 Passwall";
const match = menu.fuzzyMatch("🚀", title);
assert.ok(match);
assert.deepEqual(match.ranges, [[0, 2]]);
assert.deepEqual(sliced(title, match.ranges), ["🚀"]);
});
test("merges adjacent astral matches into one range", () => {
const title = "𠀀𠀁 Extension";
const match = menu.fuzzyMatch("𠀀𠀁", title);
assert.ok(match);
assert.deepEqual(match.ranges, [[0, 4]]);
assert.deepEqual(sliced(title, match.ranges), ["𠀀𠀁"]);
});
test("an empty query matches every entry unranked and unhighlighted", () => {
assert.deepEqual(menu.matchPaletteEntry("", { title: "Status" }), {
score: 0,
ranges: null,
});
});
test("title hits outrank name hits and are the only highlighted ones", () => {
const title = menu.matchPaletteEntry("dns", {
title: "DNS",
name: "dns",
group: "Network",
});
const viaName = menu.matchPaletteEntry("dhcp", {
title: "DNS",
name: "dhcp",
group: "Network",
});
assert.ok(title.ranges);
assert.equal(viaName.ranges, null);
assert.ok(title.score > viaName.score);
});
test("falls back to the group when neither title nor name matches", () => {
const match = menu.matchPaletteEntry("netw", {
title: "DNS",
name: "dns",
group: "Network",
});
assert.ok(match);
assert.equal(match.ranges, null);
});
test("returns falsy when nothing on the entry matches", () => {
assert.ok(
!menu.matchPaletteEntry("zzz", {
title: "DNS",
name: "dns",
group: "Network",
}),
);
});
test("wraps matched ranges in <mark> and keeps the surrounding text", () => {
const parts = menu.highlightPaletteMatch("DHCP and DNS", [[9, 12]]);
assert.deepEqual(
parts.map((part) => (typeof part === "string" ? part : part.children[0])),
["DHCP and ", "DNS", ""],
);
assert.equal(parts[1].tagName, "mark");
});
test("highlights astral characters without splitting the surrogate pair", () => {
const parts = menu.highlightPaletteMatch("🚀 Passwall", [[0, 2]]);
assert.equal(parts[1].children[0], "🚀");
assert.ok(!parts.some((part) => String(part).includes("�")));
});
test("skips highlighting when case folding would skew the offsets", () => {
// "İ".toLowerCase() is two code units, so ranges measured on the lowercased
// copy no longer index the original.
assert.deepEqual(menu.highlightPaletteMatch("İnterface", [[0, 1]]), [
"İnterface",
]);
});
test("renders the plain title when there are no ranges", () => {
assert.deepEqual(menu.highlightPaletteMatch("Status", null), ["Status"]);
});
// ---- Recents: recording, storage hygiene, and browse-order floating ----
const RECENTS_KEY = "aurora.paletteRecents";
const fakeStorage = (initial = {}) => {
const map = new Map(Object.entries(initial));
return {
getItem: (key) => (map.has(key) ? map.get(key) : null),
setItem: (key, value) => map.set(key, String(value)),
map,
};
};
const navPage = (name, title, group = "Group") => ({
title,
name,
group,
href: `/${name}`,
});
const paletteWith = (storage, index) => {
const module = loadMenuModule(storage);
module.paletteIndex = index;
return module;
};
const browseIndex = () => [
navPage("status/overview", "概览", "状态"),
navPage("status/syslog", "系统日志", "状态"),
navPage("network/iface", "接口", "网络"),
navPage("network/firewall", "防火墙", "网络"),
{ title: "浅色模式", name: "theme light", group: "主题", mode: "light" },
];
const stored = (recents) => ({ [RECENTS_KEY]: JSON.stringify(recents) });
const names = (matches) => matches.map((match) => match.page.name);
test("a recorded selection lands at the head of the stored recents", () => {
const storage = fakeStorage();
const palette = paletteWith(storage, browseIndex());
palette.recordPaletteRecent("status/syslog");
palette.recordPaletteRecent("network/iface");
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
"network/iface",
"status/syslog",
]);
});
test("re-recording an entry moves it up instead of duplicating it", () => {
const storage = fakeStorage(stored(["network/iface", "status/syslog"]));
const palette = paletteWith(storage, browseIndex());
palette.recordPaletteRecent("status/syslog");
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
"status/syslog",
"network/iface",
]);
});
test("the store has no artificial cap — dedupe is the only bound", () => {
const storage = fakeStorage(stored(["h", "g", "f", "e", "d", "c", "b", "a"]));
const palette = paletteWith(storage, browseIndex());
palette.recordPaletteRecent("i");
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
"i",
"h",
"g",
"f",
"e",
"d",
"c",
"b",
"a",
]);
});
test("corrupt or foreign stored values read as no history", () => {
for (const value of ["not json", '"just a string"', "[1,2,3]", "{}"]) {
const palette = paletteWith(
fakeStorage({ [RECENTS_KEY]: value }),
browseIndex(),
);
assert.deepEqual(palette.readPaletteRecents(), [], value);
}
});
test("a throwing storage neither breaks reads nor records", () => {
const hostile = {
getItem: () => {
throw new Error("privacy mode");
},
setItem: () => {
throw new Error("quota");
},
};
const palette = paletteWith(hostile, browseIndex());
assert.deepEqual(palette.readPaletteRecents(), []);
assert.doesNotThrow(() => palette.recordPaletteRecent("status/overview"));
});
test("browsing floats stored recents in recency order, menu order behind", () => {
const palette = paletteWith(
fakeStorage(stored(["network/iface", "status/syslog"])),
browseIndex(),
);
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"network/iface",
"status/syslog",
"status/overview",
"network/firewall",
"theme light",
]);
});
test("every visited page floats in recency order; vanished names drop", () => {
const wide = [
navPage("p1", "One"),
navPage("p2", "Two"),
navPage("p3", "Three"),
navPage("p4", "Four"),
navPage("p5", "Five"),
navPage("p6", "Six"),
navPage("p7", "Seven"),
];
const palette = paletteWith(
fakeStorage(stored(["removed/page", "p7", "p6", "p5", "p4", "p3", "p2"])),
wide,
);
// Pure LRU: all six visited pages float, only p1 keeps its menu slot.
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"p7",
"p6",
"p5",
"p4",
"p3",
"p2",
"p1",
]);
});
test("a stored name never floats a command row", () => {
const palette = paletteWith(
fakeStorage(stored(["theme light"])),
browseIndex(),
);
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"status/overview",
"status/syslog",
"network/iface",
"network/firewall",
"theme light",
]);
});
test("typed queries rank by fuzzy score alone", () => {
const palette = paletteWith(
fakeStorage(stored(["diag"])),
[navPage("dns", "DNS Settings"), navPage("diag", "Diagnostics")],
);
// "Diagnostics" is the recent entry, but the tight title hit must win.
assert.deepEqual(names(palette.collectPaletteMatches("dns")), [
"dns",
"diag",
]);
});
test("'>' browsing lists command rows only, unaffected by recents", () => {
const palette = paletteWith(
fakeStorage(stored(["network/iface"])),
browseIndex(),
);
assert.deepEqual(names(palette.collectPaletteMatches(">")), ["theme light"]);
});
// ---- Logout as a ">"-only command ----
const logoutIndex = () => [
navPage("p1", "Alpha"),
navPage("p2", "Beta"),
navPage("p3", "Gamma"),
navPage("p4", "Delta"),
navPage("p5", "Epsilon"),
navPage("p6", "Zeta"),
{ title: "浅色模式", name: "theme light", group: "主题", mode: "light" },
{ title: "Logout", name: "logout", group: null, href: "/logout", isLogout: true },
];
test("'>' browsing includes the logout command", () => {
const palette = paletteWith(fakeStorage(), logoutIndex());
assert.deepEqual(names(palette.collectPaletteMatches(">")), [
"theme light",
"logout",
]);
});
test("plain browsing hides the logout command", () => {
const palette = paletteWith(fakeStorage(), logoutIndex());
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"p1",
"p2",
"p3",
"p4",
"p5",
"p6",
"theme light",
]);
});
test("typed queries still reach logout", () => {
const palette = paletteWith(fakeStorage(), logoutIndex());
assert.deepEqual(names(palette.collectPaletteMatches("logout")), ["logout"]);
});
test("a stored logout name never floats", () => {
const palette = paletteWith(
fakeStorage(stored(["logout", "p6", "p5", "p4", "p3", "p2"])),
logoutIndex(),
);
assert.deepEqual(names(palette.collectPaletteMatches("")), [
"p6",
"p5",
"p4",
"p3",
"p2",
"p1",
"theme light",
]);
});
test("initPalette appends logout as the last command row", () => {
const toggle = {
setAttribute() {},
addEventListener() {},
};
const doc = {
querySelector: (sel) => (sel === "#cmdk-trigger" ? toggle : null),
addEventListener() {},
};
const palette = loadMenuModule(fakeStorage(), { document: doc });
palette.initPalette([
{
name: "status",
title: "Status",
hasChildren: true,
pages: [{ name: "overview", title: "Overview", href: "/overview" }],
},
{
name: "logout",
title: "Logout",
href: "/logout",
hasChildren: false,
isLogout: true,
},
]);
const last = palette.paletteIndex.at(-1);
assert.equal(last.isLogout, true);
assert.equal(last.href, "/logout");
assert.equal(last.title, "Logout");
// After the theme commands, so ">" lists modes first, logout last.
assert.ok(palette.paletteIndex.findIndex((page) => page.mode) >= 0);
assert.ok(
palette.paletteIndex.findIndex((page) => page.mode) <
palette.paletteIndex.indexOf(last),
);
});
// ---- Tabs: third-level menu nodes ----
// ui.menu.getChildren() as luci-base ships it, down to handing out alias
// nodes with their target's children (none, for a tab target).
const menuUi = {
menu: {
getChildren: (node) =>
Object.entries(node?.children ?? {})
.filter(([, child]) => child.satisfied && "title" in child)
.map(([name, child]) =>
child.action?.type === "alias"
? { ...child, name, children: undefined }
: Object.assign(child, { name }),
)
.sort((a, b) => (a.order ?? 1000) - (b.order ?? 1000)),
},
};
const node = (title, order, action, children) => ({
satisfied: true,
title,
order,
action,
children,
});
const view = { type: "view" };
const menuRoot = () => ({
name: "admin",
children: {
status: node("Status", 1, view, {
overview: node("Overview", 1, view),
logs: node(
"System Log",
2,
{ type: "alias", path: "admin/status/logs/syslog" },
{
dmesg: node("Kernel Log", 2, view),
syslog: node("System Log", 1, view),
},
),
}),
system: node("System", 2, view, {
admin: node(
"Administration",
1,
{ type: "firstchild" },
{
dropbear: node("SSH Access", 2, view),
hidden: { satisfied: true, order: 0, action: view },
password: node("Router Password", 1, view),
},
),
bmx: node("BMX", 2, view, { nodes: node("Nodes", 1, view) }),
}),
},
});
const menuItems = () => [
{
name: "status",
title: "Status",
hasChildren: true,
pages: [
{ name: "overview", title: "Overview", href: "/admin/status/overview" },
{ name: "logs", title: "System Log", href: "/admin/status/logs" },
],
},
{
name: "system",
title: "System",
hasChildren: true,
pages: [
{ name: "admin", title: "Administration", href: "/admin/system/admin" },
{ name: "bmx", title: "BMX", href: "/admin/system/bmx" },
],
},
];
const initTabbedPalette = (storage = fakeStorage()) => {
const doc = {
querySelector: (sel) =>
sel === "#cmdk-trigger"
? { setAttribute() {}, addEventListener() {} }
: null,
addEventListener() {},
};
const palette = loadMenuModule(storage, { document: doc, ui: menuUi });
palette.initPalette(menuItems(), menuRoot());
return palette;
};
const navRows = (palette) =>
palette.paletteIndex.filter((page) => !page.mode && !page.isLogout);
test("tabs join the index; a redirecting parent gives way to them", () => {
const rows = navRows(initTabbedPalette());
assert.deepEqual(
rows.map((page) => [page.name, page.parent ?? null]),
[
["status/overview", null],
["status/logs/syslog", "System Log"],
["status/logs/dmesg", "System Log"],
["system/admin/password", "Administration"],
["system/admin/dropbear", "Administration"],
["system/bmx", null],
["system/bmx/nodes", "BMX"],
],
);
assert.equal(rows[2].href, "/admin/status/logs/dmesg");
assert.equal(rows[2].group, "Status");
});
test("redirecting parents map to the tab they open", () => {
assert.deepEqual(initTabbedPalette().paletteAliases, {
"status/logs": "status/logs/syslog",
"system/admin": "system/admin/password",
});
});
test("a stored parent name reads as its tab, deduplicated", () => {
const palette = initTabbedPalette(
fakeStorage(
stored(["system/admin", "system/admin/password", "status/logs"]),
),
);
assert.deepEqual(palette.readPaletteRecents(), [
"system/admin/password",
"status/logs/syslog",
]);
assert.deepEqual(names(palette.collectPaletteMatches("")).slice(0, 3), [
"system/admin/password",
"status/logs/syslog",
"status/overview",
]);
});
const tab = (name, title, parent = "Firewall") => ({
title,
parent,
name: `network/firewall/${name}`,
group: "Network",
href: `/${name}`,
});
const firewallIndex = () => [
navPage("status/nftables", "Firewall", "Status"),
tab("zones", "General Settings"),
tab("forwards", "Port Forwards"),
tab("rules", "Traffic Rules"),
tab("ipsets", "IP Sets"),
];
test("a parent-only query keeps its tabs in menu order", () => {
const palette = paletteWith(fakeStorage(), firewallIndex());
assert.deepEqual(names(palette.collectPaletteMatches("firewall")), [
"status/nftables",
"network/firewall/zones",
"network/firewall/forwards",
"network/firewall/rules",
"network/firewall/ipsets",
]);
});
test("a parent hit highlights the parent, not the title", () => {
const match = menu.matchPaletteEntry(
"fire",
tab("forwards", "Port Forwards"),
);
assert.equal(match.ranges, null);
assert.deepEqual(sliced("Firewall", match.parentRanges), ["Fire"]);
});
test("a spaced query pairs parent words with title words", () => {
const palette = paletteWith(fakeStorage(), firewallIndex());
const [hit, ...rest] = palette.collectPaletteMatches("firewall port");
assert.equal(hit.page.name, "network/firewall/forwards");
assert.deepEqual(rest, []);
assert.deepEqual(sliced("Port Forwards", hit.ranges), ["Port"]);
assert.deepEqual(sliced("Firewall", hit.parentRanges), ["Firewall"]);
});
test("a query no longer scatters across path segments", () => {
const other = {
title: "Other Settings",
parent: "PassWall 2",
name: "services/passwall2/other",
group: "Services",
};
assert.ok(!menu.matchPaletteEntry("ssh", other));
assert.ok(
menu.matchPaletteEntry("sshkeys", {
title: "SSH 密钥",
parent: "管理权",
name: "system/admin/sshkeys",
group: "系统",
}),
);
});
test("path words land in segments in order, never scattered", () => {
const wireless = { title: "无线", name: "network/wireless", group: "网络" };
const forwards = tab("forwards", "端口转发", "防火墙");
assert.ok(menu.matchPaletteEntry("network wireless", wireless));
assert.ok(
menu.matchPaletteEntry(
"status/overview",
navPage("status/overview", "概览"),
),
);
assert.ok(menu.matchPaletteEntry("network firewall", forwards));
assert.ok(menu.matchPaletteEntry("netw forw", forwards));
assert.ok(!menu.matchPaletteEntry("wireless network", wireless));
assert.ok(!menu.matchPaletteEntry("netwire", wireless));
assert.ok(!menu.matchPaletteEntry("/", wireless));
});
test("a parent keeps its row unless it redirects to one of its own tabs", () => {
const doc = {
querySelector: (sel) =>
sel === "#cmdk-trigger"
? { setAttribute() {}, addEventListener() {} }
: null,
addEventListener() {},
};
const palette = loadMenuModule(fakeStorage(), { document: doc, ui: menuUi });
const root = {
name: "admin",
children: {
services: node("Services", 1, view, {
// Redirects to an untitled page, so no tab stands in for it.
foo: node(
"Foo",
1,
{ type: "alias", path: "admin/services/foo/main" },
{
main: { satisfied: true, order: 1, action: view },
log: node("Log", 2, view),
},
),
bar: node(
"Bar",
2,
{ type: "firstchild" },
{ only: { ...node("Only", 1, view), firstchild_ineligible: true } },
),
}),
},
};
palette.initPalette(
[
{
name: "services",
title: "Services",
hasChildren: true,
pages: [
{ name: "foo", title: "Foo", href: "/admin/services/foo" },
{ name: "bar", title: "Bar", href: "/admin/services/bar" },
],
},
],
root,
);
assert.deepEqual(
navRows(palette).map((page) => page.name),
["services/foo", "services/foo/log", "services/bar", "services/bar/only"],
);
assert.deepEqual(palette.paletteAliases, {});
});
test("recording after a legacy read writes the mapped names back", () => {
const storage = fakeStorage(stored(["status/logs", "status/overview"]));
const palette = initTabbedPalette(storage);
palette.recordPaletteRecent("system/admin/dropbear");
assert.deepEqual(JSON.parse(storage.map.get(RECENTS_KEY)), [
"system/admin/dropbear",
"status/logs/syslog",
"status/overview",
]);
});
test("a tab row renders its parent before the title unless they share a name", () => {
const rows = [];
const palette = paletteWith(fakeStorage(), [
tab("forwards", "Port Forwards"),
{ ...tab("firewall", "Firewall"), name: "network/firewall/firewall" },
]);
palette.paletteInput = { removeAttribute() {}, setAttribute() {} };
palette.paletteList = {
replaceChildren() {},
appendChild: (row) => rows.push(row),
};
palette.setPaletteSelection = () => {};
palette.renderPaletteResults("fire");
const title = (name) =>
rows.find((row) => row.attributes["data-name"] === name).children[0]
.children;
const forwards = title("network/firewall/forwards");
const same = title("network/firewall/firewall");
assert.deepEqual(
forwards.map((part) => part.attributes.class),
["cmdk-parent", "cmdk-label"],
);
const marked = (parts) => parts.some((part) => part.tagName === "mark");
// The parent hit is highlighted inside the parent, the title stays plain.
assert.ok(marked(forwards[0].children));
assert.deepEqual(forwards[1].children, ["Port Forwards"]);
// Same name: a plain highlighted title, no prefix.
assert.ok(marked(same));
assert.ok(!same.some((part) => part.attributes?.class === "cmdk-parent"));
});
+187
View File
@@ -0,0 +1,187 @@
// Geometry checks for the DOM compat fixtures (see ../compat/README.md).
// Each fixture is a trimmed page that links the built main.css and declares
// its checks in <script type="application/json" id="compat">. They run in
// headless Chrome over raw CDP — no npm dependency, same approach as the
// performance skill's bench scripts.
import assert from "node:assert/strict";
import { spawn, execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import { setTimeout as sleep } from "node:timers/promises";
const fixtureDir = resolve(import.meta.dirname, "../compat/fixtures");
const output = resolve(import.meta.dirname, "../../htdocs/luci-static/aurora");
// header.ut inlines fonts/aurora-font.css; its absolute /luci-static/ URL
// cannot resolve under file://, so the runner injects it with a file URL.
// Without the theme font the fallback metrics differ enough to flip a wrap.
const fontCss = existsSync(join(output, "fonts/aurora-font.css"))
? readFileSync(join(output, "fonts/aurora-font.css"), "utf8")
.replaceAll("/luci-static/aurora/", pathToFileURL(output).href + "/")
: "";
const fixtures = existsSync(fixtureDir)
? readdirSync(fixtureDir).filter((f) => f.endsWith(".html")).sort()
: [];
function findChrome() {
const candidates = [
process.env.CHROME_BIN,
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
].filter(Boolean);
for (const c of candidates) if (existsSync(c)) return c;
for (const name of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]) {
try {
return execFileSync("which", [name], { encoding: "utf8" }).trim() || null;
} catch {}
}
return null;
}
const chromeBin = findChrome();
const skipReason =
fixtures.length === 0
? "no fixtures in compat/fixtures"
: chromeBin
? null
: "no Chrome found (set CHROME_BIN); required when CI is set";
if (skipReason && process.env.CI && fixtures.length > 0) {
test("compat fixtures need a browser on CI", () => assert.fail(skipReason));
}
// The checks run inside the page; `sel` is the selector from the fixture.
const CHECKS = {
// Every matched element's text sits on one line, and no text starts below
// an inline checkbox/indicator inside it (the "label under the icon" wrap).
nowrap: (sel) => `(() => {
const bad = [];
for (const el of document.querySelectorAll(${JSON.stringify(sel)})) {
const tops = [];
const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
for (let n; (n = w.nextNode());) {
if (!n.textContent.trim()) continue;
const r = document.createRange(); r.selectNodeContents(n);
for (const x of r.getClientRects()) if (x.width > 0) tops.push(x.top);
}
const ind = el.querySelector('input, .partition-color-indicator, img, svg');
const wrapped = tops.some((t) => Math.abs(t - tops[0]) > 3) ||
(ind && tops.length && tops[0] >= ind.getBoundingClientRect().bottom - 1);
if (wrapped) bad.push(el.textContent.trim().slice(0, 40));
}
return bad;
})()`,
// No horizontal overflow inside, and the box stays inside its parent.
fits: (sel) => `(() => {
const bad = [];
for (const el of document.querySelectorAll(${JSON.stringify(sel)})) {
const r = el.getBoundingClientRect(), p = el.parentElement.getBoundingClientRect();
if (el.scrollWidth > el.clientWidth + 1 || r.right > p.right + 1)
bad.push((el.id || el.className || el.tagName) + ' ' + Math.round(el.scrollWidth) + '/' + Math.round(el.clientWidth) + ' right ' + Math.round(r.right) + '>' + Math.round(p.right));
}
return bad;
})()`,
js: (expr) => `(() => { const v = (${expr}); return v ? [] : ['expression was ' + JSON.stringify(v)]; })()`,
};
function readSpec(file) {
const html = readFileSync(join(fixtureDir, file), "utf8");
const m = html.match(/<script[^>]*id="compat"[^>]*>([\s\S]*?)<\/script>/);
assert.ok(m, `${file}: missing <script id="compat"> block`);
const spec = JSON.parse(m[1]);
assert.ok(Array.isArray(spec.checks) && spec.checks.length, `${file}: no checks`);
return spec;
}
test("compat fixtures keep their geometry", { skip: skipReason ?? false }, async (t) => {
const profile = mkdtempSync(join(tmpdir(), "cdp-compat-"));
const chrome = spawn(
chromeBin,
["--headless=new", "--remote-debugging-port=0", `--user-data-dir=${profile}`,
"--no-first-run", "--no-default-browser-check", "--allow-file-access-from-files", "about:blank"],
{ stdio: "ignore" },
);
let port = null;
for (let i = 0; i < 100 && !port; i++) {
await sleep(100);
const f = join(profile, "DevToolsActivePort");
if (existsSync(f)) port = +readFileSync(f, "utf8").split("\n")[0];
}
assert.ok(port, "chrome did not expose a DevTools port");
const { webSocketDebuggerUrl } = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json();
const ws = new WebSocket(webSocketDebuggerUrl);
await new Promise((res, rej) => {
ws.addEventListener("open", res, { once: true });
ws.addEventListener("error", () => rej(new Error("CDP connect failed")), { once: true });
});
let mid = 0;
const pending = new Map();
const handlers = new Set();
ws.addEventListener("message", (ev) => {
const m = JSON.parse(ev.data);
if (m.id && pending.has(m.id)) {
const { res, rej } = pending.get(m.id);
pending.delete(m.id);
m.error ? rej(new Error(m.error.message)) : res(m.result);
} else for (const h of handlers) h(m);
});
const send = (method, params = {}, sessionId) =>
new Promise((res, rej) => {
const id = ++mid;
pending.set(id, { res, rej });
ws.send(JSON.stringify({ id, method, params, sessionId }));
});
try {
for (const file of fixtures) {
const spec = readSpec(file);
const { targetId } = await send("Target.createTarget", { url: "about:blank" });
const { sessionId } = await send("Target.attachToTarget", { targetId, flatten: true });
await send("Page.enable", {}, sessionId);
if (fontCss)
await send("Page.addScriptToEvaluateOnNewDocument", { source: `
document.addEventListener('DOMContentLoaded', () => {
const s = document.createElement('style');
s.textContent = ${JSON.stringify(fontCss)};
document.head.prepend(s);
});` }, sessionId);
const evaljs = async (expression) => {
const r = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true }, sessionId);
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description ?? r.exceptionDetails.text);
return r.result.value;
};
for (const width of spec.viewports ?? [1280]) {
await send("Emulation.setDeviceMetricsOverride",
{ width, height: 900, deviceScaleFactor: 1, mobile: width < 768 }, sessionId);
const loaded = new Promise((res) => {
const h = (m) => { if (m.method === "Page.loadEventFired" && m.sessionId === sessionId) { handlers.delete(h); res(); } };
handlers.add(h);
});
await send("Page.navigate", { url: pathToFileURL(join(fixtureDir, file)).href }, sessionId);
await loaded;
// Force the theme font to load before measuring (font-display: swap
// would otherwise measure the fallback), then give layout one frame.
await evaljs(`document.fonts.load('12px "Lato"').then(() => document.fonts.ready)
.then(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))))`);
const fontOk = await evaljs(`!${JSON.stringify(!!fontCss)} || document.fonts.check('12px "Lato"')`);
assert.ok(fontOk, `${file}: theme font did not load from ${output}/fonts`);
const sheets = await evaljs("[...document.styleSheets].map(s => { try { return s.cssRules.length } catch { return -1 } })");
assert.ok(sheets.some((n) => n > 100), `${file}: main.css did not load (rules per sheet: ${sheets}) — run pnpm build`);
for (const check of spec.checks) {
if (check.viewports && !check.viewports.includes(width)) continue;
const [kind, arg] = Object.entries(check).find(([k]) => k in CHECKS) ?? [];
assert.ok(kind, `${file}: unknown check ${JSON.stringify(check)}`);
const bad = await evaljs(CHECKS[kind](arg));
assert.deepEqual(bad, [], `${file} @${width} ${kind} ${JSON.stringify(arg)} (ledger ${spec.ledger}): ${bad.join("; ")}`);
}
}
await send("Target.closeTarget", { targetId });
}
} finally {
ws.close();
chrome.kill();
await sleep(200);
rmSync(profile, { recursive: true, force: true });
}
});
@@ -0,0 +1,186 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { existsSync, readFileSync, statSync } from "node:fs";
import { resolve } from "node:path";
import { test } from "node:test";
/* The patch ships as a plain deferred script, not a module. Evaluate it with
an injected `module` to reach its parsing exports; `document` stays
undefined so the DOM bootstrap is skipped. */
const source = await readFile(
new URL("../src/resource/patches/admin-status-logs.js", import.meta.url),
"utf8",
);
const mod = { exports: {} };
new Function("module", "exports", "document", "window", source)(
mod,
mod.exports,
undefined,
undefined,
);
const { parseSyslogLine, parseDmesgLine, evaluate, PARSE_GATE } = mod.exports;
test("classic logread format (23.05 syslog, 24.10 syslog-wrapper) parses", () => {
const r = parseSyslogLine(
"Sat Jul 26 03:14:15 2026 daemon.info dnsmasq-dhcp[1]: DHCPACK(br-lan) 192.168.1.140",
);
assert.deepEqual(
{ time: r.time, fac: r.fac, sev: r.sev, sevTok: r.sevTok, tag: r.tag },
{
time: "Sat Jul 26 03:14:15 2026",
fac: "daemon",
sev: "info",
sevTok: "daemon.info",
tag: "dnsmasq-dhcp[1]",
},
);
assert.equal(r.msg, "DHCPACK(br-lan) 192.168.1.140");
// ctime() pads single-digit days with a space — kept verbatim.
const padded = parseSyslogLine(
"Sun Jul 6 09:05:07 2026 daemon.warn odhcpd[2029]: No default route present",
);
assert.equal(padded.time, "Sun Jul 6 09:05:07 2026");
// Kernel lines routed through logread; hostapd logs without a pid.
assert.equal(
parseSyslogLine(
"Sat Jul 26 03:14:16 2026 kern.info kernel: [73412.882110] br-lan: port 3(lan3) entered forwarding state",
).tag,
"kernel",
);
assert.equal(
parseSyslogLine(
"Sat Jul 26 03:14:17 2026 daemon.notice hostapd: wlan1: AP-STA-CONNECTED 3c:22:fb:8a:12:9d",
).tag,
"hostapd",
);
});
test("master RPC bracket format parses across browser locales", () => {
// The datestr renders verbatim, brackets and timezone included — the
// stamp is UTC-formatted upstream and hiding the zone read as local time.
const en = parseSyslogLine(
"[Jul 24, 2026, 10:59:50 PM UTC] daemon.info: dnsmasq-dhcp[1]: DHCPREQUEST(br-lan) 192.168.1.140",
);
assert.equal(en.time, "[Jul 24, 2026, 10:59:50 PM UTC]");
assert.equal(en.sevTok, "daemon.info:");
assert.equal(
parseSyslogLine(
"[Jul 24, 2026, 10:59:50 PM Coordinated Universal Time] daemon.warn: odhcpd[2029]: No default route present",
).time,
"[Jul 24, 2026, 10:59:50 PM Coordinated Universal Time]",
);
// Non-en datestrs parse fine and display untouched — never guessed at.
const zh = parseSyslogLine(
"[2026年7月24日 GMT 22:59:50] daemon.err: odhcpd[2029]: Failed to send RA",
);
assert.equal(zh.sev, "err");
assert.equal(zh.time, "[2026年7月24日 GMT 22:59:50]");
});
test("rendered tokens rejoin into the source line byte for byte", () => {
const lines = [
"[Jul 24, 2026, 10:59:50 PM UTC] daemon.info: dnsmasq-dhcp[1]: DHCPREQUEST(br-lan) 192.168.1.140",
"[Jul 25, 2026, 4:30:55 PM UTC] kern.info: [ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x51af8014]",
"Sun Jul 6 09:05:07 2026 daemon.warn odhcpd[2029]: No default route present",
"Sat Jul 26 03:14:16 2026 kern.info kernel: [73412.882110] br-lan: port 3(lan3) entered forwarding state",
];
for (const line of lines) {
const p = parseSyslogLine(line);
const parts = [p.time, p.sevTok];
if (p.tag) parts.push(p.tag + ":");
parts.push(p.msg);
assert.equal(parts.join(" "), line, line);
}
const dm = "[ 12.345678] br-lan: port 3(lan3) entered forwarding state";
const d = parseDmesgLine(dm);
assert.equal([d.time, d.msg].join(" "), dm);
});
test("severity comes from the field, never from message content", () => {
const r = parseSyslogLine(
"[Jul 24, 2026, 11:00:00 PM UTC] daemon.info: logger: user said daemon.err: fake",
);
assert.equal(r.sev, "info");
assert.equal(r.msg, "user said daemon.err: fake");
});
test("unknown facility/severity and foreign formats are rejected", () => {
assert.equal(
parseSyslogLine("[Jul 24, 2026, 11:00:00 PM UTC] bogus.info: x: y"),
null,
);
assert.equal(
parseSyslogLine("[Jul 24, 2026, 11:00:00 PM UTC] daemon.bogus: x: y"),
null,
);
// syslog-ng's classic template has no facility.severity field at all.
assert.equal(
parseSyslogLine("Jul 26 10:00:00 OpenWrt dnsmasq[123]: query answered"),
null,
);
assert.equal(parseSyslogLine(""), null);
});
test("dmesg lines: uptime stamp only, message verbatim, no severity", () => {
// Kernel lines have no uniform tag grammar (upstream renders them as plain
// text for the same reason) — a partial split styled only some lines and
// ate only their colons, so no tag is ever extracted and the message keeps
// its punctuation untouched.
const r = parseDmesgLine(
"[ 12.345678] br-lan: port 3(lan3) entered forwarding state",
);
assert.equal(r.time, "[ 12.345678]");
assert.equal(r.tag, "");
assert.equal(r.sev, "");
assert.equal(r.msg, "br-lan: port 3(lan3) entered forwarding state");
const pci = parseDmesgLine(
"[75821.004518] mt7921e 0000:01:00.0: Message 00000010 timeout",
);
assert.equal(pci.tag, "");
assert.equal(pci.msg, "mt7921e 0000:01:00.0: Message 00000010 timeout");
assert.equal(parseDmesgLine("no bracket prefix here"), null);
});
test("parse gate: mixed content falls back, empty value stays calm", () => {
const good = "Sat Jul 26 03:14:15 2026 daemon.info dnsmasq[1]: ok";
const bad = "Jul 26 10:00:00 OpenWrt dnsmasq[123]: syslog-ng style";
const healthy = evaluate(
[good, good, good, good, bad].join("\n"),
parseSyslogLine,
);
assert.ok(healthy.ratio >= PARSE_GATE, "4/5 parsed must stay enhanced");
const foreign = evaluate([bad, bad, bad].join("\n"), parseSyslogLine);
assert.ok(foreign.ratio < PARSE_GATE, "foreign log must fall back");
const empty = evaluate("", parseSyslogLine);
assert.equal(empty.rows.length, 0);
assert.equal(empty.ratio, 1, "empty log must not trip the gate");
});
test("built log payloads land in their homes", () => {
const out = resolve(import.meta.dirname, "../../htdocs/luci-static/aurora");
// The JS ships on-demand under one name — every supported release
// (23.05/24.10/master) mounts both log pages under admin/status/logs/*.
const js = resolve(out, "patches/admin-status-logs.js");
assert.ok(existsSync(js), `${js} missing — run pnpm build first`);
assert.ok(statSync(js).size <= 6_000, `${js} exceeds 6 KB`);
// The CSS is core-page styling and lives in the main bundle, where the
// Tailwind token pipeline (radius-base chain, app shadows) applies — a
// stray patches/ copy would mean the component migration regressed.
const main = readFileSync(resolve(out, "main.css"), "utf8");
assert.ok(main.includes(".syslog-view"), "main.css lost the log viewer");
assert.ok(
main.includes("calc(var(--radius-base)"),
"radius utilities no longer resolve through the reactive base chain",
);
assert.ok(!existsSync(resolve(out, "patches/admin-status-logs.css")));
assert.ok(!existsSync(resolve(out, "patches/_log-shared.css")));
});
@@ -0,0 +1,242 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";
const header = readFileSync(
resolve(import.meta.dirname, "../../ucode/template/themes/aurora/header.ut"),
"utf8",
);
// Just enough ucode for these scripts: comments and the nav-type branches.
const renderFor = (source, navType) =>
source
.replace(/\{#[\s\S]*?#\}/g, "")
.replace(
/\{% if \(nav_type == 'sidebar'\): %\}([\s\S]*?)\{% endif %\}/g,
(_, body) => (navType === "sidebar" ? body : ""),
);
const replaySource = header.match(
/<script>\s*(\{% if \(nav_type == 'sidebar'\): %\}\s*\{#[^#]*#\}\s*function auroraCrumb[\s\S]*?)<\/script>/,
)?.[1];
const loginSource = header.match(
/\{% if \(blank_page\): %\}\s*<script>([\s\S]*?)<\/script>/,
)?.[1];
const classes = (...initial) => {
const set = new Set(initial);
return {
set,
contains: (name) => set.has(name),
toggle: (name, on) => (on ? set.add(name) : set.delete(name), on),
};
};
const surface = (extra = {}) => {
const el = { style: { display: "none" }, dataset: {}, html: null, ...extra };
Object.defineProperty(el, "innerHTML", {
set(value) {
el.html = value;
},
});
return el;
};
// A sidebar as the snapshot left it: System was the active group then.
const sidebarFixture = (activePath) => {
const group = (name, label, links) => {
const item = { classList: classes(), lastChild: {} };
if (name === "system")
item.classList.set.add("is-active-group").add("is-expanded");
item.toggle = {
tagName: "BUTTON",
dataset: { section: name },
parentNode: item,
};
item.label = { textContent: label };
item.links = links.map(([page, text]) => ({
pathname: `/cgi-bin/luci/admin/${name}/${page}`,
textContent: text,
classList: classes(
...(name === "system" && page === "system" ? ["is-active-page"] : []),
),
}));
item.querySelector = (sel) =>
sel === ".nav-category-label" ? item.label : null;
return item;
};
const direct = {
tagName: "A",
dataset: { section: "status" },
textContent: "Status",
classList: classes(),
};
const groups = [
group("network", "Network", [
["wireless", "Wireless"],
["network", "Interfaces"],
]),
group("system", "System", [
["system", "System"],
["admin", "Administration"],
]),
];
const links = [direct, ...groups.flatMap((g) => g.links)];
const list = surface({
dataset: { activePath },
querySelectorAll: (sel) =>
sel === "[data-section]"
? [direct, ...groups.map((g) => g.toggle)]
: sel === ".navigation-sublink"
? groups.flatMap((g) => g.links)
: [],
querySelector: (sel) =>
sel === ".is-active-group"
? (groups.find((g) => g.classList.contains("is-active-group")) ?? null)
: sel === ".is-active-page"
? (links.find((l) => l.classList.contains("is-active-page")) ?? null)
: null,
});
return { list, direct, groups };
};
const run = ({
navType,
stamp = "s",
cached = "s",
luciMenu = true,
elements,
}) => {
const store = {
"aurora.nav": JSON.stringify([
cached,
Object.fromEntries(
Object.keys(elements)
.filter((id) => id !== "header-crumb")
.map((id) => [id, `<${id}>`]),
),
]),
"luci-session-store": JSON.stringify({ sid: luciMenu ? { menu: {} } : {} }),
};
const document = {
body: { dataset: { navStamp: stamp } },
getElementById: (id) => elements[id] ?? null,
createElement: () => ({}),
};
new Function("document", "sessionStorage", renderFor(replaySource, navType))(
document,
{
getItem: (key) => store[key] ?? null,
},
);
};
test("header.ut carries the replay and the login-page clear", () => {
assert.ok(replaySource, "replay script not found");
assert.ok(loginSource, "login-page script not found");
});
test("a sidebar replay paints inert and re-marks the current page", () => {
const { list, direct, groups } = sidebarFixture("network/wireless");
const footer = surface();
const crumb = { replaceChildren: (...items) => (crumb.items = items) };
run({
navType: "sidebar",
elements: {
"sidebar-list": list,
"sidebar-footer": footer,
"header-crumb": crumb,
},
});
for (const el of [list, footer]) {
assert.equal(
el.html,
`<${el === list ? "sidebar-list" : "sidebar-footer"}>`,
);
assert.equal(el.inert, true);
assert.equal(el.dataset.restored, "");
}
const [network, system] = groups;
assert.deepEqual(
[...network.classList.set],
["is-active-group", "is-expanded"],
);
assert.deepEqual([...system.classList.set], []);
assert.equal(direct.classList.contains("is-active-page"), false);
assert.deepEqual(
groups
.flatMap((g) => g.links)
.filter((l) => l.classList.contains("is-active-page"))
.map((l) => l.textContent),
["Wireless"],
);
assert.deepEqual(
crumb.items.map((li) => [li.className, li.textContent]),
[
["", "Network"],
["crumb-sep", "/"],
["current", "Wireless"],
],
);
});
test("a direct row is marked on its own section, with no sublink", () => {
const { list, direct, groups } = sidebarFixture("status");
const crumb = { replaceChildren: (...items) => (crumb.items = items) };
run({
navType: "sidebar",
elements: { "sidebar-list": list, "header-crumb": crumb },
});
assert.equal(direct.classList.contains("is-active-page"), true);
assert.ok(groups.every((g) => g.classList.set.size === 0));
assert.ok(
groups
.flatMap((g) => g.links)
.every((l) => !l.classList.contains("is-active-page")),
);
assert.deepEqual(
crumb.items.map((li) => [li.className, li.textContent]),
[["current", "Status"]],
);
});
test("a top menu replay only paints, inert", () => {
const topmenu = surface();
run({ navType: "mega-menu", elements: { topmenu } });
assert.equal(topmenu.html, "<topmenu>");
assert.equal(topmenu.style.display, "");
assert.equal(topmenu.inert, true);
});
test("nothing is replayed for another stamp or after luci-base flushed its menu", () => {
for (const options of [{ cached: "other user" }, { luciMenu: false }]) {
const topmenu = surface();
run({ navType: "mega-menu", elements: { topmenu }, ...options });
assert.equal(topmenu.html, null, JSON.stringify(options));
assert.equal(topmenu.inert, undefined);
}
});
test("the login page drops the cached nav", () => {
const removed = [];
new Function(
"localStorage",
"sessionStorage",
"window",
"document",
renderFor(loginSource),
)(
{ getItem: () => null },
{ removeItem: (key) => removed.push(key) },
{ matchMedia: () => ({ matches: false }) },
{ documentElement: { setAttribute() {} } },
);
assert.deepEqual(removed, ["aurora.nav"]);
});
@@ -0,0 +1,569 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
const source = await readFile(
new URL("../src/resource/menu-aurora.js", import.meta.url),
"utf8",
);
const menuOrder = (item) => (Number.isFinite(item.order) ? item.order : 1000);
const compareMenuItems = (a, b) => {
const orderDifference = menuOrder(a) - menuOrder(b);
if (orderDifference) return orderDifference;
if (a.name === b.name) return 0;
return a.name < b.name ? -1 : 1;
};
const getMenuChildren = (children = {}) =>
Object.entries(children)
.filter(([, child]) => child?.satisfied !== false && child?.title)
.map(([name, child]) => ({ ...child, name }))
.sort(compareMenuItems);
const loadMenuModule = (
dispatchpath,
getChildren = (item) => getMenuChildren(item?.children),
) => {
const baseclass = {
extend(module) {
return module;
},
};
const ui = { menu: { getChildren } };
const E = () => ({});
const L = {
env: { dispatchpath },
url: (...segments) => `/${segments.join("/")}`,
};
const translate = (title) => `translated:${title}`;
const document = {};
const window = {};
const localStorage = {};
return new Function(
"baseclass",
"ui",
"E",
"L",
"_",
"document",
"window",
"localStorage",
source,
)(baseclass, ui, E, L, translate, document, window, localStorage);
};
test("normalizes, orders, and filters a mixed navigation model", () => {
const menu = loadMenuModule(["admin", "network", "wireless"]);
const tree = {
children: {
logout: { title: "Logout", order: 30 },
titleless: { order: 5 },
status: { title: "Status", order: 20 },
unavailable: { title: "Unavailable", order: 4, satisfied: false },
network: {
title: "Network",
order: 10,
children: {
wireless: { title: "Wireless", order: 10 },
hidden: { title: "Hidden", order: 5, satisfied: false },
titleless: { order: 6 },
interfaces: { title: "Interfaces", order: 10 },
},
},
},
};
assert.deepEqual(
menu.buildNavigationModel(getMenuChildren(tree.children), "admin"),
[
{
name: "network",
title: "translated:Network",
href: "/admin/network",
hasChildren: true,
isLogout: false,
isActiveGroup: true,
isActivePage: false,
activePage: {
name: "wireless",
title: "translated:Wireless",
href: "/admin/network/wireless",
isActivePage: true,
},
pages: [
{
name: "interfaces",
title: "translated:Interfaces",
href: "/admin/network/interfaces",
isActivePage: false,
},
{
name: "wireless",
title: "translated:Wireless",
href: "/admin/network/wireless",
isActivePage: true,
},
],
},
{
name: "status",
title: "translated:Status",
href: "/admin/status",
hasChildren: false,
isLogout: false,
isActiveGroup: false,
isActivePage: false,
activePage: null,
pages: [],
},
{
name: "logout",
title: "translated:Logout",
href: "/admin/logout",
hasChildren: false,
isLogout: true,
isActiveGroup: false,
isActivePage: false,
activePage: null,
pages: [],
},
],
);
});
test("marks a direct current destination as an active page, not a group", () => {
const menu = loadMenuModule(["admin", "status"]);
const tree = {
children: {
status: { title: "Status" },
},
};
assert.deepEqual(
menu.buildNavigationModel(getMenuChildren(tree.children), "admin"),
[
{
name: "status",
title: "translated:Status",
href: "/admin/status",
hasChildren: false,
isLogout: false,
isActiveGroup: false,
isActivePage: true,
activePage: null,
pages: [],
},
],
);
});
test("omits invalid top-level items without a name", () => {
const menu = loadMenuModule(["admin", "status"]);
assert.deepEqual(
menu.buildNavigationModel(
[
{ title: "Invalid", children: {} },
...getMenuChildren({
status: { title: "Status" },
}),
],
"admin",
),
[
{
name: "status",
title: "translated:Status",
href: "/admin/status",
hasChildren: false,
isLogout: false,
isActiveGroup: false,
isActivePage: true,
activePage: null,
pages: [],
},
],
);
});
test("omits pages without a name returned by the menu API", () => {
const getChildren = () => [
{ title: "Invalid" },
{ name: "wireless", title: "Wireless" },
];
const menu = loadMenuModule(["admin", "network", "wireless"], getChildren);
assert.deepEqual(
menu.buildNavigationModel(
[{ name: "network", title: "Network" }],
"admin",
)[0].pages,
[
{
name: "wireless",
title: "translated:Wireless",
href: "/admin/network/wireless",
isActivePage: true,
},
],
);
});
test("returns an empty navigation model for empty input", () => {
const menu = loadMenuModule(["admin"]);
assert.deepEqual(menu.buildNavigationModel([], "admin"), []);
});
const createClassList = (...classes) => {
const values = new Set(classes);
return {
contains(className) {
return values.has(className);
},
toggle(className, force) {
const enabled = force ?? !values.has(className);
if (enabled) values.add(className);
else values.delete(className);
return enabled;
},
};
};
const createFakeElement = (...classes) => {
const attributes = new Map();
const element = {
classList: createClassList(...classes),
parentElement: null,
closest(selector) {
if (!selector.startsWith(".")) return null;
const className = selector.slice(1);
let current = element;
while (current) {
if (current.classList?.contains(className)) return current;
current = current.parentElement;
}
return null;
},
getAttribute(name) {
return attributes.get(name) ?? null;
},
hasAttribute(name) {
return attributes.has(name);
},
querySelector() {
return null;
},
removeAttribute(name) {
attributes.delete(name);
},
setAttribute(name, value) {
attributes.set(name, String(value));
},
};
return element;
};
const createNavigationGroup = (...classes) => {
const item = createFakeElement("navigation-group", ...classes);
const toggle = createFakeElement("navigation-group-toggle");
const region = createFakeElement("navigation-group-region");
toggle.parentElement = item;
region.parentElement = item;
item.querySelector = (selector) => {
if (selector === ".navigation-group-toggle") return toggle;
if (selector === ".navigation-group-region") return region;
return null;
};
return { item, region, toggle };
};
const createNavigationSurface = (groups) => {
const items = groups.map((group) => group.item);
const listeners = new Map();
return {
dataset: {},
addEventListener(type, listener) {
const typeListeners = listeners.get(type) ?? [];
typeListeners.push(listener);
listeners.set(type, typeListeners);
},
contains(item) {
return items.includes(item);
},
dispatch(type, event) {
for (const listener of listeners.get(type) ?? []) listener(event);
},
listenerCount(type) {
return listeners.get(type)?.length ?? 0;
},
querySelector(selector) {
if (selector === ".navigation-group.is-active-group") {
return (
items.find((item) => item.classList.contains("is-active-group")) ??
null
);
}
return null;
},
querySelectorAll(selector) {
if (selector === ".navigation-group.is-expanded") {
return items.filter((item) => item.classList.contains("is-expanded"));
}
return [];
},
};
};
test("sets one navigation group expanded while preserving active state", () => {
const menu = loadMenuModule(["admin"]);
const { item, region, toggle } = createNavigationGroup(
"is-active-group",
"custom-state",
);
region.setAttribute("aria-hidden", "true");
region.setAttribute("inert", "");
menu.setNavigationGroupExpanded(item, true);
assert.equal(item.classList.contains("is-expanded"), true);
assert.equal(item.classList.contains("is-active-group"), true);
assert.equal(item.classList.contains("custom-state"), true);
assert.equal(toggle.getAttribute("aria-expanded"), "true");
assert.equal(region.getAttribute("aria-hidden"), "false");
assert.equal(region.hasAttribute("inert"), false);
menu.setNavigationGroupExpanded(item, false);
assert.equal(item.classList.contains("is-expanded"), false);
assert.equal(item.classList.contains("is-active-group"), true);
assert.equal(item.classList.contains("custom-state"), true);
assert.equal(toggle.getAttribute("aria-expanded"), "false");
assert.equal(region.getAttribute("aria-hidden"), "true");
assert.equal(region.getAttribute("inert"), "");
});
test("sets an expanded navigation group when controls or regions are missing", () => {
const menu = loadMenuModule(["admin"]);
const item = createFakeElement("navigation-group");
assert.doesNotThrow(() => menu.setNavigationGroupExpanded(item, true));
assert.equal(item.classList.contains("is-expanded"), true);
});
test("exclusively expands one navigation group", () => {
const menu = loadMenuModule(["admin"]);
const first = createNavigationGroup();
const second = createNavigationGroup();
const surface = createNavigationSurface([first, second]);
menu.setNavigationGroupExpanded(first.item, true);
menu.setExclusiveNavigationGroupExpanded(surface, second.item, true);
assert.equal(first.item.classList.contains("is-expanded"), false);
assert.equal(first.toggle.getAttribute("aria-expanded"), "false");
assert.equal(first.region.getAttribute("aria-hidden"), "true");
assert.equal(first.region.hasAttribute("inert"), true);
assert.equal(second.item.classList.contains("is-expanded"), true);
assert.equal(second.toggle.getAttribute("aria-expanded"), "true");
assert.equal(second.region.getAttribute("aria-hidden"), "false");
assert.equal(second.region.hasAttribute("inert"), false);
});
test("collapses only the requested navigation group", () => {
const menu = loadMenuModule(["admin"]);
const first = createNavigationGroup();
const second = createNavigationGroup();
const surface = createNavigationSurface([first, second]);
menu.setNavigationGroupExpanded(first.item, true);
menu.setNavigationGroupExpanded(second.item, true);
menu.setExclusiveNavigationGroupExpanded(surface, second.item, false);
assert.equal(first.item.classList.contains("is-expanded"), true);
assert.equal(first.toggle.getAttribute("aria-expanded"), "true");
assert.equal(second.item.classList.contains("is-expanded"), false);
assert.equal(second.toggle.getAttribute("aria-expanded"), "false");
});
test("ignores missing exclusive navigation group arguments", () => {
const menu = loadMenuModule(["admin"]);
const group = createNavigationGroup();
const surface = createNavigationSurface([group]);
assert.doesNotThrow(() =>
menu.setExclusiveNavigationGroupExpanded(null, group.item, true),
);
assert.doesNotThrow(() =>
menu.setExclusiveNavigationGroupExpanded(surface, null, true),
);
assert.equal(group.item.classList.contains("is-expanded"), false);
});
test("resets every expanded navigation group", () => {
const menu = loadMenuModule(["admin"]);
const first = createNavigationGroup();
const second = createNavigationGroup();
const collapsed = createNavigationGroup();
const surface = createNavigationSurface([first, second, collapsed]);
menu.setNavigationGroupExpanded(first.item, true);
menu.setNavigationGroupExpanded(second.item, true);
menu.resetNavigationGroups(surface);
assert.equal(first.item.classList.contains("is-expanded"), false);
assert.equal(first.toggle.getAttribute("aria-expanded"), "false");
assert.equal(first.region.hasAttribute("inert"), true);
assert.equal(second.item.classList.contains("is-expanded"), false);
assert.equal(second.toggle.getAttribute("aria-expanded"), "false");
assert.equal(second.region.hasAttribute("inert"), true);
assert.equal(collapsed.item.classList.contains("is-expanded"), false);
assert.equal(collapsed.toggle.getAttribute("aria-expanded"), null);
assert.doesNotThrow(() => menu.resetNavigationGroups(null));
});
test("expands the active navigation group exclusively", () => {
const menu = loadMenuModule(["admin"]);
const first = createNavigationGroup();
const active = createNavigationGroup("is-active-group");
const surface = createNavigationSurface([first, active]);
menu.setNavigationGroupExpanded(first.item, true);
menu.expandActiveNavigationGroup(surface);
assert.equal(first.item.classList.contains("is-expanded"), false);
assert.equal(active.item.classList.contains("is-expanded"), true);
assert.equal(active.item.classList.contains("is-active-group"), true);
assert.equal(active.toggle.getAttribute("aria-expanded"), "true");
assert.equal(active.region.hasAttribute("inert"), false);
});
test("ignores missing active navigation groups", () => {
const menu = loadMenuModule(["admin"]);
const surface = createNavigationSurface([createNavigationGroup()]);
assert.doesNotThrow(() => menu.expandActiveNavigationGroup(surface));
assert.doesNotThrow(() => menu.expandActiveNavigationGroup(null));
});
test("binds one delegated navigation accordion listener", () => {
const menu = loadMenuModule(["admin"]);
const first = createNavigationGroup();
const second = createNavigationGroup();
const surface = createNavigationSurface([first, second]);
let prevented = 0;
let stopped = 0;
menu.setNavigationGroupExpanded(first.item, true);
menu.bindNavigationAccordion(surface);
menu.bindNavigationAccordion(surface);
assert.equal(surface.dataset.accordionBound, "true");
assert.equal(surface.listenerCount("click"), 1);
surface.dispatch("click", {
target: second.toggle,
preventDefault() {
prevented += 1;
},
stopPropagation() {
stopped += 1;
},
});
assert.equal(prevented, 1);
assert.equal(stopped, 1);
assert.equal(first.item.classList.contains("is-expanded"), false);
assert.equal(second.item.classList.contains("is-expanded"), true);
surface.dispatch("click", {
target: createFakeElement("outside"),
preventDefault() {
prevented += 1;
},
stopPropagation() {
stopped += 1;
},
});
assert.equal(prevented, 1);
assert.equal(stopped, 1);
assert.equal(second.item.classList.contains("is-expanded"), true);
assert.doesNotThrow(() => menu.bindNavigationAccordion(null));
});
test("ignores delegated clicks from targets without closest", () => {
const menu = loadMenuModule(["admin"]);
const group = createNavigationGroup();
const surface = createNavigationSurface([group]);
let prevented = 0;
let stopped = 0;
menu.bindNavigationAccordion(surface);
assert.doesNotThrow(() =>
surface.dispatch("click", {
target: {},
preventDefault() {
prevented += 1;
},
stopPropagation() {
stopped += 1;
},
}),
);
assert.equal(prevented, 0);
assert.equal(stopped, 0);
assert.equal(group.item.classList.contains("is-expanded"), false);
});
test("handles delegated clicks from inside a navigation group toggle", () => {
const menu = loadMenuModule(["admin"]);
const first = createNavigationGroup();
const second = createNavigationGroup();
const surface = createNavigationSurface([first, second]);
const label = createFakeElement("navigation-group-label");
const span = createFakeElement("navigation-group-label-text");
let prevented = 0;
let stopped = 0;
label.parentElement = second.toggle;
span.parentElement = label;
menu.setNavigationGroupExpanded(first.item, true);
menu.bindNavigationAccordion(surface);
surface.dispatch("click", {
target: span,
preventDefault() {
prevented += 1;
},
stopPropagation() {
stopped += 1;
},
});
assert.equal(prevented, 1);
assert.equal(stopped, 1);
assert.equal(first.item.classList.contains("is-expanded"), false);
assert.equal(second.item.classList.contains("is-expanded"), true);
});
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,336 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
const readStylesheet = (path) =>
readFile(new URL(`../src/media/${path}`, import.meta.url), "utf8");
const [navigationStyles, layoutStyles, overlayStyles] = await Promise.all([
readStylesheet("components/_nav.css"),
readStylesheet("_layout.css"),
readStylesheet("components/_overlay.css"),
]);
const getBlock = (source, selector) => {
const selectorIndex = source.indexOf(selector);
assert.notEqual(selectorIndex, -1, `Missing selector: ${selector}`);
const blockStart = source.indexOf("{", selectorIndex);
assert.notEqual(blockStart, -1, `Missing block for selector: ${selector}`);
let depth = 1;
for (let index = blockStart + 1; index < source.length; index += 1) {
if (source[index] === "{") depth += 1;
if (source[index] === "}") depth -= 1;
if (depth === 0) return source.slice(blockStart + 1, index);
}
assert.fail(`Unclosed block for selector: ${selector}`);
};
const assertIncludesUtilities = (block, utilities) => {
for (const utility of utilities) {
assert.match(
block,
new RegExp(
`(^|\\s)${utility.replaceAll("[", "\\[").replaceAll("]", "\\]")}($|\\s|;)`,
),
);
}
};
test("shared navigation styles define active and expanded states", () => {
const direct = getBlock(navigationStyles, ".navigation-direct");
const directActive = getBlock(
navigationStyles,
".navigation-direct.is-active-page",
);
const expandedToggle = getBlock(
navigationStyles,
".navigation-group.is-expanded > .navigation-group-toggle",
);
const activeGroupToggle = getBlock(
navigationStyles,
".navigation-group.is-active-group > .navigation-group-toggle",
);
const activeSublink = getBlock(
navigationStyles,
".navigation-sublink.is-active-page",
);
const sublink = getBlock(navigationStyles, ".navigation-sublink");
assertIncludesUtilities(direct, ["text-text", "hover:text-text"]);
// Active page is a filled brand pill on both direct links and sublinks.
assertIncludesUtilities(directActive, [
"text-brand",
"hover:text-brand",
"bg-brand-subtle",
]);
// Weight is the surface's call — the two run different scales.
assert.doesNotMatch(directActive, /font-(?:normal|medium|semibold|bold)/);
// An expanded group's label turns brand and rotates its arrow open.
assertIncludesUtilities(expandedToggle, ["text-brand"]);
assertIncludesUtilities(getBlock(expandedToggle, "&::after"), ["rotate-90"]);
// The active group keeps a brand label even when manually collapsed, so the
// current section stays marked while its pill is hidden — but it must not
// rotate the arrow open in that collapsed state.
assertIncludesUtilities(activeGroupToggle, ["text-brand"]);
assert.doesNotMatch(activeGroupToggle, /rotate-90/);
// The pill shape lives with the pill fill in the shared recipe, so the
// hover/active background is rounded the same way on desktop and mobile.
assertIncludesUtilities(sublink, [
"font-medium",
"hover:bg-hover-faint",
"rounded-xl",
]);
assertIncludesUtilities(activeSublink, [
"text-brand",
"hover:text-brand",
"font-semibold",
"bg-brand-subtle",
]);
// The left accent bar is gone — no ::before rail on the active sublink.
assert.doesNotMatch(activeSublink, /before/);
});
test("one first-level row recipe serves both kinds on both surfaces", () => {
const row = getBlock(navigationStyles, ".nav-row");
// Defined once, so a group toggle and a direct destination cannot come
// out with different frames (#97).
assertIncludesUtilities(row, [
"flex",
"w-full",
"items-center",
"appearance-none",
"border-0",
"bg-transparent",
"shadow-none",
"no-underline",
]);
// Mode files consume it; no per-kind or per-surface box class survives.
assert.match(layoutStyles, /& \.sidebar-list \.nav-row \{/);
assert.match(overlayStyles, /& \.nav-row \{/);
assert.doesNotMatch(overlayStyles, /mobile-nav-link/);
assert.doesNotMatch(navigationStyles, /nav-category \{/);
// :where() keeps the resting treatment at (0,1,0), below every state.
const sidebarResting = getBlock(navigationStyles, ":where(.sidebar-list)");
assertIncludesUtilities(getBlock(sidebarResting, "& .nav-row"), [
"text-text-muted",
"hover:text-text",
]);
assertIncludesUtilities(getBlock(sidebarResting, "& .navigation-direct"), [
"hover:bg-hover-faint",
]);
});
test("the drawer collapses a group to nothing so the row rhythm is declared", () => {
const drawer = getBlock(overlayStyles, ".mobile-menu-overlay");
const list = getBlock(drawer, "& .mobile-nav-list");
const submenu = getBlock(drawer, "& .mobile-nav-submenu-list");
// The rhythm is the list's own gap, not a collapsed group's leftover box.
assertIncludesUtilities(list, ["max-md:gap-y-4"]);
// Full-bleed square rows must keep suppressing the shared selected fill.
assertIncludesUtilities(getBlock(drawer, "& .nav-row"), [
"max-md:bg-transparent",
]);
// Direct child of the 0fr track: its own box outlives the collapse.
assertIncludesUtilities(submenu, ["max-md:m-0", "max-md:p-0"]);
assert.doesNotMatch(submenu, /max-md:(?:mb-|py-|mt-|mx-|pt-|pb-)/);
});
test("shared navigation styles own accordion animation without a guide rail", () => {
const toggle = getBlock(navigationStyles, ".navigation-group-toggle");
const region = getBlock(navigationStyles, ".navigation-group-region");
const expandedRegion = getBlock(
navigationStyles,
".navigation-group.is-expanded > .navigation-group-region",
);
const submenu = getBlock(navigationStyles, ".navigation-submenu-list");
assertIncludesUtilities(getBlock(toggle, "&::after"), [
"transition-[transform,opacity]",
"duration-[250ms]",
]);
assert.match(toggle, /var\(--icon-arrow-right\)/);
assertIncludesUtilities(region, [
"grid",
"grid-rows-[0fr]",
"opacity-0",
"transition-[grid-template-rows,opacity]",
"duration-[250ms]",
]);
assertIncludesUtilities(expandedRegion, ["grid-rows-[1fr]", "opacity-100"]);
// The vertical guide rail is removed: the submenu list carries no ::before
// hairline anymore.
assert.doesNotMatch(submenu, /before/);
});
test("desktop sidebar styles only provide desktop navigation density", () => {
const sidebar = getBlock(layoutStyles, 'body[data-nav-type="sidebar"]');
const row = getBlock(sidebar, "& .sidebar-list .nav-row");
const icon = getBlock(sidebar, "& .sidebar-list .nav-icon");
const submenu = getBlock(sidebar, "& .sidebar-submenu");
const sublink = getBlock(sidebar, "& .sidebar-submenu .navigation-sublink");
// Density for both first-level kinds at once.
assertIncludesUtilities(row, [
"gap-2",
"px-3",
"py-2",
"text-lg",
"font-semibold",
"tracking-wide",
]);
// The frame is the shared recipe's; the mode file must not restate it.
assert.doesNotMatch(row, /(^|\s)(?:flex|items-center|w-full)($|\s|;)/);
// Nor colour: nesting under body[data-nav-type] would outrank the states.
assert.doesNotMatch(row, /text-text|text-brand|bg-/);
assertIncludesUtilities(icon, ["size-4.5"]);
// pl-6.5 hangs sublink text off the parent label (px-3 + icon + gap −
// the sublink's own px-3), not the row edge.
assertIncludesUtilities(submenu, ["pl-6.5"]);
assertIncludesUtilities(sublink, ["px-3", "py-1.5", "text-sm"]);
assert.doesNotMatch(
sidebar,
/sidebar-section|sidebar-group-open|nav-link-active|has-active/,
);
assert.doesNotMatch(sidebar, /bg-brand-subtle/);
});
test("desktop sidebar toggles move the column and the panel as one", () => {
const sidebar = getBlock(layoutStyles, 'body[data-nav-type="sidebar"]');
const easing = "250ms_var(--ease-in-out)";
// The column width and the panel's translate share one curve and length,
// so the content edge stays on the sidebar edge in every frame.
assert.ok(
sidebar.includes(`md:[transition:grid-template-columns_${easing}]`),
);
assert.ok(
getBlock(sidebar, "& .sidebar-panel-inner {").includes(
`[transition:translate_${easing}]`,
),
);
// Hidden only after the slide-out, shown at once on the way in.
assertIncludesUtilities(
getBlock(sidebar, "&.sidebar-collapsed .sidebar-panel {"),
["invisible", "[transition:visibility_0s_250ms]"],
);
assert.doesNotMatch(layoutStyles, /sidebar-anim|sidebar-run-/);
});
test("mobile drawer styles only provide mobile navigation density", () => {
const drawer = getBlock(overlayStyles, ".mobile-menu-overlay");
const icon = getBlock(drawer, "& .nav-icon");
const submenu = getBlock(drawer, "& .mobile-nav-submenu-list");
const sublink = getBlock(drawer, "& .mobile-nav-sublink");
// Row icons at drawer scale, against the text-2xl labels.
assertIncludesUtilities(icon, ["max-md:size-6"]);
// pl-6 hangs sublink text off the parent label (rows are px-0: icon +
// gap − the sublink's own px-3).
assertIncludesUtilities(submenu, ["max-md:pl-6"]);
assertIncludesUtilities(sublink, [
"max-md:min-h-10",
"max-md:px-3",
"max-md:py-2",
"max-md:text-base",
]);
assert.doesNotMatch(sublink, /max-md:font-(?:normal|medium|semibold|bold)/);
assert.doesNotMatch(
drawer,
/has-submenu|submenu-expanded|nav-link-active|has-active/,
);
assert.doesNotMatch(drawer, /bg-brand-subtle/);
});
test("mega-menu panels scroll within the viewport", () => {
const megaMenu = getBlock(layoutStyles, '[data-nav-type="mega-menu"] &');
const panel = getBlock(megaMenu, "& .desktop-nav");
assert.ok(panel.includes("max-h-[calc(100dvh-3.5rem)]"));
assertIncludesUtilities(panel, ["overflow-y-auto", "overscroll-contain"]);
});
test("mega-menu reveal and retract share the page-top origin", () => {
const megaMenu = getBlock(layoutStyles, '[data-nav-type="mega-menu"] &');
const headerContent = getBlock(layoutStyles, "& .header-content");
const container = getBlock(megaMenu, "& .desktop-menu-container");
const sheet = getBlock(container, "& .desktop-menu-sheet");
const canvas = getBlock(container, "& .desktop-menu-canvas");
const panel = getBlock(megaMenu, "& .desktop-nav");
const headerLift = layoutStyles.match(
/The bar must sit above[\s\S]*?(\[data-nav-type="mega-menu"\][\s\S]*?)\n\s*\.brand/,
)?.[1];
assertIncludesUtilities(headerContent, ["z-10"]);
assertIncludesUtilities(container, ["top-0", "z-0"]);
assert.doesNotMatch(
container,
/@apply bg-mega-menu-bg pointer-events-none absolute inset-x-0 top-0 h-14/,
);
assert.match(
container,
/&\.active,\s*&\.closing\s*\{[\s\S]*@apply[^;]*\bvisible\b/,
);
assert.match(
container,
/&\.closing\s*\{[\s\S]*@apply[^;]*\bopacity-0\b[^;]*\btransition-opacity\b[^;]*\bduration-\[220ms\]/,
);
assert.match(
container,
/&\.active\s*\{[\s\S]*@apply[^;]*pointer-events-auto/,
);
assertIncludesUtilities(sheet, ["top-0", "-translate-y-full"]);
assert.match(
sheet,
/h-\[calc\(var\(--mega-menu-height,0px\)\+3\.5rem\)\]/,
);
assertIncludesUtilities(canvas, ["translate-y-full"]);
assertIncludesUtilities(panel, ["top-14"]);
// z-70 must span the retract too (or the curtain dims the closing panel).
assert.match(
headerLift ?? "",
/desktop-menu-container:is\(\.active, \.closing\)\)\s*\{\s*@apply[^;]*z-70/,
);
// The bar no longer performs its own background transition. The flyout sheet
// owns the opened surface and retracts fully to page top before hiding.
assert.doesNotMatch(headerLift ?? "", /bg-mega-menu-bg/);
});
test("theme flips repaint the bar in the same frame as the page", () => {
const headerDeclaration = layoutStyles.match(
/^header \{\s*@apply ([^;]+);/m,
)?.[1];
assert.ok(headerDeclaration, "Missing header root declaration");
assertIncludesUtilities(headerDeclaration, ["bg-bg", "sticky"]);
// A colour transition on the bar itself would ease its bg/text over
// --mega-menu-duration on every data-darkmode flip, lagging the bar behind
// the untransitioned page in all three nav modes. The mega-menu wipe colour
// lives on .desktop-menu-sheet, which carries its own transition.
assert.doesNotMatch(headerDeclaration, /\btransition/);
});
test("mega-menu category masks use Tailwind arbitrary utilities", () => {
const title = getBlock(layoutStyles, "& .desktop-nav-title");
const icon = getBlock(title, "&::before");
assert.match(
icon,
/@apply[^;]*\[mask:var\(--menu-icon,var\(--icon-category\)\)_center\/contain_no-repeat\]/,
);
// The default lives in the var() fallback only. Declaring --menu-icon on
// the title compiles into a (0,5,1) selector chain that outranks every
// .desktop-nav-title[data-section=…] (0,2,0) mapping in _nav.css.
assert.doesNotMatch(title, /--menu-icon:/);
assert.doesNotMatch(layoutStyles, /^\s*mask\s*:/m);
});
@@ -0,0 +1,27 @@
import { readFile } from "node:fs/promises";
import { test } from "node:test";
import assert from "node:assert/strict";
const overlayCss = await readFile(
new URL("../src/media/components/_overlay.css", import.meta.url),
"utf8",
);
const layoutCss = await readFile(
new URL("../src/media/_layout.css", import.meta.url),
"utf8",
);
test("mobile and mega navigation share the translucent menu background", () => {
const overlayRule = overlayCss.match(
/\.mobile-menu-overlay\s*\{([\s\S]*?)\n\s*\.mobile-nav\s*\{/,
)?.[1];
const navRule = overlayCss.match(
/\.mobile-nav\s*\{\s*@apply\s+([^;]+);/,
)?.[1];
assert.ok(overlayRule?.includes("max-md:bg-mega-menu-bg"));
assert.ok(!overlayRule?.includes("max-md:bg-scrim"));
assert.ok(!overlayRule?.includes("max-md:p-1"));
assert.ok(!navRule?.includes("max-md:bg-"));
assert.match(layoutCss, /\bbg-mega-menu-bg\b/);
});
@@ -0,0 +1,141 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
const read = (p) => readFileSync(new URL(p, import.meta.url), "utf8");
// ── 共享渐进层 ──────────────────────────────────────────────────────────
test("the shared page-bg partial exists and both entries import it", () => {
const partial = read("../src/media/_page-bg.css");
assert.match(partial, /\.page-bg\s*\{/);
assert.match(read("../src/media/main.css"), /@import "\.\/_page-bg\.css"/);
assert.match(read("../src/media/login.css"), /@import "\.\/_page-bg\.css"/);
});
test("login.css no longer carries its own .login-bg rules, only the token mapping", () => {
const login = read("../src/media/login.css");
assert.doesNotMatch(login, /\.login-bg/);
assert.match(login, /--page-bg:\s*var\(--login-bg\)/);
assert.match(login, /--page-bg-lqip:\s*var\(--login-bg-lqip\)/);
});
test("the partial is pure mechanism: it consumes only the --page-bg pair", () => {
const partial = read("../src/media/_page-bg.css");
// No entry-specific tokens — each consumer maps its own pair, so the
// shared sheet never drags admin tokens into login.css (or vice versa).
assert.doesNotMatch(partial, /var\(--main-bg/);
assert.doesNotMatch(partial, /var\(--login-bg/);
// LQIP paints first, the full image fades in on ::after — the login page's
// established progressive contract, now shared.
assert.match(partial, /background-image:var\(--page-bg-lqip,var\(--page-bg\)\)/);
assert.match(partial, /&\.full-loaded::after/);
});
test("sysauth.ut renders the shared layer and its loader reads --page-bg generically", () => {
const ut = readFileSync(
new URL("../../ucode/template/themes/aurora/sysauth.ut", import.meta.url),
"utf8"
);
assert.match(ut, /class="page-bg"/);
assert.doesNotMatch(ut, /login-bg/);
assert.match(ut, /getComputedStyle\(bg\)\.getPropertyValue\('--page-bg'\)/);
});
// ── header.ut 条件渲染:未配置背景时功能不存在 ─────────────────────────
test("header.ut gates every main-bg artifact on struct_main_bg", () => {
const ut = readFileSync(
new URL("../../ucode/template/themes/aurora/header.ut", import.meta.url),
"utf8"
);
// 共享 preload 助手:login/main 两处调用,data: URI 不 preload
assert.match(ut, /function bg_preload_url/);
assert.match(ut, /bg_preload_url\(tokens\.struct_login_bg\)/);
assert.match(ut, /bg_preload_url\(tokens\.struct_main_bg\)/);
// body 的 data-bg 属性与其守卫在同一行模板上:admin 看 struct_main_bg,
// 登录页看 struct_login_bg(登录三参数需要这个开关)
assert.match(ut, /blank_page \? tokens\.struct_login_bg : tokens\.struct_main_bg[^\n]*data-bg/);
assert.match(ut, /class="page-bg"/);
// admin loader 与 sysauth 同款:读元素自身的 --page-bg
assert.match(ut, /getComputedStyle\(bg\)\.getPropertyValue\('--page-bg'\)/);
});
// ── D 方案:frost 只许上顶栏+侧栏,卡片禁 blur,默认值只在 fallback ────
test("scheme D: frost is confined to the two chrome surfaces", () => {
const mode = read("../src/media/_page-bg-mode.css");
assert.match(mode, /body\[data-bg\]/);
// backdrop-filter 恰好出现一次:header + sidebar 合用的那条规则
assert.equal(mode.match(/backdrop-filter/g).length, 1);
// 卡片规则存在且派生自 --surface(color-mix,无 blur)
assert.match(mode, /\.cbi-section[^{]*\{[^}]*color-mix\(in_srgb,var\(--surface\)/);
// 遮罩叠在大图层之上
assert.match(mode, /page-bg::before/);
assert.match(mode, /z-1/);
// admin 的 token 映射也归 mode 文件:login.css 永不掺入 --main-bg
assert.match(mode, /--page-bg:\s*var\(--main-bg\)/);
assert.match(mode, /--page-bg-lqip:\s*var\(--main-bg-lqip\)/);
});
test("scheme D: the three tunables default only in var() fallbacks", () => {
const mode = read("../src/media/_page-bg-mode.css");
assert.match(mode, /var\(--main-bg-alpha,67%\)/);
assert.match(mode, /var\(--main-bg-blur,20px\)/);
assert.match(mode, /var\(--main-bg-scrim,20%\)/);
// body 上不得声明默认(UCI 注入在 :root,body 层声明会遮蔽)
assert.doesNotMatch(mode, /--main-bg-alpha:\s*67%/);
assert.match(read("../src/media/main.css"), /@import "\.\/_page-bg-mode\.css"/);
assert.doesNotMatch(read("../src/media/login.css"), /_page-bg-mode/);
});
test("reduced transparency restores opaque surfaces under a custom background", () => {
const rt = read("../src/media/_reduced-transparency.css");
assert.match(rt, /body\[data-bg\][^}]*--main-bg-alpha:\s*100%/s);
});
// 固定定位的 .page-bg 按 CSS 绘制顺序画在非 positioned 普通流内容之上——
// 登录页靠 .login-screen 的 relative 浮起,admin 侧必须同样把 #maincontent
// 提为 relative。负 z-index 不是出路:body 自身不透明背景会把层盖掉。
test("admin content is lifted above the fixed background layer", () => {
const mode = read("../src/media/_page-bg-mode.css");
assert.match(mode, /& #maincontent\s*\{[^}]*relative/);
});
// 内容画布:#maincontent 铺一层派生自 --bg 的半透明底,给任意插件 DOM 一个
// 可读性地板——兼容性不靠认识每个组件,靠这一条通用规则。55% 是定死的
// 内部常量,刻意不做成配置项。
test("the content canvas floors readability for arbitrary plugin DOM", () => {
const mode = read("../src/media/_page-bg-mode.css");
assert.match(mode, /& #maincontent\s*\{[^}]*color-mix\(in_srgb,var\(--bg\)_55%,transparent\)/);
});
// 登录背景的三参数:卡片半透明+磨砂、全屏遮罩。默认 100%/0px/0% 逐像素
// 复刻无参数时的现状——老用户零变化,磨砂玻璃卡是拉了滑杆才有的选择。
test("login tunables default to today's exact look", () => {
const login = read("../src/media/login.css");
assert.match(login, /var\(--login-bg-alpha,100%\)/);
assert.match(login, /var\(--login-bg-blur,0px\)/);
assert.match(login, /var\(--login-bg-scrim,0%\)/);
assert.match(login, /body\[data-bg\] \.login-card/);
// 无障碍:减少透明度偏好下登录卡回不透明——必须写在 login.css 自己身上
// (它不引入 _reduced-transparency.css,否则会拖进 admin 专属 token)
assert.match(login, /prefers-reduced-transparency[^}]*\{[^]*?--login-bg-alpha:\s*100%/);
// 登录页设了背景时 body 也要打 data-bg(模板守卫)
const ut = readFileSync(
new URL("../../ucode/template/themes/aurora/header.ut", import.meta.url),
"utf8"
);
assert.match(ut, /blank_page && tokens\.struct_login_bg[^\n]*data-bg|\(blank_page \? tokens\.struct_login_bg : tokens\.struct_main_bg\)[^\n]*data-bg/);
});
// studio 背景组件的版式曾走页面补丁通道(admin-system-aurora),等于把
// luci-app-aurora-config 自己的组件绑死在本主题上:装到 shadcn 下补丁不加载,
// .bg-preview 丢掉 position/height/overflow,内部八层 absolute 逃出去糊满全页。
// 版式已随 app 走(studio.js 的 ensureBgCardStyles 注入),这里只守住"别回来"。
test("the studio background component's layout is the app's, not this theme's", () => {
const src = new URL("../src/media/patches/admin-system-aurora.css", import.meta.url);
const built = new URL(
"../../htdocs/luci-static/aurora/patches/admin-system-aurora.css",
import.meta.url
);
assert.ok(!existsSync(src), "the retired patch source must stay deleted");
assert.ok(!existsSync(built), "and so must its build artifact");
});
@@ -0,0 +1,37 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";
const root = resolve(import.meta.dirname, "../..");
const css = readFileSync(resolve(root, "htdocs/luci-static/aurora/main.css"), "utf8");
const header = readFileSync(resolve(root, "ucode/template/themes/aurora/header.ut"), "utf8");
test("only the typed theme toggle captures the page in a view transition", () => {
assert.match(header, /ViewTransitionTypeSet \? \{ update: apply, types: \['theme'\] \} : apply/);
// Standalone rule: merged into a selector list, an engine without
// :active-view-transition-type() would drop its neighbours too.
assert.match(css, /\}:root:not\(:active-view-transition-type\(theme\)\)\{view-transition-name:none\}/);
});
test("late content fades in without a filling animation", () => {
const rule = css.match(/#view>:not\(\.spinning\)[^{]*\{([^}]*)\}/);
assert.ok(rule, "missing page-entry fade rule");
assert.match(rule[1], /\bbackwards\b/);
assert.doesNotMatch(rule[1], /\bboth\b/);
});
test("a collapsed sidebar is restored before anything resolves body's style", () => {
// The sidebar column transitions; a class landing after a style flush
// (the page-bg script's getComputedStyle) would ease it shut on load.
const restore = header.indexOf("aurora.sidebarCollapsed");
assert.ok(restore > header.indexOf("<body"), "restore runs inside body");
assert.ok(
restore < header.indexOf("getComputedStyle"),
"restore precedes the page-bg style read",
);
assert.ok(
restore < header.indexOf("<header>"),
"restore precedes the header markup",
);
});
@@ -0,0 +1,74 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveMode } from "@eamonxg/luci-theme-tokens/aurora";
test("resolveMode(light) returns flat oklch for all tokens", () => {
const out = resolveMode("light");
// 10 inputs + 19 derived = 29 color tokens.
for (const k of [
"bg",
"surface",
"text",
"brand",
"text_muted",
"surface_sunken",
"hairline",
"brand_subtle",
"focus_ring",
"progress_start",
"progress_end",
"info_surface",
"scrim",
"mega_menu_bg",
]) {
assert.ok(out[k], `missing ${k}`);
assert.ok(
!out[k].includes("color-mix") && !out[k].includes("var("),
`${k} not flat: ${out[k]}`,
);
}
});
test("progress_end aliases brand", () => {
const out = resolveMode("light");
assert.equal(out.progress_end, out.brand);
});
test("scrim uses one 60% alpha value in both modes", () => {
assert.equal(resolveMode("light").scrim, "oklch(0% 0 0 / 0.6)");
assert.equal(resolveMode("dark").scrim, "oklch(0% 0 0 / 0.6)");
});
test("menu panel is fully opaque (frost lives on the curtain, not the panel)", () => {
const l = resolveMode("light");
const d = resolveMode("dark");
// The panel animates height, so it carries no backdrop-filter; and it is
// fully opaque so the dimmed curtain never bleeds through to grey it off the
// header tone or leak page content. The header lifts to this same colour on
// open, so any alpha here would break the one-continuous-surface look.
assert.ok(!l.mega_menu_bg.includes("/"), `light opaque: ${l.mega_menu_bg}`);
assert.ok(!d.mega_menu_bg.includes("/"), `dark opaque: ${d.mega_menu_bg}`);
});
test("light bg is the snow-blue canvas; surface stays pure white", () => {
const light = resolveMode("light");
assert.equal(light.bg, "oklch(0.984 0.004 235)");
assert.equal(light.surface, "oklch(1 0 0)");
});
test("dark brand and progress colors match the mobile navigation design", () => {
const dark = resolveMode("dark");
assert.equal(dark.brand, "oklch(0.6 0.13 188.745)");
assert.equal(dark.on_brand, "oklch(1 0 0)");
// Derived from brand like light: end aliases brand, start mixes toward sunken.
assert.equal(dark.progress_start, "oklch(44.78% 0.0865 193.2)");
assert.equal(dark.progress_end, dark.brand);
});
test("dark differs from light", () => {
assert.notEqual(
resolveMode("light").surface_sunken,
resolveMode("dark").surface_sunken,
);
});
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";
const projectRoot = resolve(import.meta.dirname, "../..");
const source = (path) => readFileSync(resolve(projectRoot, path), "utf8");
test("navigation transitions avoid blanket and full-page layout animation", () => {
const nav = source(".dev/src/media/components/_nav.css");
const layout = source(".dev/src/media/_layout.css");
assert.doesNotMatch(nav, /\btransition-all\b/);
assert.doesNotMatch(layout, /transition-\[grid-template-columns\]/);
});
test("login effects do not reserve compositor layers permanently", () => {
const login = source(".dev/src/media/login.css");
assert.doesNotMatch(login, /\[will-change:opacity\]/);
});
test("login template reuses server data when including the header", () => {
const sysauth = source("ucode/template/themes/aurora/sysauth.ut");
const header = source("ucode/template/themes/aurora/header.ut");
assert.match(
sysauth,
/include\('header',\s*\{[^}]*prefetched_boardinfo:\s*boardinfo[^}]*prefetched_tokens:\s*themeTokens[^}]*\}\)/s,
);
assert.match(header, /prefetched_boardinfo\s*\?\?/);
assert.match(header, /prefetched_tokens\s*\?\?/);
});
@@ -0,0 +1,218 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import Color from "colorjs.io";
import { resolveMode } from "@eamonxg/luci-theme-tokens/aurora";
// [L, C, H] in oklch; alpha separate.
const lch = (s) => {
const c = new Color(s).to("oklch");
return { L: c.coords[0], C: c.coords[1], H: c.coords[2] || 0, a: c.alpha };
};
const nearHue = (s, h, tol = 8) => {
const H = lch(s).H;
const d = Math.abs(((H - h + 540) % 360) - 180);
return d <= tol;
};
const read = (p) => readFileSync(new URL(p, import.meta.url), "utf8");
test("light low-emphasis surfaces carry the Gray tint (chroma > 0)", () => {
const l = resolveMode("light");
assert.ok(lch(l.surface_sunken).C > 0.001, `sunken flat: ${l.surface_sunken}`);
assert.ok(lch(l.surface_overlay).C > 0.001, `overlay flat: ${l.surface_overlay}`);
});
test("light bg, sunken and overlay share one hue family (~235)", () => {
const l = resolveMode("light");
for (const k of ["bg", "surface_sunken", "surface_overlay"]) {
assert.ok(nearHue(l[k], 235), `${k} hue off: ${l[k]}`);
}
});
test("light sunken is darker than bg, overlay is lighter (toward white)", () => {
const l = resolveMode("light");
assert.ok(lch(l.surface_sunken).L < lch(l.bg).L, "sunken not below bg");
assert.ok(lch(l.surface_overlay).L > lch(l.bg).L, "overlay not above bg");
});
test("dark surfaces are the Gray family (~264), not zinc (286)", () => {
const d = resolveMode("dark");
for (const k of ["bg", "surface", "surface_sunken", "text_muted"]) {
assert.ok(nearHue(d[k], 264, 10), `${k} hue off: ${d[k]}`);
}
});
test("dark layering: page < card, sunken between page and card", () => {
const d = resolveMode("dark");
const bg = lch(d.bg).L, surf = lch(d.surface).L, sunk = lch(d.surface_sunken).L;
assert.ok(bg < surf, "page not darker than card");
assert.ok(sunk >= bg && sunk < surf, `sunken not between page and card: ${d.surface_sunken}`);
});
test("mega-menu reveal is compositor-only and the frost never overlaps it", () => {
const layout = read("../src/media/_layout.css");
const overlay = read("../src/media/components/_overlay.css");
// The sheet is the moving surface, so it must carry NO backdrop-filter —
// blurring an animating box re-rasterises every frame (the old clip-path
// flicker) — and must animate translate, not height: a height transition
// pays main-thread layout + repaint per frame (the low-end hover jank).
const sheet =
layout.match(/& \.desktop-menu-sheet\s*\{([\s\S]*?)\n\s*\}/)?.[1] ?? "";
assert.ok(!sheet?.includes("backdrop-blur"), `sheet must not blur: ${sheet}`);
assert.ok(
!layout.includes(
"@apply bg-mega-menu-bg pointer-events-none absolute inset-x-0 top-0 h-14",
),
"header band must not be static",
);
// Apple-style geometry: the sheet is the whole opened surface, including the
// header-height band. Its closed bottom edge is page top, matching the
// reveal origin.
assert.ok(
sheet?.includes("transition-[translate]") &&
sheet?.includes("top-0") &&
sheet?.includes("h-[calc(var(--mega-menu-height,0px)+3.5rem)]") &&
sheet?.includes("-translate-y-full"),
`sheet must move from and to page top: ${sheet}`,
);
const containerRule =
layout.match(/& \.desktop-menu-container\s*\{\s*@apply\s+([^;]+);/)?.[1] ??
"";
assert.ok(
!containerRule.includes("transition") &&
!containerRule.includes("will-change"),
`container is a static frame, it must not animate: ${containerRule}`,
);
// A translated sheet moves inside a static clip frame rooted at the page top.
// The container adds shadow slack below the travel but does not move itself.
assert.ok(
containerRule.includes("top-0") &&
containerRule.includes("h-[calc(var(--mega-menu-height,0px)+3.5rem+3rem)]") &&
containerRule.includes("overflow-clip"),
`container must clip the page-top retract: ${containerRule}`,
);
assert.match(
layout,
/&\.closing\s*\{[\s\S]*?@apply[^;]*opacity-0[^;]*transition-opacity[^;]*duration-\[220ms\]/,
);
// The counter-transformed canvas must mirror the sheet's endpoints and
// timing exactly or the content drifts during the wipe; both take the
// distance-adaptive duration from the same variable.
const canvasRule =
layout.match(/& \.desktop-menu-canvas\s*\{([\s\S]*?)\n\s*\}/)?.[1] ?? "";
for (const token of [
"transition-[translate]",
"duration-(--mega-menu-duration,300ms)",
"translate-y-full",
]) {
assert.ok(canvasRule.includes(token), `canvas missing ${token}: ${canvasRule}`);
}
assert.ok(
sheet?.includes("duration-(--mega-menu-duration,300ms)"),
`sheet must share the adaptive duration: ${sheet}`,
);
// The curtain only dims: a blurred full-screen fade dropped frames on weak
// GPUs. Opacity fades out at once; visibility waits for it without tweening.
assert.ok(
!overlay.includes("max-md:backdrop-blur-lg"),
"opaque mobile overlay must not blur hidden pixels",
);
const curtain = overlay
.split("\n")
.find((l) => l.includes("bg-mega-menu-scrim"));
assert.ok(
!curtain?.includes("backdrop-"),
`desktop curtain must not blur: ${curtain}`,
);
assert.ok(
curtain?.includes("[transition:opacity_220ms_") &&
curtain.includes(",visibility_0s_220ms]") &&
!curtain.includes("delay-"),
`desktop curtain: immediate opacity exit, delayed visibility: ${curtain}`,
);
assert.ok(
/&\.active\s*\{\s*@apply[^;]*\[transition-delay:0s\]/.test(overlay),
"desktop curtain must show without delay",
);
assert.ok(!overlay.includes(".settled"), "settle-gated frost must stay gone");
});
test("maincontent cards use a hairline border, not heavy shadow", () => {
const card = read("../src/media/components/_card.css");
const rule = card.match(/#maincontent &\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
assert.ok(rule.includes("border-hairline"), `no hairline: ${rule}`);
assert.ok(rule.includes("border"), `no border: ${rule}`);
assert.ok(!rule.includes("shadow-lg"), `still shadow-lg: ${rule}`);
});
test("main view does not create an animation stacking context", () => {
const layout = read("../src/media/_layout.css");
const rule = layout.match(/#view\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
for (const utility of [
"animate-in",
"fade-in-0",
"slide-in-from-top-2",
"fill-mode-backwards",
"fill-mode-both",
]) {
assert.ok(!rule.includes(utility), `view still uses ${utility}: ${rule}`);
}
});
test("content dropdowns stay above the closed header and below the open mega-menu", () => {
const layer = (value) => ["z", value].join("-");
const layout = read("../src/media/_layout.css");
const dropdown = read("../src/media/components/_dropdown.css");
const message = read("../src/media/components/_message.css");
const overlay = read("../src/media/components/_overlay.css");
const headerRule = layout.match(/^header\s*\{\s*@apply\s+([^;]+);/m)?.[1] ?? "";
const activeHeaderRule =
layout.match(
/\[data-nav-type="mega-menu"\]\s*&:has\([\s\S]*?\.desktop-menu-container[\s\S]*?\.active[\s\S]*?\)\s*\{\s*@apply\s+([^;]+);/,
)?.[1] ?? "";
const dropdownRule = dropdown.match(/&\.dropdown\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
const messageRule =
message.match(/\.alert-message\s*\{[\s\S]*?@apply\s+([^;]+);/)?.[1] ?? "";
const overlayRule =
overlay.match(/& \.desktop-menu-overlay\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
// The alert banner stays in flow (d747e5f): ui.addNotification() inserts
// banners as siblings, so a sticky one would stack on top of the next and
// pin opaque surface over scroll targets. In flow it needs no layer at all.
assert.ok(messageRule, "alert-message rule not found");
assert.ok(
!/\b(sticky|fixed)\b/.test(messageRule) && !/\bz-/.test(messageRule),
`alert banner must stay in flow with no layer: ${messageRule}`,
);
assert.ok(headerRule.includes(layer(40)), `closed header layer changed: ${headerRule}`);
assert.ok(dropdownRule.includes(layer(50)), `dropdown layer changed: ${dropdownRule}`);
assert.ok(overlayRule.includes(layer(60)), `menu overlay layer changed: ${overlayRule}`);
assert.ok(
activeHeaderRule.includes(layer(70)),
`open mega-menu layer changed: ${activeHeaderRule}`,
);
});
test("tables get a hairline frame + sunken header, body stays unclipped", () => {
const t = read("../src/media/components/_table.css");
const root = t.match(/table\.table,\s*\.table\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
assert.ok(root.includes("border-hairline") && root.includes("border"), `no frame: ${root}`);
assert.ok(root.includes("rounded-2xl"), `no radius: ${root}`);
// Dropdowns inside cells must not be clipped: no global overflow-hidden on the table.
assert.ok(!root.includes("overflow-hidden"), `body clipped: ${root}`);
assert.ok(root.includes("overflow-visible"), `not overflow-visible: ${root}`);
// Header carries the sunken surface.
const thRules = [...t.matchAll(/&\s*th,\s*&\s*\.th\s*\{\s*@apply\s+([^;]+);/g)].map((m) => m[1]);
const th = thRules.find((rule) => rule.includes("bg-surface-sunken")) ?? "";
assert.ok(th, `header not sunken: ${thRules.join(" | ")}`);
// Top corners round only on the table's first row; a second header row
// (e.g. .cbi-section-table-descr) must stay square.
assert.ok(!th.includes("rounded-t"), `generic header cells carry corner radius: ${th}`);
const firstRow =
t.match(/&:first-child\s*\{\s*&\s*th,\s*&\s*\.th\s*\{\s*@apply\s+([^;]+);/)?.[1] ?? "";
assert.ok(
firstRow.includes("first:rounded-tl-2xl") && firstRow.includes("last:rounded-tr-2xl"),
`first-row corner radius missing: ${firstRow}`,
);
});