"use strict"; const assert = require("assert"); const fs = require("fs"); const cdpPort = process.env.CDP_PORT || "9222"; const requestedRoute = process.env.MUMU_ROUTE || ""; const getTargets = async () => (await fetch(`http://127.0.0.1:${cdpPort}/json/list`)).json(); const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)); const latestRouteTarget = (targets, routePrefix = "pages/") => targets .filter((candidate) => candidate.type === "page" && candidate.title.startsWith(routePrefix)) .sort((left, right) => { const leftIndex = Number(left.title.match(/\[(\d+)\]$/)?.[1] || 0); const rightIndex = Number(right.title.match(/\[(\d+)\]$/)?.[1] || 0); return rightIndex - leftIndex; })[0]; const connect = async (target) => { const socket = new WebSocket(target.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; const requestId = id; pending.set(requestId, { resolve, reject }); socket.send(JSON.stringify({ id: requestId, method, params })); }); return { socket, send }; }; const run = async () => { const configuredRoutes = JSON.parse(fs.readFileSync("pages.json", "utf8")).pages.map((page) => page.path); const routes = requestedRoute ? [requestedRoute] : configuredRoutes; assert.ok(routes.every((route) => configuredRoutes.includes(route)), "requested route is not configured in pages.json"); const failures = []; let previousRoute = ""; for (const route of routes) { await sleep(previousRoute === "pages/auth/a01-entry" ? 1600 : 350); const activeTarget = latestRouteTarget(await getTargets()); if (!activeTarget) { failures.push(`${route}: no active Uni page WebView`); continue; } const active = await connect(activeTarget); try { void active.send("Runtime.evaluate", { expression: `uni.reLaunch({ url: ${JSON.stringify(`/${route}`)} })`, returnByValue: true, }).catch(() => {}); } finally { active.socket.close(); } const deadline = Date.now() + 5000; let routedTarget = null; while (!routedTarget && Date.now() < deadline) { routedTarget = latestRouteTarget(await getTargets(), `${route}[`); if (!routedTarget) await sleep(100); } if (!routedTarget) { await sleep(500); const retryTarget = latestRouteTarget(await getTargets()); if (retryTarget) { const retry = await connect(retryTarget); try { void retry.send("Runtime.evaluate", { expression: `uni.reLaunch({ url: ${JSON.stringify(`/${route}`)} })`, returnByValue: true, }).catch(() => {}); } finally { retry.socket.close(); } const retryDeadline = Date.now() + 5000; while (!routedTarget && Date.now() < retryDeadline) { routedTarget = latestRouteTarget(await getTargets(), `${route}[`); if (!routedTarget) await sleep(100); } } } if (!routedTarget) { failures.push(`${route}: native route did not open`); continue; } const routed = await connect(routedTarget); const exceptions = []; routed.socket.addEventListener("message", (event) => { const message = JSON.parse(event.data); if (message.method === "Runtime.exceptionThrown") exceptions.push(message.params.exceptionDetails.text); }); try { await routed.send("Runtime.enable"); let page = null; while (Date.now() < deadline) { const result = await routed.send("Runtime.evaluate", { expression: "JSON.stringify({ textLength: document.body?.innerText?.trim().length || 0, loading: [...document.querySelectorAll('.app-loading, .uni-loading, .loading-spinner')].some((element) => { const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) > 0 && rect.width > 0 && rect.height > 0; }) })", returnByValue: true, }); page = JSON.parse(result.result?.value || "{}"); if (page.textLength > 0 && !page.loading) break; await sleep(100); } if (!page?.textLength || page.loading) { failures.push(`${route}: page did not reach a stable rendered state`); } else if (exceptions.length) { failures.push(`${route}: ${exceptions.join("; ")}`); } else { process.stdout.write(`PASS ${route}\n`); } } finally { routed.socket.close(); } previousRoute = route; } if (failures.length) throw new Error(`MuMu native route audit failures:\n${failures.map((failure) => `- ${failure}`).join("\n")}`); process.stdout.write(`MUMU-NATIVE-ROUTE-RUNTIME-SMOKE PASS routes=${routes.length}\n`); }; run().catch((error) => { process.stderr.write(`${error.stack || error.message}\n`); process.exit(1); });