后端:apps/ai QualityWord(stage/slot/text/sort/enabled)+ migration;services.py quality_words/quality_suffix, 4 个 builder 接入(person/model_tryon/storyboard/video)无配置回落原写死值零回归;adminpanel CRUD + 审计。 前端:adminApi quality 系列;Admin 质量词页(按 stage 分组卡 + 编辑模式开关 + 行内增改删 chip)。 测试:adminpanel 23 + accounts 22 = 45 单测过(CRUD/权限/生成读配置+回落/停用词不取); 无头 e2e _admin-p4.mjs 4 断言过 + 0 console error + live 库零残留;tsc+build 绿。 注:apps.ai 3 个 image_edit 路由测试失败为进场前既有(已 stash 比对 HEAD 同样失败),非本 Phase 引入。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
3.9 KiB
JavaScript
84 lines
3.9 KiB
JavaScript
// 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);
|