- app.css 删除 8 个无引用 legacy 别名,新增 --id-1..5 身份灰阶实色阶 - 画布去紫罗兰二级 palette(tokens.css 删除,改用全站 token) - 画布/镜头台/智能体/omni/视频组件硬编码色全部归位 token - agentPalette 彩色身份色改为显影灰阶 --id 阶;修复 3 处相纸白底白字不可读 - WorkflowLibrary 试运行抽屉焦点还原(异步禁用触发键丢焦)修复 - e2e: fpages/import 色值断言对齐现行 token;新增 hel685 走查脚本 (1440/1024/390 截图 + 控制台错误 + 减动效 + 键盘焦点 + 对比度探针); pagination 空库自举;fpages 增加逐页控制台错误检查 Co-authored-by: multica-agent <github@multica.ai>
589 lines
21 KiB
JavaScript
589 lines
21 KiB
JavaScript
/* HEL-655 E 批 browser validation against the isolated stack:
|
||
* vite dev (5173) → backend (8247) → fake ComfyUI (18188).
|
||
* Covers the import drawer four-step checklist, the test-run drawer,
|
||
* keyboard/Esc/focus, reduced motion, 1440/1024/390 widths, and the
|
||
* preserved boundaries (unreachable connection, missing nodes, dry-run
|
||
* refusal, artifact isolation, no-output failure).
|
||
* Prints PASS/FAIL per check; screenshots land in e2e/shots/hel655-*.png. */
|
||
import { chromium } from "playwright";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
||
const SHOTS = path.join(ROOT, "shots");
|
||
fs.mkdirSync(SHOTS, { recursive: true });
|
||
|
||
const API = "http://127.0.0.1:8247";
|
||
const MODE_FILE = path.join(ROOT, "fake_mode.json");
|
||
const setMode = (m) => fs.writeFileSync(MODE_FILE, JSON.stringify(m));
|
||
const baseMode = () => ({ mode: "ok", delay_sec: 0 });
|
||
|
||
let failures = 0;
|
||
function ok(name, cond, extra = "") {
|
||
const line = `${cond ? "PASS" : "FAIL"} ${name}${extra ? " — " + extra : ""}`;
|
||
console.log(line);
|
||
if (!cond) failures++;
|
||
}
|
||
|
||
const api = async (p, init) => {
|
||
const r = await fetch(API + p, {
|
||
headers: { "Content-Type": "application/json" },
|
||
...init,
|
||
});
|
||
const body = await r.text();
|
||
try {
|
||
return JSON.parse(body);
|
||
} catch {
|
||
return { raw: body, status: r.status };
|
||
}
|
||
};
|
||
|
||
/* ---------------- workflow fixtures ---------------- */
|
||
const WF_NAME = `E批·文生图回归 ${Date.now() % 10000}`;
|
||
const T2I = {
|
||
1: {
|
||
class_type: "CLIPTextEncode",
|
||
inputs: { text: "默认提示词" },
|
||
_meta: { title: "(Input:prompt) 正向提示词" },
|
||
},
|
||
9: {
|
||
class_type: "SaveImage",
|
||
inputs: { images: "x" },
|
||
_meta: { title: "(Output:image) 图片输出" },
|
||
},
|
||
};
|
||
|
||
/** Self-consistent /object_info catalogue for a workflow (backend parity:
|
||
* every class present, every string widget a combo listing its value). */
|
||
function buildObjectInfo(wf, { dropClasses = [] } = {}) {
|
||
const info = {};
|
||
for (const [id, node] of Object.entries(wf)) {
|
||
const cls = node.class_type;
|
||
if (!cls || dropClasses.includes(cls)) continue;
|
||
if (info[cls]) continue;
|
||
const required = {};
|
||
for (const [k, v] of Object.entries(node.inputs ?? {})) {
|
||
if (typeof v === "string") required[k] = [[v], {}];
|
||
else if (typeof v === "number")
|
||
required[k] = Number.isInteger(v)
|
||
? ["INT", { min: 0, max: 2 ** 31 }]
|
||
: ["FLOAT", { min: 0, max: 1e6 }];
|
||
}
|
||
info[cls] = { input: { required }, output: ["IMAGE"] };
|
||
}
|
||
return info;
|
||
}
|
||
|
||
/* ---------------- stack prep ---------------- */
|
||
const seeded = (async () => {
|
||
// one fake endpoint + one imported t2i workflow bound to it
|
||
for (const e of (await api("/api/comfyui/endpoints")).endpoints ?? []) {
|
||
if (e.base_url === "http://127.0.0.1:18188")
|
||
await api(`/api/comfyui/endpoints/${e.id}`, { method: "DELETE" });
|
||
}
|
||
let r = await api("/api/comfyui/endpoints", {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
name: "假 ComfyUI",
|
||
base_url: "http://127.0.0.1:18188",
|
||
auth_type: "none",
|
||
}),
|
||
});
|
||
const ep = r.endpoints.find((e) => e.base_url === "http://127.0.0.1:18188");
|
||
await api(`/api/comfyui/endpoints/${ep.id}/activate`, { method: "POST" });
|
||
// dead endpoint for the unreachable-connection scenario (must be unique by
|
||
// name: drop older 19999 endpoints now that the fake one is active)
|
||
for (const e of (await api("/api/comfyui/endpoints")).endpoints ?? []) {
|
||
if (e.base_url === "http://127.0.0.1:19999")
|
||
await api(`/api/comfyui/endpoints/${e.id}`, { method: "DELETE" });
|
||
}
|
||
await api("/api/comfyui/endpoints", {
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
name: "断开的连接",
|
||
base_url: "http://127.0.0.1:19999",
|
||
auth_type: "none",
|
||
}),
|
||
});
|
||
return { endpointId: ep.id };
|
||
})();
|
||
|
||
/* The connections page keeps its own add-connection side form open when the
|
||
* viewport shrinks below 1439 — close it so it doesn't cover the library. */
|
||
async function closeSideFormIfOpen(page) {
|
||
const close = page.locator(".form-col.open .form-head .icon-btn");
|
||
if ((await page.locator(".form-col.open").count()) === 0) return;
|
||
if ((await close.count()) > 0) {
|
||
await close.first().click();
|
||
} else {
|
||
// below 1439 the add-form's own full-screen backdrop covers the page
|
||
await page.mouse.click(20, 400);
|
||
}
|
||
await page.waitForTimeout(250);
|
||
}
|
||
|
||
const run = async () => {
|
||
const { endpointId } = await seeded;
|
||
const browser = await chromium.launch();
|
||
const ctx = await browser.newContext({
|
||
viewport: { width: 1440, height: 900 },
|
||
});
|
||
const page = await ctx.newPage();
|
||
page.on("pageerror", (e) => console.log("PAGEERROR:", e.message));
|
||
|
||
await page.goto("http://127.0.0.1:5173/connections");
|
||
await page.waitForSelector(".wf-panel", { timeout: 15000 });
|
||
|
||
// ---------- 1. 导入入口统一:按钮打开右侧抽屉 ----------
|
||
await page
|
||
.getByRole("button", { name: /导入工作流/ })
|
||
.first()
|
||
.click();
|
||
await page.waitForSelector(".drawer-panel");
|
||
const panel = page.locator(".drawer-panel");
|
||
ok(
|
||
"导入抽屉从右侧滑入(挂载即向左滑动)",
|
||
await panel.evaluate((el) => {
|
||
const name = getComputedStyle(el).animationName;
|
||
return name.includes("drawer-slide") || name === "none";
|
||
}),
|
||
);
|
||
await page.waitForTimeout(360); // let the 280ms drawer settle
|
||
const box = await panel.boundingBox();
|
||
ok("抽屉宽度 380px", Math.round(box.width) === 380, `w=${box.width}`);
|
||
ok("抽屉贴右缘", Math.abs(box.x + box.width - 1440) < 2, `x=${box.x}`);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-01-import-input-1440.png` });
|
||
|
||
// ---------- 2. Step1:无效 JSON 的中文结论 ----------
|
||
await page
|
||
.locator(".drawer-panel textarea.a-textarea")
|
||
.fill('{"不是":"API 格式"}');
|
||
ok(
|
||
"无效 JSON 显示中文错误",
|
||
await page
|
||
.locator(".drawer-panel .field-error")
|
||
.textContent()
|
||
.then((s) => s.includes("API 格式")),
|
||
);
|
||
ok(
|
||
"无效 JSON 时「分析」禁用",
|
||
await page.locator(".drawer-panel .a-btn.primary").isDisabled(),
|
||
);
|
||
await page
|
||
.locator(".drawer-panel textarea.a-textarea")
|
||
.fill(JSON.stringify(T2I));
|
||
await page.locator(".drawer-panel input.a-input").fill(WF_NAME);
|
||
await page
|
||
.locator(".drawer-panel select.a-select")
|
||
.selectOption({ label: "假 ComfyUI" });
|
||
|
||
// ---------- 3. 四步检查 · 全绿 ----------
|
||
setMode({ ...baseMode(), object_info: buildObjectInfo(T2I) });
|
||
await page.locator(".drawer-panel .a-btn.primary").click();
|
||
await page.waitForSelector(".drawer-panel .checks .check");
|
||
const t0 = Date.now();
|
||
await page.waitForFunction(
|
||
() =>
|
||
document.querySelectorAll(".drawer-panel .checks .check").length === 4,
|
||
);
|
||
const spread = Date.now() - t0;
|
||
ok(
|
||
"四步检查逐条出现(首条→第 4 条 ≥140ms)",
|
||
spread >= 140,
|
||
`spread=${spread}ms`,
|
||
);
|
||
const labels = await page
|
||
.locator(".drawer-panel .checks .check-label")
|
||
.allTextContents();
|
||
ok(
|
||
"四步顺序:解析 JSON→识别节点→核对模型→建立输入槽",
|
||
labels[0].includes("解析") &&
|
||
labels[1].includes("识别") &&
|
||
labels[2].includes("核对") &&
|
||
labels[3].includes("输入槽"),
|
||
labels.join("|"),
|
||
);
|
||
const states = await page
|
||
.locator(".drawer-panel .checks .check")
|
||
.evaluateAll((els) =>
|
||
els.map((el) => ({
|
||
ok: el.className.includes("state-ok"),
|
||
color: getComputedStyle(el.querySelector(".mark")).color,
|
||
})),
|
||
);
|
||
ok(
|
||
"① 解析 JSON 亮灰 ✓",
|
||
states[0].ok && states[0].color === "rgb(221, 221, 215)",
|
||
states[0].color,
|
||
);
|
||
ok(
|
||
"② 识别节点判定文生图(绿)",
|
||
states[1].ok,
|
||
await page
|
||
.locator(".drawer-panel .checks .check")
|
||
.nth(1)
|
||
.locator(".check-line")
|
||
.textContent(),
|
||
);
|
||
ok(
|
||
"③ 核对模型全亮灰(假 object_info 装齐)",
|
||
states[2].ok && states[2].color === "rgb(221, 221, 215)",
|
||
states[2].color,
|
||
);
|
||
ok("④ 建立输入槽(1 参数 0 媒体)", states[3].ok);
|
||
await page.screenshot({
|
||
path: `${SHOTS}/hel655-02-checks-allgreen-1440.png`,
|
||
});
|
||
|
||
// ---------- 4. 缺节点:③ 红 + 可展开错误 + 重试恢复 ----------
|
||
setMode({
|
||
...baseMode(),
|
||
object_info: buildObjectInfo(T2I, { dropClasses: ["SaveImage"] }),
|
||
});
|
||
await page
|
||
.locator(".drawer-panel .checks .check")
|
||
.nth(2)
|
||
.locator(".recheck")
|
||
.click();
|
||
await page.waitForFunction(() => {
|
||
const el = document.querySelectorAll(".drawer-panel .checks .check")[2];
|
||
return el && el.className.includes("state-fail");
|
||
});
|
||
const failLine = await page
|
||
.locator(".drawer-panel .checks .check")
|
||
.nth(2)
|
||
.locator(".check-line")
|
||
.textContent();
|
||
ok("③ 缺节点显示中文问题数", /1/.test(failLine), failLine);
|
||
const finding = await page
|
||
.locator(".drawer-panel .findings li .finding-msg")
|
||
.first()
|
||
.textContent();
|
||
ok(
|
||
"错误详情可展开且为中文原因",
|
||
finding.includes("缺少自定义节点"),
|
||
finding.slice(0, 40),
|
||
);
|
||
ok(
|
||
"给出下一步建议",
|
||
(
|
||
await page
|
||
.locator(".drawer-panel .checks .check .suggest")
|
||
.allTextContents()
|
||
).some((s) => s.includes("重新检查")),
|
||
);
|
||
ok(
|
||
"缺节点不阻断保存(沿用既有边界)",
|
||
!(await page.locator(".drawer-panel .a-btn.primary").last().isDisabled()),
|
||
);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-03-missing-node-1440.png` });
|
||
setMode({ ...baseMode(), object_info: buildObjectInfo(T2I) });
|
||
await page
|
||
.locator(".drawer-panel .checks .check")
|
||
.nth(2)
|
||
.locator(".recheck")
|
||
.click();
|
||
await page.waitForFunction(() => {
|
||
const el = document.querySelectorAll(".drawer-panel .checks .check")[2];
|
||
return el && el.className.includes("state-ok");
|
||
});
|
||
ok("「重新检查」复查后恢复绿", true);
|
||
|
||
// ---------- 5. 断开的连接:琥珀「无法核查」,不伪装通过 ----------
|
||
await page.locator(".drawer-panel .a-btn.secondary").first().click(); // 上一步
|
||
await page
|
||
.locator(".drawer-panel select.a-select")
|
||
.selectOption({ label: "断开的连接" });
|
||
await page.locator(".drawer-panel .a-btn.primary").click(); // 重新分析
|
||
await page.waitForFunction(
|
||
() =>
|
||
document.querySelectorAll(".drawer-panel .checks .check").length === 4,
|
||
);
|
||
await page.waitForFunction(() => {
|
||
const el = document.querySelectorAll(".drawer-panel .checks .check")[2];
|
||
return el && el.className.includes("state-warn");
|
||
});
|
||
const warnColor = await page
|
||
.locator(".drawer-panel .checks .check")
|
||
.nth(2)
|
||
.locator(".mark")
|
||
.evaluate((el) => getComputedStyle(el).color);
|
||
ok(
|
||
"无法核查 = 琥珀(非绿非红)",
|
||
warnColor === "rgb(163, 145, 95)",
|
||
warnColor,
|
||
);
|
||
ok(
|
||
"无法核查给中文原因与建议",
|
||
(
|
||
await page
|
||
.locator(".drawer-panel .checks .check")
|
||
.nth(2)
|
||
.locator(".check-line")
|
||
.textContent()
|
||
).includes("无法核查"),
|
||
);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-04-unreachable-1440.png` });
|
||
|
||
// ---------- 6. 保存导入(回到可用连接) ----------
|
||
await page.locator(".drawer-panel .a-btn.secondary").first().click(); // 上一步
|
||
await page
|
||
.locator(".drawer-panel select.a-select")
|
||
.selectOption({ label: "假 ComfyUI" });
|
||
await page.locator(".drawer-panel .a-btn.primary").click(); // 分析
|
||
await page.waitForFunction(
|
||
() =>
|
||
document.querySelectorAll(".drawer-panel .checks .check").length === 4,
|
||
);
|
||
await page.locator(".drawer-panel .a-btn.primary").last().click(); // 确认保存
|
||
await page.waitForSelector(".drawer-panel", {
|
||
state: "detached",
|
||
timeout: 10000,
|
||
});
|
||
ok("确认保存后抽屉关闭", true);
|
||
await page.waitForFunction(
|
||
(n) => document.body.textContent.includes(n),
|
||
WF_NAME,
|
||
);
|
||
ok("工作流入库名称正确", true);
|
||
|
||
// ---------- 7. 试运行抽屉:全链路(进行中→完成→隔离) ----------
|
||
const wfId = (await api("/api/workflows")).find(
|
||
(w) => w.name === WF_NAME,
|
||
)?.id;
|
||
ok("已导入工作流可查询", !!wfId);
|
||
const row = page.locator(".wf-row", { hasText: WF_NAME });
|
||
await row.locator(".wf-test-btn").first().click();
|
||
await page.waitForSelector(".drawer-panel");
|
||
const tbox = await page.locator(".drawer-panel").boundingBox();
|
||
ok(
|
||
"试运行抽屉 420px 右侧",
|
||
Math.round(tbox.width) === 420,
|
||
`w=${tbox.width}`,
|
||
);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-05-test-form-1440.png` });
|
||
|
||
// dry_run 拒绝(边界保留)
|
||
await api("/api/settings", {
|
||
method: "POST",
|
||
body: JSON.stringify({ dry_run: true }),
|
||
});
|
||
await page.locator(".drawer-panel .a-btn.primary").click();
|
||
await page.locator(".modal-panel .modal-footer .a-btn.primary").click();
|
||
await page.waitForFunction(() => {
|
||
const els = [...document.querySelectorAll(".drawer-panel .field-error")];
|
||
return els.some((e) => e.textContent.includes("Dry-run"));
|
||
});
|
||
ok("dry_run 下试运行被拒绝(中文原因)", true);
|
||
await api("/api/settings", {
|
||
method: "POST",
|
||
body: JSON.stringify({ dry_run: false }),
|
||
});
|
||
|
||
// 真实试运行:进行中(青进度 + 阶段日志)→ 通过(绿 + 缩略图横排)
|
||
setMode({ mode: "ok", delay_sec: 3, object_info: buildObjectInfo(T2I) });
|
||
await page.locator(".drawer-panel .a-btn.primary").click();
|
||
await page.locator(".modal-panel .modal-footer .a-btn.primary").click();
|
||
await page.waitForSelector(".drawer-panel .wft-active", { timeout: 8000 });
|
||
await page.waitForSelector(".drawer-panel .wft-active .chip.run", {
|
||
timeout: 8000,
|
||
});
|
||
const chipColor = await page
|
||
.locator(".drawer-panel .wft-active .chip.run")
|
||
.evaluate((el) => getComputedStyle(el).color);
|
||
ok("进行中状态为运行灰", chipColor === "rgb(184, 182, 172)", chipColor);
|
||
ok(
|
||
"进行中显示进度条",
|
||
(await page.locator(".drawer-panel .wft-active .track").count()) === 1,
|
||
);
|
||
await page.waitForSelector(".drawer-panel .wft-log p", { timeout: 6000 });
|
||
const logText = await page.locator(".drawer-panel .wft-log").textContent();
|
||
ok(
|
||
"阶段日志为中文滚动区",
|
||
/队列|排队|连接|运行|保存|执行|ComfyUI/.test(logText),
|
||
logText.slice(0, 30),
|
||
);
|
||
ok(
|
||
"阶段日志为 mono 字体",
|
||
await page
|
||
.locator(".drawer-panel .wft-log")
|
||
.evaluate((el) => getComputedStyle(el).fontFamily.includes("Mono")),
|
||
);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-06-test-running-1440.png` });
|
||
await page.waitForSelector(".drawer-panel .wft-result.ok", {
|
||
timeout: 30000,
|
||
});
|
||
const verdict = await page
|
||
.locator(".drawer-panel .wft-verdict")
|
||
.textContent();
|
||
ok("只有真实输出才算通过(绿判定)", verdict.includes("测试通过"), verdict);
|
||
const thumbs = await page
|
||
.locator(".drawer-panel .wft-outputs .wft-out")
|
||
.count();
|
||
ok("结果缩略图横排(≥1 可下载)", thumbs >= 1, `n=${thumbs}`);
|
||
// 产物隔离:测试任务产物落在 assets/_workflow_test/{id}/
|
||
const jobs = await api("/api/jobs");
|
||
const testJob = jobs.find(
|
||
(j) =>
|
||
j.workflow_id === wfId &&
|
||
j.status === "done" &&
|
||
(j.output_paths ?? []).length,
|
||
);
|
||
ok(
|
||
"试运行产物与普通素材隔离(_workflow_test/)",
|
||
!!testJob && (testJob.output_paths ?? [])[0].includes("_workflow_test"),
|
||
testJob?.output_paths?.[0],
|
||
);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-07-test-passed-1440.png` });
|
||
|
||
// ---------- 8. 失败路径:执行报错 红 + 中文 ----------
|
||
setMode({ mode: "fail" });
|
||
await page.waitForSelector(".drawer-panel .wft-form", { timeout: 10000 });
|
||
await page.locator(".drawer-panel .a-btn.primary").click();
|
||
await page.locator(".modal-panel .modal-footer .a-btn.primary").click();
|
||
await page.waitForSelector(".drawer-panel .wft-result.bad", {
|
||
timeout: 30000,
|
||
});
|
||
const badColor = await page
|
||
.locator(".drawer-panel .wft-result.bad .wft-verdict")
|
||
.evaluate((el) => getComputedStyle(el).color);
|
||
ok(
|
||
"失败判定为红 + 中文标题",
|
||
badColor === "rgb(208, 122, 107)",
|
||
await page
|
||
.locator(".drawer-panel .wft-result.bad .wft-verdict")
|
||
.textContent(),
|
||
);
|
||
ok(
|
||
"失败给出具体原因",
|
||
((await page.locator(".drawer-panel .wft-error").textContent()) ?? "")
|
||
.length > 0,
|
||
);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-08-test-failed-1440.png` });
|
||
|
||
// ---------- 9. 无输出:不算通过 ----------
|
||
setMode({ mode: "ok", delay_sec: 0, no_outputs: true });
|
||
await page.waitForSelector(".drawer-panel .wft-form", { timeout: 10000 });
|
||
await page.locator(".drawer-panel .a-btn.primary").click();
|
||
await page.locator(".modal-panel .modal-footer .a-btn.primary").click();
|
||
await page.waitForFunction(
|
||
() => {
|
||
const bad = document.querySelector(".drawer-panel .wft-result.bad");
|
||
if (!bad) return false;
|
||
// the previous fail verdict also matches .bad — wait until the error text changes
|
||
return (
|
||
!bad.textContent.includes("boom") || bad.textContent.includes("输出")
|
||
);
|
||
},
|
||
{ timeout: 30000 },
|
||
);
|
||
ok("无输出不判通过(红/失败结论)", true);
|
||
setMode({ ...baseMode() });
|
||
|
||
// ---------- 10. 键盘:Esc 关闭、焦点圈、焦点回归 ----------
|
||
await page.keyboard.press("Escape");
|
||
await page.waitForSelector(".drawer-panel", { state: "detached" });
|
||
ok("Esc 关闭试运行抽屉", true);
|
||
await row.locator(".wf-test-btn").first().click();
|
||
await page.waitForSelector(".drawer-panel");
|
||
await page.waitForTimeout(300); // focus moves on the next animation frame
|
||
const focusIn = await page.evaluate(
|
||
() => document.activeElement.closest(".drawer-panel") !== null,
|
||
);
|
||
ok("抽屉打开后焦点进入面板", focusIn);
|
||
for (let i = 0; i < 12; i++) await page.keyboard.press("Tab");
|
||
const stillIn = await page.evaluate(
|
||
() => document.activeElement.closest(".drawer-panel") !== null,
|
||
);
|
||
ok("Tab 在抽屉内循环(焦点不逃逸)", stillIn);
|
||
await page.keyboard.press("Escape");
|
||
await page.waitForSelector(".drawer-panel", { state: "detached" });
|
||
const backOnBtn = await page.evaluate(
|
||
() =>
|
||
document.activeElement?.getAttribute("title") === "测试工作流" ||
|
||
(document.activeElement?.textContent ?? "").trim().startsWith("测试"),
|
||
);
|
||
ok("关闭后焦点回到触发按钮", backOnBtn === true);
|
||
|
||
// ---------- 11. 减少动态效果:抽屉只留透明度 ----------
|
||
await page.evaluate(() => (document.documentElement.dataset.reduce = "1"));
|
||
await page
|
||
.getByRole("button", { name: /导入工作流/ })
|
||
.first()
|
||
.click();
|
||
await page.waitForSelector(".drawer-panel");
|
||
const anim = await page
|
||
.locator(".drawer-panel")
|
||
.evaluate((el) => getComputedStyle(el).animationName);
|
||
ok("减动效时抽屉无位移动画(仅淡变)", anim.includes("drawer-fade"), anim);
|
||
await page.keyboard.press("Escape");
|
||
await page.evaluate(() => delete document.documentElement.dataset.reduce);
|
||
|
||
// ---------- 12. 中屏 1024 / 窄屏 390 ----------
|
||
await page.setViewportSize({ width: 1024, height: 768 });
|
||
await closeSideFormIfOpen(page);
|
||
await page
|
||
.getByRole("button", { name: /导入工作流/ })
|
||
.first()
|
||
.click();
|
||
await page.waitForSelector(".drawer-panel");
|
||
await page.waitForTimeout(320);
|
||
const mid = await page.locator(".drawer-panel").boundingBox();
|
||
ok(
|
||
"中屏抽屉仍 380px 贴右",
|
||
Math.round(mid.width) === 380 && Math.abs(mid.x + mid.width - 1024) < 2,
|
||
);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-09-import-1024.png` });
|
||
await page.keyboard.press("Escape");
|
||
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
await closeSideFormIfOpen(page);
|
||
await page
|
||
.getByRole("button", { name: /导入工作流/ })
|
||
.first()
|
||
.click();
|
||
await page.waitForSelector(".drawer-panel");
|
||
await page.waitForTimeout(320);
|
||
const narrow = await page.locator(".drawer-panel").boundingBox();
|
||
ok("窄屏抽屉全屏宽", Math.round(narrow.width) === 390, `w=${narrow.width}`);
|
||
const overflow = await page.evaluate(
|
||
() =>
|
||
document.scrollingElement.scrollWidth -
|
||
document.scrollingElement.clientWidth,
|
||
);
|
||
ok("窄屏无横向溢出", overflow <= 0, `overflow=${overflow}px`);
|
||
await page.screenshot({ path: `${SHOTS}/hel655-10-import-390.png` });
|
||
await page.keyboard.press("Escape");
|
||
await page.waitForSelector(".drawer-panel", { state: "detached" });
|
||
await page.setViewportSize({ width: 1440, height: 900 });
|
||
|
||
// ---------- 13. 试运行历史与清除 ----------
|
||
await row.locator(".wf-test-btn").first().click();
|
||
await page.waitForSelector(".drawer-panel");
|
||
await page.waitForSelector(".drawer-panel .wft-rows li", { timeout: 8000 });
|
||
const histCount = await page.locator(".drawer-panel .wft-rows li").count();
|
||
ok("测试记录列表保留", histCount >= 2, `n=${histCount}`);
|
||
await page.locator(".drawer-panel .link-danger").click();
|
||
await page.waitForFunction(
|
||
() => document.querySelectorAll(".drawer-panel .wft-rows li").length === 0,
|
||
{ timeout: 8000 },
|
||
);
|
||
ok("清除测试记录生效", true);
|
||
await page.keyboard.press("Escape");
|
||
|
||
await browser.close();
|
||
console.log(failures === 0 ? "\nALL PASS" : `\n${failures} FAILURES`);
|
||
process.exit(failures === 0 ? 0 : 1);
|
||
};
|
||
|
||
run().catch(async (e) => {
|
||
console.error("FATAL", e?.message?.slice(0, 300));
|
||
try {
|
||
const fs = await import("node:fs");
|
||
const { chromium } = await import("playwright");
|
||
} catch {}
|
||
console.error("FATAL", e);
|
||
process.exit(1);
|
||
});
|