Merge pull request #301 from pikasTech/fix/ui-layout-smoke-273
test: add Cloud Workbench layout smoke
This commit is contained in:
@@ -43,6 +43,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥
|
|||||||
- Cloud Web 静态检查:`npm run web:check`
|
- Cloud Web 静态检查:`npm run web:check`
|
||||||
- Cloud Web 构建:`npm run web:build`
|
- Cloud Web 构建:`npm run web:build`
|
||||||
- Cloud Web M3 只读护栏:`npm run web:m3-readonly`
|
- Cloud Web M3 只读护栏:`npm run web:m3-readonly`
|
||||||
|
- Cloud Workbench 布局/遮挡 smoke:`node scripts/dev-cloud-workbench-layout-smoke.mjs --static`
|
||||||
- DEV artifact 发布预检:`npm run dev-artifact:preflight`
|
- DEV artifact 发布预检:`npm run dev-artifact:preflight`
|
||||||
- runner GitHub 可见性预检:`npm run runner:issue-visibility:preflight`
|
- runner GitHub 可见性预检:`npm run runner:issue-visibility:preflight`
|
||||||
- D601 k3s 只读观测:`npm run d601:k3s:readonly`
|
- D601 k3s 只读观测:`npm run d601:k3s:readonly`
|
||||||
|
|||||||
@@ -97,10 +97,40 @@ when identity drift blocks the full journey. It never sends `POST
|
|||||||
must not be used as a DEV-LIVE reply, Secret, mutation, or M3/M4/M5 acceptance
|
must not be used as a DEV-LIVE reply, Secret, mutation, or M3/M4/M5 acceptance
|
||||||
claim.
|
claim.
|
||||||
|
|
||||||
|
## Layout Smoke
|
||||||
|
|
||||||
|
`scripts/dev-cloud-workbench-layout-smoke.mjs` 是 #273 前端布局/遮挡护栏的
|
||||||
|
runner 入口。它使用 Playwright 覆盖 `1366x768`、`1024x768` 和 `390x844`
|
||||||
|
三档视口,检查 M3 控制区、Code Agent 输入区、右侧硬件/可信记录容器、当前
|
||||||
|
`/gate` 内部页、外层滚动锁和内部面板滚动。命中测试使用真实 Playwright
|
||||||
|
点击与 `document.elementsFromPoint()`;不得使用 `force: true` 或纯 DOM
|
||||||
|
dispatch 绕过用户命中。
|
||||||
|
|
||||||
|
常用命令:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node scripts/dev-cloud-workbench-layout-smoke.mjs --static --report reports/dev-gate/dev-cloud-workbench-layout.json
|
||||||
|
node scripts/dev-cloud-workbench-layout-smoke.mjs --build --report reports/dev-gate/dev-cloud-workbench-layout-build.json
|
||||||
|
node scripts/dev-cloud-workbench-layout-smoke.mjs --live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-layout-live.json
|
||||||
|
```
|
||||||
|
|
||||||
|
报告字段必须能定位 `status=pass|blocked|skip`、`viewport`、`selector`、
|
||||||
|
`failureType` 和 artifact 路径。当前允许的 `failureType` 包括 `overlap`、
|
||||||
|
`covered-hit-target`、`overflow`、`outer-scroll-regression`、`screenshot-diff`
|
||||||
|
以及未上线覆盖项的 `skip`。脚本会保存右侧面板、M3 控制表单、Code Agent
|
||||||
|
输入区和 `/gate` 当前页局部截图,但不做整页像素 diff 门禁。
|
||||||
|
|
||||||
|
#287 的硬件状态标签化和 #288 的 `/gate` 单一大表如果尚未上线,layout smoke
|
||||||
|
只能把对应覆盖项记录为 `skip`,同时继续检查当前可见容器的溢出、遮挡和外层
|
||||||
|
滚动。该 UI smoke 只能证明布局/可点击性护栏通过,不等于 M3 DEV-LIVE 硬件
|
||||||
|
闭环通过,也不能替代 #227 的 DO/DI 功能验收。
|
||||||
|
|
||||||
## Stable Sources
|
## Stable Sources
|
||||||
|
|
||||||
- [pikasTech/HWLAB#99](https://github.com/pikasTech/HWLAB/issues/99): default
|
- [pikasTech/HWLAB#99](https://github.com/pikasTech/HWLAB/issues/99): default
|
||||||
Cloud Workbench direction.
|
Cloud Workbench direction.
|
||||||
|
- [pikasTech/HWLAB#273](https://github.com/pikasTech/HWLAB/issues/273):
|
||||||
|
Playwright layout/overlap smoke guardrail.
|
||||||
- [pikasTech/HWLAB#108](https://github.com/pikasTech/HWLAB/issues/108): no
|
- [pikasTech/HWLAB#108](https://github.com/pikasTech/HWLAB/issues/108): no
|
||||||
outer scroll, Chinese UI, internal Markdown help.
|
outer scroll, Chinese UI, internal Markdown help.
|
||||||
- [code-agent-chat-readiness.md](code-agent-chat-readiness.md): Code Agent
|
- [code-agent-chat-readiness.md](code-agent-chat-readiness.md): Code Agent
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseSmokeArgs,
|
||||||
|
printSmokeHelp,
|
||||||
|
runDevCloudWorkbenchLayoutSmoke
|
||||||
|
} from "./src/dev-cloud-workbench-smoke-lib.mjs";
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
|
export function parseLayoutSmokeArgs(argv) {
|
||||||
|
if (argv.includes("--help")) return { help: true };
|
||||||
|
const normalized = ["--layout"];
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index];
|
||||||
|
if (arg === "--static" || arg === "--source") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--live") {
|
||||||
|
const next = argv[index + 1];
|
||||||
|
if (next && !next.startsWith("--")) {
|
||||||
|
normalized.push("--url");
|
||||||
|
normalized.push(next);
|
||||||
|
index += 1;
|
||||||
|
} else {
|
||||||
|
normalized.push("--url");
|
||||||
|
normalized.push("http://74.48.78.17:16666/");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalized.push(arg);
|
||||||
|
if (["--url", "--report"].includes(arg)) {
|
||||||
|
index += 1;
|
||||||
|
if (!argv[index]) throw new Error(`${arg} requires a value`);
|
||||||
|
normalized.push(argv[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parseSmokeArgs(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function layoutSmokeExitCode(report) {
|
||||||
|
return report.status === "pass" || report.status === "usage" ? 0 : report.status === "skip" ? 2 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||||
|
try {
|
||||||
|
const args = parseLayoutSmokeArgs(process.argv.slice(2));
|
||||||
|
const report = args.help
|
||||||
|
? {
|
||||||
|
...printSmokeHelp(),
|
||||||
|
command: "node scripts/dev-cloud-workbench-layout-smoke.mjs [--static|--build|--live --url http://74.48.78.17:16666/] [--report reports/dev-gate/dev-cloud-workbench-layout.json]",
|
||||||
|
layoutNotes: [
|
||||||
|
"默认 --static 使用 source/static 本地静态服务。",
|
||||||
|
"--build 会先运行 web/hwlab-cloud-web/scripts/build.mjs,再检查本地 dist 构建产物。",
|
||||||
|
"--live --url http://74.48.78.17:16666/ 只做 DEV live UI 布局/命中/溢出检查,不发送 Code Agent 消息,不调用 M3 IO,不等于 M3 DEV-LIVE 功能通过。",
|
||||||
|
"结构化失败会包含 status、viewport、selector、failureType、artifact screenshot/report path。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: await runDevCloudWorkbenchLayoutSmoke(args);
|
||||||
|
if (args.reportPath && report.status !== "usage") {
|
||||||
|
const reportPath = path.resolve(process.cwd(), args.reportPath);
|
||||||
|
report.artifacts = { ...(report.artifacts ?? {}), reportPath };
|
||||||
|
await mkdir(path.dirname(reportPath), { recursive: true });
|
||||||
|
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||||
|
}
|
||||||
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||||
|
process.exitCode = layoutSmokeExitCode(report);
|
||||||
|
} catch (error) {
|
||||||
|
const failure = {
|
||||||
|
status: "blocked",
|
||||||
|
mode: "layout-browser",
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
failureType: "blocked",
|
||||||
|
selector: null,
|
||||||
|
viewport: null,
|
||||||
|
artifact: null,
|
||||||
|
reportPath: null,
|
||||||
|
summary: error instanceof Error ? error.message : String(error)
|
||||||
|
};
|
||||||
|
process.stderr.write(`[dev-cloud-workbench-layout-smoke] ${error instanceof Error ? error.stack : String(error)}\n`);
|
||||||
|
process.stdout.write(`${JSON.stringify(failure, null, 2)}\n`);
|
||||||
|
process.exitCode = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { repoRoot };
|
||||||
@@ -19,6 +19,9 @@ import {
|
|||||||
decideSmokeReportWrite,
|
decideSmokeReportWrite,
|
||||||
smokeCliExitCode
|
smokeCliExitCode
|
||||||
} from "./dev-cloud-workbench-smoke.mjs";
|
} from "./dev-cloud-workbench-smoke.mjs";
|
||||||
|
import {
|
||||||
|
parseLayoutSmokeArgs
|
||||||
|
} from "./dev-cloud-workbench-layout-smoke.mjs";
|
||||||
|
|
||||||
const sourceIdentity = Object.freeze({
|
const sourceIdentity = Object.freeze({
|
||||||
status: "observed",
|
status: "observed",
|
||||||
@@ -161,12 +164,37 @@ test("smoke args include reusable layout-only browser mode", () => {
|
|||||||
assert.equal(local.mode, "layout");
|
assert.equal(local.mode, "layout");
|
||||||
assert.equal(local.urlExplicit, undefined);
|
assert.equal(local.urlExplicit, undefined);
|
||||||
|
|
||||||
|
const build = parseSmokeArgs(["--layout", "--build"]);
|
||||||
|
assert.equal(build.mode, "layout");
|
||||||
|
assert.equal(build.build, true);
|
||||||
|
|
||||||
const live = parseSmokeArgs(["--layout", "--url", "http://74.48.78.17:16666/"]);
|
const live = parseSmokeArgs(["--layout", "--url", "http://74.48.78.17:16666/"]);
|
||||||
assert.equal(live.mode, "layout");
|
assert.equal(live.mode, "layout");
|
||||||
assert.equal(live.url, "http://74.48.78.17:16666/");
|
assert.equal(live.url, "http://74.48.78.17:16666/");
|
||||||
assert.equal(live.urlExplicit, true);
|
assert.equal(live.urlExplicit, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("dedicated layout smoke CLI supports static build and DEV live shorthand", () => {
|
||||||
|
const local = parseLayoutSmokeArgs(["--static"]);
|
||||||
|
assert.equal(local.mode, "layout");
|
||||||
|
assert.equal(local.urlExplicit, undefined);
|
||||||
|
|
||||||
|
const build = parseLayoutSmokeArgs(["--build", "--report", "reports/dev-gate/dev-cloud-workbench-layout.json"]);
|
||||||
|
assert.equal(build.mode, "layout");
|
||||||
|
assert.equal(build.build, true);
|
||||||
|
assert.equal(build.reportPath, "reports/dev-gate/dev-cloud-workbench-layout.json");
|
||||||
|
|
||||||
|
const liveDefault = parseLayoutSmokeArgs(["--live"]);
|
||||||
|
assert.equal(liveDefault.mode, "layout");
|
||||||
|
assert.equal(liveDefault.url, "http://74.48.78.17:16666/");
|
||||||
|
assert.equal(liveDefault.urlExplicit, true);
|
||||||
|
|
||||||
|
const liveExplicit = parseLayoutSmokeArgs(["--live", "--url", "http://74.48.78.17:16666/"]);
|
||||||
|
assert.equal(liveExplicit.mode, "layout");
|
||||||
|
assert.equal(liveExplicit.url, "http://74.48.78.17:16666/");
|
||||||
|
assert.equal(liveExplicit.urlExplicit, true);
|
||||||
|
});
|
||||||
|
|
||||||
test("source/default smoke covers #278 sidebar collapse static contract", () => {
|
test("source/default smoke covers #278 sidebar collapse static contract", () => {
|
||||||
const report = runDevCloudWorkbenchStaticSmoke();
|
const report = runDevCloudWorkbenchStaticSmoke();
|
||||||
assert.equal(report.checks.find((check) => check.id === "feedback-278-sidebar-collapse-contract")?.status, "pass");
|
assert.equal(report.checks.find((check) => check.id === "feedback-278-sidebar-collapse-contract")?.status, "pass");
|
||||||
@@ -447,12 +475,28 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
|
|||||||
"layout-desktop-expanded",
|
"layout-desktop-expanded",
|
||||||
"layout-desktop-collapsed",
|
"layout-desktop-collapsed",
|
||||||
"layout-desktop-restored",
|
"layout-desktop-restored",
|
||||||
|
"layout-narrow-desktop-expanded",
|
||||||
|
"layout-narrow-desktop-collapsed",
|
||||||
|
"layout-narrow-desktop-restored",
|
||||||
"layout-mobile-collapsed",
|
"layout-mobile-collapsed",
|
||||||
"layout-mobile-drawer"
|
"layout-mobile-drawer",
|
||||||
|
"layout-gate-desktop",
|
||||||
|
"layout-gate-narrow-desktop",
|
||||||
|
"layout-gate-mobile"
|
||||||
]) {
|
]) {
|
||||||
assert.equal(report.checks.find((check) => check.id === id)?.status, "pass", id);
|
assert.equal(report.checks.find((check) => check.id === id)?.status, "pass", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
report.viewports.map((viewport) => `${viewport.width}x${viewport.height}`),
|
||||||
|
["1366x768", "1024x768", "390x844"]
|
||||||
|
);
|
||||||
|
assert.equal(Array.isArray(report.failures), true);
|
||||||
|
assert.equal(Array.isArray(report.skipped), true);
|
||||||
|
assert.equal(report.safety.hitTestMethod.includes("elementsFromPoint"), true);
|
||||||
|
assert.equal(report.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status, "skip");
|
||||||
|
assert.equal(report.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status, "skip");
|
||||||
|
|
||||||
const desktopCollapsed = report.checks.find((check) => check.id === "layout-desktop-collapsed")?.observations;
|
const desktopCollapsed = report.checks.find((check) => check.id === "layout-desktop-collapsed")?.observations;
|
||||||
assert.equal(desktopCollapsed.explorerCollapsed, true);
|
assert.equal(desktopCollapsed.explorerCollapsed, true);
|
||||||
assert.equal(desktopCollapsed.toggle.ariaLabel, "展开左侧资源树");
|
assert.equal(desktopCollapsed.toggle.ariaLabel, "展开左侧资源树");
|
||||||
@@ -464,4 +508,9 @@ test("layout smoke verifies desktop and mobile sidebar collapse geometry", async
|
|||||||
assert.equal(mobileCollapsed.explorerCollapsed, true);
|
assert.equal(mobileCollapsed.explorerCollapsed, true);
|
||||||
assert.equal(mobileCollapsed.defaultCollapsedMobile, true);
|
assert.equal(mobileCollapsed.defaultCollapsedMobile, true);
|
||||||
assert.equal(mobileCollapsed.keyTargetsReachable, true);
|
assert.equal(mobileCollapsed.keyTargetsReachable, true);
|
||||||
|
|
||||||
|
const desktopExpanded = report.checks.find((check) => check.id === "layout-desktop-expanded")?.observations;
|
||||||
|
assert.equal(desktopExpanded.failures.length, 0);
|
||||||
|
assert.equal(desktopExpanded.semanticOverlapChecks.every((check) => !check.overlaps), true);
|
||||||
|
assert.equal(desktopExpanded.overflowChecks.every((check) => check.ok), true);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user