Files
jiapuapp/tests/all-page-route-runtime-smoke.js
T
2026-07-27 06:50:23 +08:00

189 lines
11 KiB
JavaScript

"use strict";
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const origin = process.argv[2] || "http://localhost:5173";
const cdpPort = process.env.CDP_PORT || "9222";
const genealogyId = process.env.GENEALOGY_ID || "2080557121112465409";
const feedId = process.env.FEED_ID || "2080572776100487169";
const articleId = process.env.ARTICLE_ID || "2080573188207632386";
const albumId = process.env.ALBUM_ID || "2080573946172891138";
const captureDirectory = path.join(__dirname, "..", "tmp", "all-page-audit");
const routeStart = Number(process.env.PAGE_AUDIT_START || 0);
const routeEnd = Number(process.env.PAGE_AUDIT_END || 0);
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const pageExpectations = {
"pages/auth/a01-entry": { title: "", noReload: true, skipTitle: true, allowAuthenticatedRedirect: true },
"pages/auth/a04-register": { title: "注册账号" },
"pages/auth/a05-reset-password": { title: "重设密码" },
"pages/genealogy/g01-my-genealogies": { title: "我的家谱", capture: "01-genealogies.png" },
"pages/genealogy/g03-create-genealogy": { title: "创建家谱", capture: "02-create-genealogy.png" },
"pages/genealogy/g05-genealogy-overview": { title: "家谱总览", query: `genealogyId=${genealogyId}` },
"pages/genealogy/g06-search-genealogies": { title: "搜索家谱" },
"pages/genealogy/g08-join-application": { title: "申请加入", query: `genealogyId=${genealogyId}` },
"pages/genealogy/g09-my-applications": { title: "我的申请" },
"pages/genealogy/g10-application-review": { title: "申请审核", query: `genealogyId=${genealogyId}` },
"pages/genealogy/g11-genealogy-settings": { title: "家谱设置", query: `genealogyId=${genealogyId}` },
"pages/genealogy/g12-generation-poems": { title: "字辈诗", query: `genealogyId=${genealogyId}` },
"pages/tree/t01-tree-overview": { title: "世系树", query: `genealogyId=${genealogyId}`, capture: "03-tree.png" },
"pages/tree/t03-member-profile": { title: "成员档案", query: `genealogyId=${genealogyId}&state=error` },
"pages/tree/t04-add-relative": { title: "录入首位成员", query: `genealogyId=${genealogyId}&mode=first` },
"pages/tree/t05-edit-member": { title: "编辑成员", query: `genealogyId=${genealogyId}&state=error` },
"pages/tree/t06-edit-relationship": { title: "调整排行", query: `genealogyId=${genealogyId}&mode=rank&state=error` },
"pages/tree/t07-member-directory": { title: "成员目录", query: `genealogyId=${genealogyId}` },
"pages/tree/t08-member-states": { title: "成员状态", query: `genealogyId=${genealogyId}&state=error` },
"pages/family/f01-family-feed": { title: "家族动态", query: `genealogyId=${genealogyId}`, capture: "04-feed.png" },
"pages/family/f02-publish-feed": { title: "发布动态", query: `genealogyId=${genealogyId}` },
"pages/family/f03-feed-detail": { title: "动态详情", query: `genealogyId=${genealogyId}&feedId=${feedId}` },
"pages/family/f04-article-list": { title: "谱文", query: `genealogyId=${genealogyId}` },
"pages/family/f05-article-detail": { title: "谱文详情", query: `genealogyId=${genealogyId}&articleId=${articleId}` },
"pages/family/f06-article-editor": { title: "新建谱文", query: `genealogyId=${genealogyId}` },
"pages/family/f07-album-list": { title: "家族相册", query: `genealogyId=${genealogyId}` },
"pages/family/f08-album-detail": { title: "相册详情", query: `genealogyId=${genealogyId}&albumId=${albumId}` },
"pages/family/f09-media-upload": { title: "添加照片", query: `genealogyId=${genealogyId}&albumId=${albumId}` },
"pages/family/f10-video-list": { title: "家族视频", query: `genealogyId=${genealogyId}` },
"pages/records/r01-people-list": { title: "人物录", query: `genealogyId=${genealogyId}` },
"pages/records/r02-person-detail": { title: "人物详情", query: `genealogyId=${genealogyId}&state=error` },
"pages/records/r03-gift-list": { title: "贺礼簿", query: `genealogyId=${genealogyId}`, capture: "05-gifts.png" },
"pages/records/r04-gift-editor": { title: "新建往来记录", query: `genealogyId=${genealogyId}&mode=create` },
"pages/records/r05-ritual-list": { title: "礼仪活动", query: `genealogyId=${genealogyId}` },
"pages/records/r06-ritual-detail": { title: "礼仪详情", query: `genealogyId=${genealogyId}&ceremonyId=0` },
"pages/records/r07-ritual-editor": { title: "新建礼仪活动", query: `genealogyId=${genealogyId}&mode=create` },
"pages/records/r08-growth-journal": { title: "成长记录", query: `genealogyId=${genealogyId}` },
"pages/records/r09-life-events": { title: "人生事件", query: `genealogyId=${genealogyId}` },
"pages/records/r10-memo-list": { title: "家族备忘", query: `genealogyId=${genealogyId}` },
"pages/records/r11-merit-records": { title: "功德记录", query: `genealogyId=${genealogyId}` },
"pages/notification/n01-message-center": { title: "消息中心", capture: "06-messages.png" },
"pages/notification/n02-message-detail": { title: "消息详情" },
"pages/profile/m01-profile-home": { title: "我的", capture: "07-profile.png" },
"pages/profile/m02-edit-profile": { title: "编辑资料" },
"pages/profile/m03-security-settings": { title: "账号与安全" },
"pages/profile/m04-change-password": { title: "修改密码" },
"pages/profile/m05-change-phone": { title: "换绑手机号" },
"pages/profile/m06-help-center": { title: "帮助中心" },
"pages/profile/m07-feedback": { title: "意见反馈" },
"pages/profile/m08-promotion": { title: "推广中心" },
"pages/profile/m09-vip-orders": { title: "VIP 与订单" },
"pages/profile/m10-about-settings": { title: "关于家谱" },
};
const connect = async () => {
const targets = await (await fetch(`http://127.0.0.1:${cdpPort}/json`)).json();
const page = targets.find((candidate) => candidate.type === "page" && candidate.url.startsWith(`${origin}/`));
if (!page) throw new Error(`Chrome debugging has no ${origin} project page`);
const socket = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
socket.addEventListener("open", resolve, { once: true });
socket.addEventListener("error", reject, { once: true });
});
let id = 0;
const pending = new Map();
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
const request = pending.get(message.id);
if (!request) return;
pending.delete(message.id);
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result);
});
const send = (method, params = {}) => new Promise((resolve, reject) => {
id += 1;
pending.set(id, { resolve, reject });
socket.send(JSON.stringify({ id, method, params }));
});
return { socket, send };
};
const evaluate = async (send, expression) => (await send("Runtime.evaluate", {
expression,
returnByValue: true,
awaitPromise: true,
})).result?.value;
const waitFor = async (send, expression, message) => {
for (let attempt = 0; attempt < 60; attempt += 1) {
if (await evaluate(send, expression)) return;
await wait(100);
}
throw new Error(message);
};
const open = async (send, route, noReload) => {
const expectation = pageExpectations[route];
const url = `${origin}/#/${route}${pageExpectations[route].query ? `?${pageExpectations[route].query}` : ""}`;
await send("Page.navigate", { url });
const authenticatedRedirect = `${origin}/#/pages/genealogy/g01-my-genealogies`;
const expectedLocation = pageExpectations[route].allowAuthenticatedRedirect
? `location.href === ${JSON.stringify(url)} || location.href === ${JSON.stringify(authenticatedRedirect)}`
: `location.href === ${JSON.stringify(url)}`;
await waitFor(send, expectedLocation, `navigation failed: ${route}`);
if (!expectation.skipTitle) {
await waitFor(send, `document.body?.innerText.includes(${JSON.stringify(expectation.title)})`, `title did not render: ${route}`);
}
if (!noReload) {
const previousTimeOrigin = await evaluate(send, "performance.timeOrigin");
send("Page.reload").catch(() => {});
await waitFor(send, `performance.timeOrigin !== ${JSON.stringify(previousTimeOrigin)}`, `reload failed: ${route}`);
if (!expectation.skipTitle) {
await waitFor(send, `document.body?.innerText.includes(${JSON.stringify(expectation.title)})`, `title did not render after reload: ${route}`);
}
}
await waitFor(send, "document.body && document.body.innerText.length > 0", `page did not render: ${route}`);
await wait(700);
};
const capture = async (send, filename) => {
fs.mkdirSync(captureDirectory, { recursive: true });
const image = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
fs.writeFileSync(path.join(captureDirectory, filename), Buffer.from(image.data, "base64"));
};
const run = async () => {
const allRoutes = require("../pages.json").pages.map((page) => page.path);
assert.deepStrictEqual([...allRoutes].sort(), Object.keys(pageExpectations).sort(), "route coverage drifted from pages.json");
const routes = routeEnd > routeStart ? allRoutes.slice(routeStart, routeEnd) : allRoutes;
const { socket, send } = await connect();
const results = [];
try {
await send("Page.enable");
await send("Runtime.enable");
for (const route of routes) {
const expectation = pageExpectations[route];
try {
await open(send, route, expectation.noReload);
const state = await evaluate(send, `(() => {
const text = document.body.innerText || "";
return {
titlePresent: text.includes(${JSON.stringify(expectation.title)}),
loading: Boolean(document.querySelector('.app-loading, .uni-loading, .loading-spinner')),
textLength: text.length,
textStart: text.slice(0, 120),
hash: location.hash,
};
})()`);
if (!expectation.skipTitle) assert.strictEqual(state.titlePresent, true, `${route} title missing: ${state.textStart}`);
assert.strictEqual(state.loading, false, `${route} remained in a loading state`);
if (expectation.capture) await capture(send, expectation.capture);
results.push({ route, state: "PASS" });
} catch (error) {
results.push({ route, state: "FAIL", reason: error.message });
}
}
} finally {
socket.close();
}
for (const result of results) {
process.stdout.write(`${result.state} ${result.route}${result.reason ? ` — ${result.reason}` : ""}\n`);
}
const failures = results.filter((result) => result.state === "FAIL");
if (failures.length) throw new Error(`${failures.length}/${results.length} routed pages failed to reach a stable expected state`);
process.stdout.write(`ALL-PAGE-ROUTE-RUNTIME-SMOKE PASS (${results.length} pages)\n`);
};
run().catch((error) => {
process.stderr.write(`${error.stack || error.message}\n`);
process.exit(1);
});