"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 }, "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 url = `${origin}/#/${route}${pageExpectations[route].query ? `?${pageExpectations[route].query}` : ""}`; await send("Page.navigate", { url }); await waitFor(send, `location.href === ${JSON.stringify(url)}`, `navigation failed: ${route}`); if (!noReload) { const previousTimeOrigin = await evaluate(send, "performance.timeOrigin"); await send("Page.reload"); await waitFor(send, `performance.timeOrigin !== ${JSON.stringify(previousTimeOrigin)}`, `reload failed: ${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); });