Files
controller-v2/scripts/festival-regression.test.mjs
T

116 lines
5.4 KiB
JavaScript
Raw Normal View History

2026-10-09 13:56:41 +08:00
// 运行:node --test scripts/festival-regression.test.mjs;无需硬件。
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { build } from "esbuild";
const require = createRequire(import.meta.url);
// pnpm 中以渲染器的解析路径统一 React 实例,避免测试环境的重复副本。
const react = createRequire(require.resolve("react-dom/server"))("react");
const { createElement } = react;
const { renderToStaticMarkup } = require("react-dom/server");
const src = fileURLToPath(new URL("../client/src", import.meta.url));
const base = "/x8/v2/modern/";
async function loadModule(relative) {
const result = await build({
entryPoints: [`${src}/${relative}`],
bundle: true,
write: false,
format: "cjs",
platform: "node",
jsx: "automatic",
external: ["react", "react/*"],
loader: { ".css": "empty" },
alias: { "@": src },
define: { "import.meta.env.BASE_URL": JSON.stringify(base) },
});
const module = { exports: {} };
const moduleRequire = id => id === "react" ? react : require(id);
new Function("require", "module", "exports", result.outputFiles[0].text)(moduleRequire, module, module.exports);
return module.exports;
}
const { getFestival, getNewYearDisplayYear, FESTIVAL_GREETINGS } = await loadModule("lib/festival.ts");
const { NewYearVolumeThumb, NewYearVolumeIcon, NewYearCardDecoration } = await loadModule("components/festival/NewYearDecoration.tsx");
const { default: FestivalGreeting } = await loadModule("components/festival/FestivalGreeting.tsx");
const date = (year, month, day, hour = 12) => new Date(year, month - 1, day, hour);
for (const [year, month, day, expected] of [
[2026, 12, 19, null], [2026, 12, 20, "christmas"],
[2026, 12, 26, "christmas"], [2026, 12, 27, "newyear"],
[2026, 12, 31, "newyear"], [2027, 1, 1, "newyear"],
[2027, 1, 3, "newyear"], [2027, 1, 4, null],
[2026, 10, 25, null], [2026, 10, 26, "halloween"],
[2026, 11, 1, "halloween"], [2026, 11, 2, null],
[2027, 12, 27, "newyear"], [2028, 1, 3, "newyear"],
]) {
test(`日期窗口 ${year}-${month}-${day} → ${expected}`, () => {
for (const hour of [0, 12, 23]) {
assert.equal(getFestival(date(year, month, day, hour), ""), expected);
}
});
}
test("URL 强制开关与未知值回退不改变原节日行为", () => {
for (const festival of ["newyear", "christmas", "halloween"]) {
assert.equal(getFestival(date(2026, 6, 1), `?festival=${festival}`), festival);
}
for (const off of ["off", "none"]) {
assert.equal(getFestival(date(2026, 12, 31), `?festival=${off}`), null);
}
for (const unknown of ["", "invalid", "toString", "__proto__"]) {
assert.equal(getFestival(date(2026, 12, 31), `?festival=${unknown}`), "newyear");
}
assert.equal(getFestival(date(2026, 12, 25), "?festival=newyear"), "newyear");
});
test("年份在十二月指向下一年,一月保持同一跨年年份", () => {
assert.equal(getNewYearDisplayYear(date(2026, 12, 27)), 2027);
assert.equal(getNewYearDisplayYear(date(2026, 12, 31)), 2027);
assert.equal(getNewYearDisplayYear(date(2027, 1, 1)), 2027);
assert.equal(getNewYearDisplayYear(date(2027, 6, 1)), 2027);
assert.equal(getNewYearDisplayYear(date(2027, 12, 31)), 2028);
});
test("滑块及大小音量使用独立元旦素材,无圣诞老人、铃铛或万圣节图案", () => {
const cases = [
[NewYearVolumeThumb, {}, "party-hat.svg"],
[NewYearVolumeIcon, { dim: true }, "sparkler.svg"],
[NewYearVolumeIcon, {}, "fireworks.svg"],
];
for (const [Component, props, asset] of cases) {
const html = renderToStaticMarkup(createElement(Component, props));
assert.ok(html.includes(`${base}images/festival/newyear/${asset}`));
assert.doesNotMatch(html, /christmas|halloween|santa|bell\.png/);
assert.match(html, /draggable="false"/);
}
const decoration = renderToStaticMarkup(createElement(NewYearCardDecoration));
assert.match(decoration, /aria-hidden="true"/);
});
test("三套节日横幅正常渲染,元旦包含动态年份与 JAN 1 日历", () => {
for (const festival of ["newyear", "christmas", "halloween"]) {
const html = renderToStaticMarkup(createElement(FestivalGreeting, { festival }));
assert.ok(html.includes(FESTIVAL_GREETINGS[festival]));
assert.ok(html.includes(`${base}images/festival/${festival}/banner-center.jpg`));
if (festival === "newyear") {
assert.ok(html.includes(String(getNewYearDisplayYear())));
assert.ok(html.includes("calendar.svg"));
assert.match(html, /NEW YEAR · NEW CHAPTER · MORE POSSIBILITIES/);
} else {
assert.doesNotMatch(html, /newyear-greeting-year|calendar\.svg/);
}
}
});
test("元旦全部素材及五个导航映射存在,部署子路径得到保留", () => {
const assets = ["party-hat.svg", "sparkler.svg", "fireworks.svg", "calendar.svg", "clock.svg", "confetti.svg",
"banner-left.jpg", "banner-center.jpg", "banner-right.jpg"];
for (const asset of assets) {
assert.ok(existsSync(new URL(`../client/public/images/festival/newyear/${asset}`, import.meta.url)), asset);
}
const nav = readFileSync(`${src}/components/BottomNav.tsx`, "utf8");
const names = [...nav.matchAll(/newyear: festivalIcon\("newyear", "([^"]+)"\)/g)].map(match => match[1]);
assert.deepEqual(names, ["confetti", "clock", "calendar", "fireworks", "party-hat"]);
});