feat(admin): Phase 4 质量词 Admin(平台单层) — QualityWord + 生成侧读配置(无配置回落) + 质量词页
后端: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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6560633959
commit
01d4f235ed
@@ -0,0 +1,83 @@
|
||||
// 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);
|
||||
Reference in New Issue
Block a user