// 静态预览验收:证明 WE 模拟面板**不依赖调试服**,且是**右边缘滑出**的。 // // 为什么要有这个测试: // ① 面板曾经只在调试服注入时才存在。用户要求 GitHub Pages 预览也能弹出它—— // 静态托管没有调试服,所以必须由 `--with-sim` 把驱动**注入 index.html**。 // ② Pages 把站点放在 `//` **子路径**下。驱动若用根绝对路径 `/scripts/…`, // 在子路径下必然 404。所以这个测试刻意把分发挂在 `/repo/` 前缀后面服务, // 而不是挂在根上——挂根上测不出这个错。 // // 用法:node tools/checks/verify-static-preview.mts [分发目录名] // 前提:已经 `pnpm build --with-sim`。 import { createServer } from "node:http"; import { createReadStream } from "node:fs"; import { stat } from "node:fs/promises"; import { extname, join, normalize, resolve } from "node:path"; import { withPage, sleep } from "../cdp.mjs"; const DIR = process.argv[2] ?? "collection-all"; const PORT = Number(process.env.STATIC_PORT ?? 8195); const ROOT = resolve("dist/releases"); /** 模拟 GitHub Pages 的 // 子路径。**不能**是空串,否则测不出根绝对路径的错。 */ const PREFIX = "/repo"; let failures = 0; const check = (ok: unknown, label: string, detail?: unknown) => { if (!ok) failures++; console.log(` ${ok ? "✓" : "✗"} ${label}${detail !== undefined ? ` → ${String(detail)}` : ""}`); }; const MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8", ".atlas": "text/plain; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".flac": "audio/flac", ".mp3": "audio/mpeg", ".ogg": "audio/ogg", }; const notFound: string[] = []; const server = createServer(async (req, res) => { const pathname = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname); if (!pathname.startsWith(`${PREFIX}/`)) { notFound.push(pathname); res.writeHead(404).end("outside prefix"); return; } let rel = pathname.slice(PREFIX.length + 1); if (rel === "" || rel.endsWith("/")) rel += "index.html"; const file = join(ROOT, normalize(rel)); if (!file.startsWith(ROOT)) { res.writeHead(403).end("escape"); return; } const info = await stat(file).catch(() => null); if (!info?.isFile()) { notFound.push(pathname); res.writeHead(404).end("not found"); return; } res.writeHead(200, { "content-type": MIME[extname(file).toLowerCase()] ?? "application/octet-stream", "content-length": info.size }); createReadStream(file).pipe(res); }); await new Promise((r) => server.listen(PORT, "127.0.0.1", () => r())); const BASE = `http://127.0.0.1:${PORT}${PREFIX}/${DIR}/`; console.log(`静态托管(模拟 Pages 子路径):${BASE}\n`); const W = 1920; const H = 1080; const EDGE_X = W - 8; // 16px 热区之内 const AWAY_X = 200; // 远离右边缘 const result = await withPage({ width: W, height: H, profile: "tools/.cache/edge-static" }, async ({ send, evaluate, consoleLines, exceptions }) => { const move = async (x, y) => { await send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "none", buttons: 0 }); await sleep(320); // > 面板 .18s 过渡 }; const open = () => evaluate(`document.getElementById("wesim").dataset.open`); await send("Page.navigate", { url: BASE }); await sleep(3500); const initial = await evaluate(`(function(){ var p = document.getElementById("wesim"); var e = document.getElementById("wesim-edge"); return { sim: !!window.__weSim, isSimulated: window.__weSim ? window.__weSim.__isSimulated : null, driverOnce: window.__weSimDriver === true, panel: !!p, edge: !!e, open: p ? p.dataset.open : null, banner: p && p.querySelector(".banner") ? p.querySelector(".banner").textContent : null, canvas: document.querySelectorAll("canvas").length, hasToggle: !!document.querySelector("#wesim .toggle") }; })()`); // ── 交互:右边缘滑出 ── await move(AWAY_X, H / 2); const closedInitially = await open(); await move(EDGE_X, H / 2); const openedByEdge = await open(); await move(AWAY_X, H / 2); const closedByLeave = await open(); await move(EDGE_X, H / 2); const reopened = await open(); // 取消收起:此刻真实指针就停在右边缘热区里。没有 armed 规则的话, // 面板滑走会立刻让边缘命中再次触发,表现为"取消关不掉"。 // (官方面板没有 ×,底部是确认/取消,所以这里点「取消」。) await evaluate(`document.querySelector("#wesim .foot button:not(.primary)").click()`); await sleep(400); const afterClose = await open(); // 离开边缘再回来 → 重新武装 await move(AWAY_X, H / 2); await move(EDGE_X, H / 2); const afterRearm = await open(); return { initial, closedInitially, openedByEdge, closedByLeave, reopened, afterClose, afterRearm, consoleLines, exceptions }; }); const r = result; console.log("=== ① 静态托管下模拟器可用(无调试服)==="); check(r.initial.sim === true, "window.__weSim 存在(驱动已随产物注入)"); check(r.initial.isSimulated === true, "window.__weSim.__isSimulated = true"); check(r.initial.driverOnce === true, "驱动幂等标记已置位"); check(r.initial.panel === true, "#wesim 面板已挂载"); check(r.initial.edge === true, "#wesim-edge 右边缘热区已挂载"); check(r.initial.hasToggle === false, "没有常驻开关按钮(打开方式只有右边缘)"); check(/模拟环境/.test(r.initial.banner ?? ""), "面板有「模拟环境」横幅", r.initial.banner?.trim()); check(r.initial.canvas > 0, "壁纸本身也渲染了", `canvas=${r.initial.canvas}`); console.log("\n=== ② 右边缘滑出交互 ==="); check(r.closedInitially === "0", "初始收起", `data-open=${r.closedInitially}`); check(r.openedByEdge === "1", "指针移到右边缘 → 滑出", `data-open=${r.openedByEdge}`); check(r.closedByLeave === "0", "指针离开 → 自动收回", `data-open=${r.closedByLeave}`); check(r.reopened === "1", "再次移到右边缘 → 再次滑出", `data-open=${r.reopened}`); check(r.afterClose === "0", "「取消」收起后不会被边缘热区立刻弹回", `data-open=${r.afterClose}`); check(r.afterRearm === "1", "离开边缘后再回来 → 重新可触发", `data-open=${r.afterRearm}`); console.log("\n=== ③ 子路径下没有 404(相对路径正确)==="); check(notFound.length === 0, "静态服务器 0 个 404", notFound.slice(0, 3).join(" | ") || "无"); const noisy = r.consoleLines.filter((l) => /error|uncaught/i.test(l)); check(r.exceptions.length === 0, "0 未捕获异常", r.exceptions.slice(0, 2).join(" | ") || "无"); check(noisy.length === 0, "0 控制台报错", noisy.slice(0, 3).join(" | ") || "干净"); server.close(); console.log(failures === 0 ? "\n静态预览:通过(面板不依赖调试服,且随右边缘滑出)" : `\n静态预览:失败 ${failures} 处`); process.exit(failures === 0 ? 0 : 1);