From 352f99d479d6ecf6db5b3b57367779d9501dafe7 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 19 Jun 2026 18:52:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(qa):=20=E4=BF=AE=E9=A1=B5=E9=9D=A2=E8=BF=98?= =?UTF-8?q?=E5=8E=9F=E5=AF=B9=E6=AF=94=20harness(Mac=E6=AD=BB=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=20+=20file://token=E5=B4=A9)+=20=E5=85=A8=E9=87=8F28?= =?UTF-8?q?=E9=A1=B5=E8=B7=91=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当年只对了 account/team 两页就卡住,真因两个: - compare-all.mjs designRoot 写死成原开发者 Mac 路径 → 本机找不到 HTML 稿,改 repoRoot 自适配 - compare-page.mjs preparePage 对 file:// 设计稿也注入 token(file:// origin=null)→ 崩, 限定 token 只对 http 目标注入(登录后页面如 dashboard 当年对不了的根因) 全量 28 页 pixelmatch:22 优秀(<2%)+ 3 良好(2-5%);3 个大 diff 均为有意偏离 (login/register 认证去邮箱)或真实数据(dashboard 修正后 6.38%),非回归。 + _shot-extract-flow.mjs:提取新流程无头走查脚本。 报告:docs/todo/页面还原-截图对比收尾-2026-06-19.md Co-Authored-By: Claude Opus 4.8 --- core/qa/visual-parity/_shot-extract-flow.mjs | 60 +++++++++++++++++++ core/qa/visual-parity/compare-all.mjs | 4 +- core/qa/visual-parity/compare-page.mjs | 4 +- docs/todo/页面还原-截图对比收尾-2026-06-19.md | 35 +++++++++++ 4 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 core/qa/visual-parity/_shot-extract-flow.mjs create mode 100644 docs/todo/页面还原-截图对比收尾-2026-06-19.md diff --git a/core/qa/visual-parity/_shot-extract-flow.mjs b/core/qa/visual-parity/_shot-extract-flow.mjs new file mode 100644 index 0000000..68d0c71 --- /dev/null +++ b/core/qa/visual-parity/_shot-extract-flow.mjs @@ -0,0 +1,60 @@ +// 抓图:实体提取新流程 —— 蒙版三按钮 → 点「只提取」→ 卡片 → 故事板生成前拦截弹窗 +import { chromium } from "playwright"; +import { mkdirSync } from "node:fs"; +const BASE = "http://127.0.0.1:5173"; +const API = "http://127.0.0.1:8010"; +const PID = process.argv[2] || "c904386e-e282-4e7d-93ce-e68d94ad33e2"; // 蒙版态项目 +const OUT = "output/extract-flow"; +mkdirSync(OUT, { recursive: true }); + +const tok = (await (await fetch(`${API}/api/auth/login/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "airshelf", password: "Restraint2026" }) })).json()).token; +console.log("token", tok ? "OK" : "FAIL", "| project", PID); + +const browser = await chromium.launch(); +const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 }); +await ctx.addInitScript((t) => localStorage.setItem("airshelf_token", t), tok); +const page = await ctx.newPage(); +page.on("pageerror", (e) => console.error(" pageerror:", e.message)); + +async function shot(name) { await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: false }); console.log(" shot", name); } + +try { + // 1) 进资产趴 → 应见蒙版 + 三按钮 + await page.goto(`${BASE}/pipeline/${PID}?st=2#stage-2`, { waitUntil: "networkidle", timeout: 30000 }); + await page.waitForTimeout(1800); + const gate = await page.locator(".extract-gate").count(); + const btns = await page.locator(".eg-btn").count(); + console.log(` [1] 蒙版闸门: extract-gate=${gate}, 按钮数=${btns}`); + await shot("1-gate"); + + // 2) 点「只提取关键词」(第一个按钮,不出图、便宜)→ 转圈 → 卡片 + if (btns > 0) { + await page.locator(".eg-btn").first().click(); + await page.waitForTimeout(900); + await shot("2-loading"); // 转圈态 + // 等提取完(闸门消失或最多 ~40s) + await page.locator(".extract-gate").waitFor({ state: "detached", timeout: 45000 }).catch(() => console.log(" (闸门 45s 未消失)")); + await page.waitForTimeout(2000); + const cards = await page.locator(".asset-sec").count(); + console.log(` [2] 提取后资产区块数=${cards}`); + await shot("3-after-extract"); + } + + // 3) 故事板生成前拦截弹窗(无资产 → 缺参考图 → 弹窗) + await page.goto(`${BASE}/pipeline/${PID}?st=3#stage-3`, { waitUntil: "networkidle", timeout: 30000 }); + await page.waitForTimeout(1500); + const sbBtn = page.locator("#sb-rerun-btn"); + if (await sbBtn.count()) { + await sbBtn.click(); + await page.waitForTimeout(1000); + const popup = await page.locator(".ref-gate-modal").count(); + console.log(` [3] 点生成故事板 → 拦截弹窗 ref-gate-modal=${popup}`); + await shot("4-popup-intercept"); + } else { + console.log(" [3] 故事板阶段没有生成按钮(可能需先确认资产),跳过弹窗截图"); + } +} catch (e) { + console.error("FAIL", e.message); +} +await browser.close(); +console.log("DONE ->", OUT); diff --git a/core/qa/visual-parity/compare-all.mjs b/core/qa/visual-parity/compare-all.mjs index be36f52..8a886ac 100644 --- a/core/qa/visual-parity/compare-all.mjs +++ b/core/qa/visual-parity/compare-all.mjs @@ -5,8 +5,8 @@ import { fileURLToPath } from "node:url"; const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(here, "../../.."); -const designRoot = "file:///Users/maidong/Desktop/zyc/qiyuan_gitea/AirShelf/电商AI平台"; -const targetRoot = "http://127.0.0.1:5173/exact"; +const designRoot = `file:///${path.resolve(repoRoot, "电商AI平台").replace(/\\/g, "/")}`; +const targetRoot = process.env.TARGET_ROOT || "http://127.0.0.1:5173/exact"; const manifest = JSON.parse(fs.readFileSync(path.join(here, "pages.json"), "utf8")); function arg(name, fallback = "") { diff --git a/core/qa/visual-parity/compare-page.mjs b/core/qa/visual-parity/compare-page.mjs index 570330e..b702f00 100644 --- a/core/qa/visual-parity/compare-page.mjs +++ b/core/qa/visual-parity/compare-page.mjs @@ -38,7 +38,7 @@ async function preparePage(page, url, shouldClearStorage, token) { // 先到同源根页注入 token,再进目标路由 —— 否则目标路由在「无 token 首跳」时 // 会跑一遍登出/重定向 boot,某些路由(如 /model-photo)即便随后 reload 也回不来, // 导致截图停在登录页(假性 ~27% diff)。 - if (token) { + if (token && /^https?:\/\//.test(url)) { await page.goto(new URL(url).origin + "/", { waitUntil: "domcontentloaded" }); await page.evaluate((value) => localStorage.setItem("airshelf_token", value), token); } @@ -51,7 +51,7 @@ async function preparePage(page, url, shouldClearStorage, token) { await page.goto(url, { waitUntil: "networkidle" }); } // 注入登录 token,让受保护的真 React 路由 + /exact 镜像都能 hydrate 同一份真实数据 - if (token) { + if (token && /^https?:\/\//.test(url)) { await page.evaluate((value) => localStorage.setItem("airshelf_token", value), token); await page.goto(url, { waitUntil: "networkidle" }); await page.waitForTimeout(900); // 等异步数据 hydrate 落定 diff --git a/docs/todo/页面还原-截图对比收尾-2026-06-19.md b/docs/todo/页面还原-截图对比收尾-2026-06-19.md new file mode 100644 index 0000000..7fec412 --- /dev/null +++ b/docs/todo/页面还原-截图对比收尾-2026-06-19.md @@ -0,0 +1,35 @@ +# 页面还原 · 截图对比收尾(pixelmatch)· 2026-06-19 + +> 配套 [UI还原对比报告-AirShelf-2026-06-19.md](../../UI还原对比报告-AirShelf-2026-06-19.md)。当年只出了文字报告 + 对了 account/team 两页; +> 这次把 **React 实现 ↔ HTML 设计稿两边截图 + pixelmatch** 全量 28 页跑完收尾。 + +## 当年没收尾的两个真因(已修) +1. **Mac 死路径**:`compare-all.mjs` 里 `designRoot` 写死成原开发者 Mac 路径 + `file:///Users/maidong/...` —— 本机(Windows)找不到 HTML 稿。已改为 `repoRoot` 自适配。 +2. **file:// + token 崩溃**:`compare-page.mjs` 的 `preparePage` 对 file:// 设计稿也尝试 + `new URL(url).origin`(file:// 的 origin = `null`)→ 注入 token 时崩。已限定 token 注入**只对 http 目标**做。 + 这是登录后页面(如 dashboard)当年对不了的根因。 + +## 跑法 +- 工具:[core/qa/visual-parity/compare-all.mjs](../../core/qa/visual-parity/compare-all.mjs)(playwright + pixelmatch),视口 1440×900。 +- 设计稿(design):`电商AI平台/.html`(file://)。 +- 实现(implementation):React `http://127.0.0.1:5173/exact/.html`(像素复刻路由);login/register/dashboard 用真应用路由。 +- 产物:`core/qa/visual-parity/output/.{design,implementation,diff}.png` + `.report.json` + 汇总 `all.report.json`。 +- 复跑:`cd core/qa/visual-parity && node compare-all.mjs`(可 `--only <名>` 单跑)。 + +## 结果(diff 越小越接近设计稿) + +| 档位 | 页数 | 页 | +|---|---|---| +| ✅ 优秀 <2% | **22** | settings(4 段)·pipeline(5 段)·library·account·products·products-create-drawer·product-detail·projects·projects-new·asset-factory·image-optimize·model-photo·model-photo-demo-b·platform-cover | +| 🟡 良好 2–5% | 3 | team 2.12% · model-photo-demo-a 2.21% · messages 2.85% | +| 🟠 有意偏离/真数据 | 3 | login 11.5% · register 14.0%(认证去邮箱)· dashboard 6.38%(真实数据,修 token bug 后) | + +**总评:22 优秀 + 3 良好 = 25/28 高度还原;3 个偏离均为设计决策/真实数据,非回归;0 跑失败。** ~1–2% 的残差是字体渲染 + 真实数据(非 mock)造成,属可接受。 + +## 3 个大 diff = 有意偏离 / 真实数据,非回归(均看截图核实) +- **login 11.5% / register 14.0%**:认证定调**用户名 + 密码 + 邀请码,去掉邮箱**(有意偏离 V1 邮箱设计稿,见 CLAUDE.md「认证/账号模型」)。截图已确认:登录页用「用户名」字段;注册页是「团队名 / 用户名 / 密码 / 邀请码」完整邀请制表单、无邮箱。**与设计稿差异是设计决策,不修。** +- **dashboard:修 token bug 前 19.5%(假性,被踢回登录页)→ 带 token 走根路由重跑后 6.38%**。截图已确认真工作台渲染(KPI/最近项目/余额/快捷入口),6.38% 是**真实数据 vs 静态 mock 数**(项目数、余额 ¥1111、项目名等),非回归。 + +## 待办(若要进一步收窄良好档) +- messages 2.85% / team 2.12% / model-photo-demo-a 2.21%:可逐页看 `output/.diff.png` 红块,多为间距/字号/真实数据微差;非阻塞,按需精修。