Files
yingqing/core/qa/visual-parity/_admin-p4.mjs
T
seaislee1209andClaude Opus 4.8 01d4f235ed 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>
2026-06-19 21:52:41 +08:00

84 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);