// Phase 5 e2e:火山人像审核队列 —— 队列加载、状态盾、筛选、多选 bulk-bar、刷新状态(只读 poll)。 // ⚠ live 库 Volcano 已启用:e2e 只做只读 poll + UI 在场校验,绝不点真送审(送审逻辑由后端 mock 单测覆盖)。 // wrapper 预建一条 person 资产保证有行,跑完删除。 import { chromium } from "playwright"; import fs from "node:fs"; import path from "node:path"; const BASE = process.env.BASE || "http://127.0.0.1:5188"; const API = process.env.API || "http://127.0.0.1:8010"; const OUT = path.resolve("output/admin"); fs.mkdirSync(OUT, { recursive: true }); async function apiLogin(u, p) { const res = await fetch(`${API}/api/auth/login/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: u, password: p }) }); return res.json(); } const admin = await apiLogin("admin", "admin123"); const r = { page: {}, select: {}, poll: {}, consoleErrors: [], pass: false }; const browser = await chromium.launch({ headless: true }); const hook = (p, tag) => { p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push(`${tag}:${m.text()}`); }); p.on("pageerror", (e) => r.consoleErrors.push(`${tag}:PAGEERR:${e.message}`)); }; const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); await ctx.addInitScript((tk) => localStorage.setItem("airshelf_token", tk), admin.token); const p = await ctx.newPage(); hook(p, "reviews"); await p.goto(BASE + "/admin/reviews", { waitUntil: "load" }); await p.waitForSelector(".admin-app", { timeout: 12000 }); // 等真表格(加载态也用 .empty-state.show,不能用它判完成);no rows 时退而求其次等非加载空态 await p.waitForFunction(() => { const t = document.querySelector(".admin-table"); const h3 = document.querySelector(".empty-state.show h3"); return Boolean(t) || (h3 && h3.textContent !== "加载中…"); }, { timeout: 20000 }).catch(() => {}); r.page.title = (await p.locator(".content h1").first().innerText().catch(() => "")).trim(); r.page.tabs = await p.locator(".tabs-sub .tab-sub").count(); r.page.refreshBtn = (await p.locator(".page-head .actions .btn").count()) >= 1; r.page.rows = await p.locator(".admin-table tbody tr").count(); await p.screenshot({ path: path.join(OUT, "p5-reviews-list.png"), fullPage: true }); // 多选 → bulk-bar 出现 + 批量送审在场(不点击,避免真送审) if (r.page.rows >= 1) { await p.locator(".admin-table tbody tr:first-child .col-check input").check(); await p.waitForTimeout(300); r.select.bulkBar = (await p.locator(".bulk-bar").count()) >= 1; r.select.submitBtn = (await p.locator(".bulk-bar .btn-primary").count()) >= 1; r.select.submitEnabled = await p.locator(".bulk-bar .btn-primary").isEnabled().catch(() => false); await p.screenshot({ path: path.join(OUT, "p5-bulkbar.png") }); // 取消选择(避免后面误触) await p.locator(".bulk-bar .btn").first().click(); await p.waitForTimeout(200); } // 刷新状态(只读 poll,安全) await p.locator(".page-head .actions .btn").click(); await p.waitForTimeout(1200); r.poll.clicked = true; await ctx.close(); await browser.close(); const checks = { pageLoads: r.page.title === "资产审核" && r.page.tabs === 5 && r.page.refreshBtn === true, hasRows: r.page.rows >= 1, bulkBarWorks: r.select.bulkBar === true && r.select.submitBtn === true && r.select.submitEnabled === true, pollClicked: r.poll.clicked === true, zeroConsoleErrors: r.consoleErrors.length === 0, }; r.checks = checks; r.pass = Object.values(checks).every(Boolean); console.log(JSON.stringify(r, null, 2)); fs.writeFileSync(path.join(OUT, "p5-summary.json"), JSON.stringify(r, null, 2)); process.exit(r.pass ? 0 : 1);