用户要求(2026-09-13): - portal-entry:「用户管理」改名「用户设置」+ 去掉「打开管理台」入口(order 让位 102→103) - business-plugins:删掉跳转 portal 的按钮;分区改为管理项列表,5 项每项点开是原生弹窗; 「用户管理」弹窗内可直接 通过/禁用/启用/删除(调平台 admin API,不跳门户) - 版本:portal-entry 0.5.2 / business-plugins 0.3.0(均已铺发生效) - 验收脚本同步更新,npm run verify 全绿
116 lines
6.0 KiB
JavaScript
116 lines
6.0 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* verify-platform-admin-section.mjs —— R2「平台管理」分区的**无浏览器**渲染验收(档案 82)
|
||
*
|
||
* 为什么需要:本机 `agent-browser` 的 daemon 起不来(2026-09-13),浏览器截图验收受阻;
|
||
* 而 `06-工作台UI规范 §7.3` 三段式在 bundle **按需动态加载** 时也拿不到带 rev 的完整 URL。
|
||
* 做法:打桩 `react` / `react/jsx-runtime` + 最小 hooks 循环 + **真执行** client.js,
|
||
* 断言分区**被注册**、**admin 渲染出 6 张卡**、**非 admin 被门禁挡住**。
|
||
* 用法:node scripts/verify-platform-admin-section.mjs 退出码 0=全绿 / 1=有失败
|
||
*/
|
||
import { readFileSync } from "node:fs";
|
||
import { fileURLToPath } from "node:url";
|
||
import { dirname, join } from "node:path";
|
||
import vm from "node:vm";
|
||
|
||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||
const src = readFileSync(join(ROOT, "poc/business-plugins/lib/client.js"), "utf8");
|
||
let failed = 0;
|
||
const ok = (name, cond, extra = "") => { console.log((cond ? " ✓ " : " ✗ ") + name + (extra ? " " + extra : "")); if (!cond) failed++; };
|
||
|
||
let slots = [], cursor = 0, dirty = false;
|
||
const React = {
|
||
useState(init) { const i = cursor++; if (!(i in slots)) slots[i] = init; return [slots[i], (v) => { slots[i] = typeof v === "function" ? v(slots[i]) : v; dirty = true; }]; },
|
||
useEffect(fn) { const i = cursor++; if (!slots[i]) { slots[i] = 1; fn(); } },
|
||
};
|
||
const jsx = (type, props) => ({ type, props: props || {} });
|
||
const jsxRuntime = { jsx, jsxs: jsx, Fragment: "Fragment" };
|
||
|
||
let def = null;
|
||
const win = {
|
||
__ModuleLoader__: { load: (d) => { def = d; } },
|
||
location: { hostname: "admin.alotbuy.com", protocol: "https:", origin: "https://admin.alotbuy.com" },
|
||
open: (u) => { globalThis.__OPENED = u; },
|
||
document: { createElement: () => ({ setAttribute() {}, remove() {}, textContent: "" }), head: { appendChild() {} } },
|
||
};
|
||
let ROLE = "admin";
|
||
const DATA = {
|
||
"/api/dsh/status": { running: true, instance: { port: 43095, restarts: 2 }, breaker: null },
|
||
"/api/admin/users": { users: [{ role: "admin" }, { role: "active" }, { role: "pending" }, { role: "disabled" }] },
|
||
"/api/admin/storage": { generatedAt: 1, users: [{}, {}] },
|
||
"/api/plugins/business": { plugins: [{}, {}, {}] },
|
||
"/api/admin/runtime": { items: [{}, {}, {}, {}, {}] },
|
||
};
|
||
const sandbox = {
|
||
console, setTimeout, clearTimeout,
|
||
JSON, Object, Promise, Buffer, URL, globalThis: undefined,
|
||
window: win, document: win.document,
|
||
fetch: async (url) => {
|
||
const p = String(url).replace("https://alotbuy.com", "");
|
||
const body = p === "/api/auth/me" ? { user: { id: "u1", username: ROLE, role: ROLE } } : DATA[p];
|
||
return { ok: body !== undefined, status: body === undefined ? 404 : 200, json: async () => body };
|
||
},
|
||
require: (m) => (m === "react" ? React : m === "react/jsx-runtime" ? jsxRuntime : (() => { throw new Error("require " + m); })()),
|
||
};
|
||
sandbox.globalThis = sandbox;
|
||
vm.createContext(sandbox);
|
||
vm.runInContext(src, sandbox);
|
||
const mod = def.factory(sandbox.require);
|
||
|
||
let DICT = {}, regs = [];
|
||
const ctx = {
|
||
effect: (fn, name) => { try { fn(); } catch (e) { console.log(" effect 抛错:", name, e.message); } },
|
||
locale: { register: (ns, d) => { DICT = Object.assign({}, dICT_zh(d)); }, bind: () => (k) => DICT[k] ?? k },
|
||
slots: { inject: (n, fn) => fn(), register: (meta, Comp) => { regs.push({ meta, Comp }); return () => {}; } },
|
||
};
|
||
function dICT_zh(d) { return d.zh || {}; }
|
||
const inj = Array.isArray(mod.inject) ? mod.inject : [];
|
||
console.log(" inject 声明:", JSON.stringify(inj));
|
||
mod.apply(ctx);
|
||
console.log(" 已注册 section:", regs.map(r => r.meta.id + " / order=" + r.meta.order + " / label=" + r.meta.label()).join(" | "));
|
||
|
||
function text(n, out = [], depth = 0) {
|
||
if (n == null || depth > 12) return out;
|
||
if (typeof n === "string" || typeof n === "number") { out.push(String(n)); return out; }
|
||
if (Array.isArray(n)) { n.forEach(x => text(x, out, depth + 1)); return out; }
|
||
if (typeof n === "object") {
|
||
if (typeof n.type === "function") { text(n.type(n.props || {}), out, depth + 1); return out; }
|
||
if (n.props) text(n.props.children, out, depth + 1);
|
||
}
|
||
return out;
|
||
}
|
||
async function render(Comp) {
|
||
cursor = 0; slots = [];
|
||
let tree = Comp();
|
||
for (let i = 0; i < 6; i++) { await new Promise(r => setTimeout(r, 30)); if (!dirty) break; dirty = false; cursor = 0; tree = Comp(); }
|
||
return tree;
|
||
}
|
||
for (const { meta, Comp } of regs.filter(r => r.meta.id === "platform-admin")) {
|
||
for (const role of ["admin", "active"]) {
|
||
ROLE = role;
|
||
delete globalThis.__OPENED;
|
||
const tree = await render(Comp);
|
||
const t = text(tree).join(" | ");
|
||
console.log("\n ── role=" + role + " ──");
|
||
console.log(" 渲染文本:", t.slice(0, 300));
|
||
console.log(" 含「平台管理」:", t.includes("平台管理"), "| 含 6 张卡:", ["当前实例","熔断","用户","存储","候选池","运行时"].every(k => t.includes(k)));
|
||
if (role === "active") console.log(" 含仅管理员说明:", t.includes("仅对管理员显示"));
|
||
}
|
||
}
|
||
|
||
ok("注册了两个 settings.section", regs.length === 2, "-> " + regs.map(r => r.meta.id).join(", "));
|
||
const pa = regs.find(r => r.meta.id === "platform-admin");
|
||
ok("存在 platform-admin 分区", !!pa);
|
||
if (pa) ok("其 order = 102", pa.meta.order === 102);
|
||
ROLE = "admin"; delete globalThis.__OPENED;
|
||
const tAdmin = text(await render(pa.Comp)).join(" | ");
|
||
ok("admin:含分区标题「系统管理」", tAdmin.includes("系统管理"));
|
||
ok("admin:5 个管理项齐全", ["用户管理","候选池","运行时","存储","当前实例"].every(k => tAdmin.includes(k)));
|
||
ok("admin:不再是只读(提示改为弹窗内管理)", tAdmin.includes("弹窗"));
|
||
ROLE = "active";
|
||
const tUser = text(await render(pa.Comp)).join(" | ");
|
||
ok("非 admin:被门禁挡住", tUser.includes("仅对管理员显示"));
|
||
ok("非 admin:不出现任何管理项", !["候选池","运行时"].some(k => tUser.includes(k)));
|
||
console.log(failed === 0 ? "\n结论:全绿 ✅" : "\n结论:有 " + failed + " 项失败 ❌");
|
||
process.exit(failed === 0 ? 0 : 1);
|