mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
The in-app updater only follows stable releases and its manifest notes just say "Release vX", so users jumping between stable versions never saw what the versions in between changed. The full release notes are too long for a popup. - Each version gets src/whats-new/<version>.json: up to 4 one-line items (new / fix / improve) in zh, zh-TW, en and ja, bundled into the app at build time. "items": [] marks a version that should not pop up. - On the first launch after an update, a dialog lists every non-empty version between the last one seen and the current one, newest first. Older versions beyond the first three are collapsed. Each version links to its page in the website changelog. - The last seen version is stored per device in settings.json (whatsNewSeenVersion) and only moves forward, so a downgrade and re-upgrade does not repeat anything. Without a record, only the current version is shown. When there is nothing to show, the version is recorded silently so the next update has a correct starting point. - The dialog waits for the welcome and new-layout dialogs. Confirming either of them also records the current version, so a 3.x user does not get two dialogs in a row. - Settings > About gets a "Recent updates" button to open the summaries again. The 4.0.0 summary is included so that list starts from 4.0; no update popup can reach it, since only 4.0.2 and later record a version. - The release workflow first requires the tagged version's summary file and validates every bundled summary with the unit test, since a malformed item would crash the dialog and put the whole app on the error screen.
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
WHATS_NEW_ITEM_TYPES,
|
|
WHATS_NEW_LANGUAGES,
|
|
type WhatsNewLanguage,
|
|
} from "@/lib/whatsNew";
|
|
|
|
// 规则见 src/whats-new/README.md:弹窗只放几句话,详细内容去官网看
|
|
const MAX_ITEMS = 4;
|
|
const MAX_LENGTH: Record<WhatsNewLanguage, number> = {
|
|
zh: 40,
|
|
"zh-TW": 40,
|
|
ja: 50,
|
|
en: 100,
|
|
};
|
|
|
|
const files = import.meta.glob<unknown>("../../src/whats-new/*.json", {
|
|
eager: true,
|
|
import: "default",
|
|
});
|
|
|
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
|
|
// 弹窗渲染时直接读 item.type、item[语言],结构不对会让整个应用落进错误页,
|
|
// 所以这里对任意 JSON 都只报问题、不抛错。发版前 release.yml 也跑这份检查。
|
|
function problemsOf(path: string, data: unknown): string[] {
|
|
const name = path.split("/").pop()!;
|
|
const problems: string[] = [];
|
|
|
|
if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?\.json$/.test(name)) {
|
|
problems.push(`${name}: file name must be <version>.json`);
|
|
}
|
|
if (!isObject(data)) {
|
|
return [...problems, `${name}: must be a JSON object`];
|
|
}
|
|
const entry = data;
|
|
if (`${String(entry.version)}.json` !== name) {
|
|
problems.push(`${name}: "version" must match the file name`);
|
|
}
|
|
if (!Array.isArray(entry.items)) {
|
|
return [...problems, `${name}: "items" must be an array`];
|
|
}
|
|
if (entry.items.length > MAX_ITEMS) {
|
|
problems.push(`${name}: at most ${MAX_ITEMS} items`);
|
|
}
|
|
entry.items.forEach((item: unknown, index) => {
|
|
const where = `${name} items[${index}]`;
|
|
if (!isObject(item)) {
|
|
problems.push(`${where}: must be an object`);
|
|
return;
|
|
}
|
|
if (!(WHATS_NEW_ITEM_TYPES as readonly unknown[]).includes(item.type)) {
|
|
problems.push(`${where}: unknown type ${String(item.type)}`);
|
|
}
|
|
for (const language of WHATS_NEW_LANGUAGES) {
|
|
const text = item[language];
|
|
if (typeof text !== "string" || text.trim() === "") {
|
|
problems.push(`${where}: missing ${language}`);
|
|
} else if ([...text].length > MAX_LENGTH[language]) {
|
|
problems.push(
|
|
`${where}: ${language} is longer than ${MAX_LENGTH[language]} characters`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
return problems;
|
|
}
|
|
|
|
describe("what's new entries", () => {
|
|
it("follow the format and stay short", () => {
|
|
const problems = Object.entries(files).flatMap(([path, data]) =>
|
|
problemsOf(path, data),
|
|
);
|
|
expect(problems).toEqual([]);
|
|
});
|
|
|
|
it("the checker catches broken entries", () => {
|
|
expect(
|
|
problemsOf("x/4.0.2.json", {
|
|
version: "4.0.3",
|
|
items: [{ type: "news", zh: "", en: "x".repeat(101) }],
|
|
}),
|
|
).toEqual([
|
|
'4.0.2.json: "version" must match the file name',
|
|
"4.0.2.json items[0]: unknown type news",
|
|
"4.0.2.json items[0]: missing zh",
|
|
"4.0.2.json items[0]: missing zh-TW",
|
|
"4.0.2.json items[0]: en is longer than 100 characters",
|
|
"4.0.2.json items[0]: missing ja",
|
|
]);
|
|
});
|
|
|
|
it("reports malformed JSON shapes instead of throwing", () => {
|
|
expect(
|
|
problemsOf("x/4.0.2.json", { version: "4.0.2", items: [null] }),
|
|
).toEqual(["4.0.2.json items[0]: must be an object"]);
|
|
expect(problemsOf("x/4.0.2.json", null)).toEqual([
|
|
"4.0.2.json: must be a JSON object",
|
|
]);
|
|
});
|
|
});
|