Files
jiapuapp/tests/page-api-ownership-runtime-smoke.js
T
2026-07-29 18:16:08 +08:00

83 lines
2.9 KiB
JavaScript

"use strict";
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const walk = (directory) => fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) =>
entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)],
);
const apiSource = fs.readFileSync("utils/api.js", "utf8");
const apiOwners = new Set([...apiSource.matchAll(/^ async ([A-Za-z0-9_]+)\(/gm)].map((match) => match[1]));
const pageSources = new Map(
walk("pages")
.filter((file) => file.endsWith(".vue"))
.map((file) => [file.replace(/\\/g, "/"), fs.readFileSync(file, "utf8")]),
);
const routedPages = new Set(
JSON.parse(fs.readFileSync("pages.json", "utf8")).pages.map((page) => `${page.path}.vue`),
);
assert.deepStrictEqual(
[...routedPages].filter((file) => !pageSources.has(file)),
[],
"every configured route must have its Vue page source",
);
const calls = [...pageSources].flatMap(([file, source]) =>
[...source.matchAll(/appApi\.([A-Za-z0-9_]+)/g)].map((match) => ({ file, method: match[1] })),
);
assert.ok(calls.length > 0, "page API ownership scan found no calls");
assert.deepStrictEqual(
calls.filter((call) => !apiOwners.has(call.method)),
[],
"every page appApi call must have one adapter owner",
);
assert.deepStrictEqual(
[...routedPages].filter((file) => !calls.some((call) => call.file === file)),
["pages/records/r09-life-events.vue"],
"only the explicitly unavailable life-events page may have no backend call",
);
const expectedCallers = {
sendSmsCode: [
"pages/auth/a01-entry.vue",
"pages/auth/a04-register.vue",
"pages/auth/a05-reset-password.vue",
"pages/profile/m05-change-phone.vue",
],
changePhone: ["pages/profile/m05-change-phone.vue"],
getRegionChildren: [
"pages/genealogy/g03-create-genealogy.vue",
"pages/genealogy/g11-genealogy-settings.vue",
],
};
for (const [method, files] of Object.entries(expectedCallers)) {
const actual = [...new Set(calls.filter((call) => call.method === method).map((call) => call.file))].sort();
assert.deepStrictEqual(actual, files, `${method} page ownership drifted`);
}
const changePhonePage = pageSources.get("pages/profile/m05-change-phone.vue");
for (const requiredToken of [
"appApi.getCaptchaRequirement",
"appApi.sendSmsCode",
"appApi.changePhone",
"AUTH_VERIFICATION_OPERATION.PHONE_CHANGE",
"<TacVerification",
]) {
assert.ok(changePhonePage.includes(requiredToken), `M05 phone-change flow is missing: ${requiredToken}`);
}
for (const source of pageSources.values()) {
for (const retiredPath of [
"/genealogy/app/auth/sms/code",
"/genealogy/app/files/upload",
"/genealogy/app/files/reference",
"/genealogy/region/",
]) {
assert.strictEqual(source.includes(retiredPath), false, `page retains retired API path: ${retiredPath}`);
}
}
process.stdout.write(`PAGE-API-OWNERSHIP-RUNTIME-SMOKE PASS calls=${calls.length}\n`);