Files
2026-10-29 18:46:39 +08:00

177 lines
6.6 KiB
JavaScript

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);
}
});