311 行
10 KiB
JavaScript
311 行
10 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { buildReport, hasBlockingGaps } from "../check-flow-coverage.mjs";
|
|
|
|
const WITNESS_SPEC = `
|
|
import { test } from "@playwright/test";
|
|
export const LOCAL_CANDIDATE_MANIFEST_DRIVEN_WITNESSES = true;
|
|
for (const flow of localCandidateFlows) {
|
|
test(flow.witnessTag, async ({ page }) => {
|
|
installBrowserErrorMonitor(page);
|
|
installSyntheticApis(page, flow.roleFixture);
|
|
await loginToWitness(page, flow.witnessRoute);
|
|
await expectUniqueH1(page, flow.expectedH1);
|
|
await expectPrimaryActionContract(page, flow);
|
|
});
|
|
}
|
|
`;
|
|
|
|
function candidateFlow(bundle = "core-flow", witnessRoute = "/core", overrides = {}) {
|
|
return {
|
|
bundle,
|
|
witnessRoute,
|
|
witnessTag: `@local-candidate:${bundle}`,
|
|
expectedH1: "核心页面",
|
|
roleFixture: "employee",
|
|
primaryAction: { allowed: "开始", forbidden: "越权操作" },
|
|
witnessSpec: "client/tests/e2e/local-candidate-witnesses.spec.ts",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function writeFixture({
|
|
routes = ["/core"],
|
|
coreBundles = ["core-flow"],
|
|
manifestRoutes = [
|
|
{
|
|
path: "/core",
|
|
classification: "core",
|
|
bundle: "core-flow",
|
|
behaviorSpecs: ["client/tests/e2e/core.spec.ts"],
|
|
},
|
|
],
|
|
localCandidateFlows = [candidateFlow()],
|
|
acceptanceSpecs = ["client/tests/e2e/acceptance.spec.ts"],
|
|
specs = {},
|
|
} = {}) {
|
|
const root = mkdtempSync(path.join(tmpdir(), "hyb-flow-coverage-"));
|
|
const routerPath = path.join(root, "client/src/router/index.tsx");
|
|
const manifestPath = path.join(root, "scripts/qa/route-manifest.json");
|
|
mkdirSync(path.dirname(routerPath), { recursive: true });
|
|
mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
writeFileSync(
|
|
routerPath,
|
|
`export function RouterFixture() { return <Routes>${routes.map((route) => `<Route path="${route}" element={<div />} />`).join("")}</Routes>; }\n`,
|
|
"utf8",
|
|
);
|
|
writeFileSync(
|
|
manifestPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
coreBundles,
|
|
acceptanceSpecs,
|
|
localCandidateFlows,
|
|
routes: manifestRoutes,
|
|
}, null, 2)}\n`,
|
|
"utf8",
|
|
);
|
|
|
|
const defaultSpecs = {
|
|
"client/tests/e2e/core.spec.ts": 'import { test } from "@playwright/test";\ntest("core", async ({ page }) => { await page.goto("/core"); });\n',
|
|
"client/tests/e2e/acceptance.spec.ts": 'import { test } from "@playwright/test";\ntest("acceptance", async ({ page }) => { await page.goto("/core"); });\n',
|
|
"client/tests/e2e/local-candidate-witnesses.spec.ts": WITNESS_SPEC,
|
|
...specs,
|
|
};
|
|
for (const [relativePath, source] of Object.entries(defaultSpecs)) {
|
|
if (source == null) continue;
|
|
const file = path.join(root, relativePath);
|
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
writeFileSync(file, source, "utf8");
|
|
}
|
|
return root;
|
|
}
|
|
|
|
function runFixture(root) {
|
|
const report = buildReport(root);
|
|
return { status: hasBlockingGaps(report) ? 1 : 0, report };
|
|
}
|
|
|
|
function withFixture(options, callback) {
|
|
const root = writeFixture(options);
|
|
try {
|
|
callback(root);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
test("完整 manifest、acceptance spec 与可信 witness 通过", () => {
|
|
withFixture({}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 0);
|
|
assert.equal(result.report.summary.blockingGapCount, 0);
|
|
assert.equal(result.report.summary.coveredCoreBundleCount, 1);
|
|
assert.equal(result.report.summary.coveredWitnessCount, 1);
|
|
assert.equal(result.report.summary.validAcceptanceSpecCount, 1);
|
|
});
|
|
});
|
|
|
|
test("未分类注册路由必须阻断", () => {
|
|
withFixture({ routes: ["/core", "/unmapped"] }, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.unclassified, 1);
|
|
});
|
|
});
|
|
|
|
test("403 与 wildcard 注册路由不能从分类扫描中豁免", () => {
|
|
withFixture({ routes: ["/core", "/403", "*"] }, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.routeCount, 3);
|
|
assert.equal(result.report.summary.gapCounts.unclassified, 2);
|
|
});
|
|
});
|
|
|
|
test("manual 或 live spec 不能充当核心行为证据", () => {
|
|
withFixture({
|
|
manifestRoutes: [
|
|
{
|
|
path: "/core",
|
|
classification: "core",
|
|
bundle: "core-flow",
|
|
behaviorSpecs: ["client/tests/e2e/manual/core.spec.ts"],
|
|
},
|
|
],
|
|
specs: {
|
|
"client/tests/e2e/manual/core.spec.ts": 'import { test } from "@playwright/test";\ntest("manual", async ({ page }) => { await page.goto("/core"); });\n',
|
|
},
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.manual_only, 1);
|
|
});
|
|
});
|
|
|
|
test("核心路由缺失行为测试必须阻断", () => {
|
|
withFixture({
|
|
manifestRoutes: [{ path: "/core", classification: "core", bundle: "core-flow" }],
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.missing_behavior, 1);
|
|
});
|
|
});
|
|
|
|
test("visualRequired 缺失可执行视觉 spec 必须阻断", () => {
|
|
withFixture({
|
|
manifestRoutes: [
|
|
{
|
|
path: "/core",
|
|
classification: "core",
|
|
bundle: "core-flow",
|
|
behaviorSpecs: ["client/tests/e2e/core.spec.ts"],
|
|
visualRequired: true,
|
|
},
|
|
],
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.missing_visual, 1);
|
|
});
|
|
});
|
|
|
|
test("核心 bundle 缺失 witness 必须阻断", () => {
|
|
withFixture({ localCandidateFlows: [] }, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.missing_local_candidate_witness, 1);
|
|
});
|
|
});
|
|
|
|
test("重复 bundle 与重复 witness tag 必须阻断", () => {
|
|
withFixture({ localCandidateFlows: [candidateFlow(), candidateFlow()] }, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.duplicate_local_candidate_bundle, 1);
|
|
assert.equal(result.report.summary.gapCounts.duplicate_witness_tag, 1);
|
|
});
|
|
});
|
|
|
|
test("未知 local-candidate bundle 必须阻断", () => {
|
|
withFixture({
|
|
localCandidateFlows: [candidateFlow(), candidateFlow("unknown-flow", "/core")],
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.unknown_local_candidate_bundle, 1);
|
|
});
|
|
});
|
|
|
|
test("重复 core bundle 必须阻断", () => {
|
|
withFixture({ coreBundles: ["core-flow", "core-flow"] }, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.duplicate_core_bundle, 1);
|
|
});
|
|
});
|
|
|
|
test("orphan manifest 路由必须阻断", () => {
|
|
withFixture({
|
|
manifestRoutes: [
|
|
{
|
|
path: "/core",
|
|
classification: "core",
|
|
bundle: "core-flow",
|
|
behaviorSpecs: ["client/tests/e2e/core.spec.ts"],
|
|
},
|
|
{ path: "/orphan", classification: "supported-noncore" },
|
|
],
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.orphan_manifest_route, 1);
|
|
});
|
|
});
|
|
|
|
test("witnessRoute 指向未注册路由必须阻断", () => {
|
|
withFixture({ localCandidateFlows: [candidateFlow("core-flow", "/missing")] }, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.orphan_witness_route, 1);
|
|
});
|
|
});
|
|
|
|
test("witnessRoute 绑定到其他 bundle 必须阻断", () => {
|
|
const otherSpec = 'import { test } from "@playwright/test";\ntest("other", async ({ page }) => { await page.goto("/other"); });\n';
|
|
withFixture({
|
|
routes: ["/core", "/other"],
|
|
coreBundles: ["core-flow", "other-flow"],
|
|
manifestRoutes: [
|
|
{
|
|
path: "/core",
|
|
classification: "core",
|
|
bundle: "core-flow",
|
|
behaviorSpecs: ["client/tests/e2e/core.spec.ts"],
|
|
},
|
|
{
|
|
path: "/other",
|
|
classification: "core",
|
|
bundle: "other-flow",
|
|
behaviorSpecs: ["client/tests/e2e/other.spec.ts"],
|
|
},
|
|
],
|
|
localCandidateFlows: [candidateFlow("core-flow", "/other"), candidateFlow("other-flow", "/other")],
|
|
specs: { "client/tests/e2e/other.spec.ts": otherSpec },
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.wrong_witness_association, 1);
|
|
});
|
|
});
|
|
|
|
test("witness 字段不完整或 tag 不稳定必须阻断", () => {
|
|
withFixture({
|
|
localCandidateFlows: [candidateFlow("core-flow", "/core", {
|
|
witnessTag: "@local-candidate:wrong",
|
|
expectedH1: "",
|
|
roleFixture: "unknown",
|
|
primaryAction: { allowed: "相同", forbidden: "相同" },
|
|
})],
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.invalid_witness_tag, 1);
|
|
assert.equal(result.report.summary.gapCounts.missing_expected_h1, 1);
|
|
assert.equal(result.report.summary.gapCounts.invalid_role_fixture, 1);
|
|
assert.equal(result.report.summary.gapCounts.invalid_primary_action, 1);
|
|
});
|
|
});
|
|
|
|
test("manual/live acceptance spec 必须阻断", () => {
|
|
withFixture({
|
|
acceptanceSpecs: ["client/tests/e2e/manual/acceptance.spec.ts"],
|
|
specs: {
|
|
"client/tests/e2e/manual/acceptance.spec.ts": 'import { test } from "@playwright/test";\ntest("manual", async () => {});\n',
|
|
},
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.equal(result.report.summary.gapCounts.manual_or_live_acceptance_spec, 1);
|
|
});
|
|
});
|
|
|
|
test("候选 spec 不同路径使用同一 basename 必须阻断", () => {
|
|
withFixture({
|
|
acceptanceSpecs: ["client/tests/e2e/nested/local-candidate-witnesses.spec.ts"],
|
|
specs: {
|
|
"client/tests/e2e/nested/local-candidate-witnesses.spec.ts":
|
|
'import { test } from "@playwright/test";\ntest("acceptance", async ({ page }) => { await page.goto("/core"); });\n',
|
|
},
|
|
}, (root) => {
|
|
const result = runFixture(root);
|
|
assert.equal(result.status, 1);
|
|
assert.deepEqual(result.report.duplicateCandidateSpecBasenames, ["local-candidate-witnesses.spec.ts"]);
|
|
assert.equal(result.report.summary.gapCounts.duplicate_candidate_spec_basename, 1);
|
|
});
|
|
});
|