// Phase 4 e2e:质量词 Admin —— 页面分组、编辑模式 chip 增删、持久化校验。 // ⚠ 跑在 live 测试库(生成链路会读它),用唯一标记词且**结束必清理**,绝不污染真实生成。 // build_* 读配置由后端单测 test_generation_reads_config_with_fallback 覆盖,这里只验 UI + 持久化。 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 }); const stamp = Date.now(); const WORD = `e2e画质${stamp}`; 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 authH = { Authorization: `Token ${admin.token}` }; const listWords = async () => (await (await fetch(`${API}/api/admin/quality-words/`, { headers: authH })).json()); const r = { page: {}, add: {}, del: {}, 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}`)); }; try { 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, "quality"); await p.goto(BASE + "/admin/quality", { waitUntil: "load" }); await p.waitForSelector(".admin-app", { timeout: 12000 }); await p.waitForSelector(".qw-stage", { timeout: 8000 }).catch(() => {}); r.page.title = (await p.locator(".content h1").first().innerText().catch(() => "")).trim(); r.page.stageCards = await p.locator(".qw-stage").count(); await p.screenshot({ path: path.join(OUT, "p4-quality-view.png"), fullPage: true }); // 进编辑模式 → person(第一张卡)添加唯一质量词 await p.locator(".page-head .actions .btn").click(); await p.waitForTimeout(300); const personAdd = p.locator(".qw-stage:first-child .qw-add .qw-chip-input"); await personAdd.fill(WORD); await personAdd.press("Enter"); await p.waitForTimeout(900); const personChips = await p.locator(".qw-stage:first-child .qw-chip").count(); r.add.chipShown = personChips >= 1; await p.screenshot({ path: path.join(OUT, "p4-after-add.png"), fullPage: true }); // API 校验持久化 const afterAdd = await listWords(); r.add.persisted = afterAdd.some((w) => w.stage === "person" && w.text === WORD); // UI 删除该词(person 卡最后一个 chip 的 ×) await p.locator(".qw-stage:first-child .qw-chip .qw-del").last().click(); await p.waitForTimeout(900); const afterDel = await listWords(); r.del.removed = !afterDel.some((w) => w.stage === "person" && w.text === WORD); await ctx.close(); } finally { await browser.close(); // 兜底清理:删掉任何残留的 e2e 标记词(防止污染 live 生成) const remaining = (await listWords()).filter((w) => w.text.startsWith("e2e画质")); for (const w of remaining) { await fetch(`${API}/api/admin/quality-words/${w.id}/`, { method: "DELETE", headers: authH }); } r.cleanedUp = remaining.length; } const checks = { pageLoads: r.page.title === "质量词" && r.page.stageCards === 4, addShownAndPersisted: r.add.chipShown === true && r.add.persisted === true, deleteRemoved: r.del.removed === 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, "p4-summary.json"), JSON.stringify(r, null, 2)); process.exit(r.pass ? 0 : 1);