83 lines
2.9 KiB
JavaScript
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`);
|