189 lines
11 KiB
JavaScript
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);
|
|
});
|