116 lines
5.4 KiB
JavaScript
116 lines
5.4 KiB
JavaScript
|
|
// 运行: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"]);
|
||
|
|
});
|