168 lines
5.1 KiB
JavaScript
168 lines
5.1 KiB
JavaScript
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
/** @param {string} name @param {string} fallback */
|
|
function argumentValue(name, fallback) {
|
|
const index = process.argv.indexOf(name);
|
|
return index >= 0 && process.argv[index + 1]
|
|
? process.argv[index + 1]
|
|
: fallback;
|
|
}
|
|
|
|
const sourceRoot = argumentValue("--source-root", "tests");
|
|
const artifactPath = argumentValue(
|
|
"--artifact",
|
|
"artifacts/quality/test-evidence.json",
|
|
);
|
|
const fixtureMode = sourceRoot !== "tests";
|
|
const failures = [];
|
|
const facts = {
|
|
scannedFiles: 0,
|
|
visualBaselines: 0,
|
|
sharedScenarios: 0,
|
|
};
|
|
|
|
/** @param {string} target @returns {Promise<string[]>} */
|
|
async function filesBelow(target) {
|
|
try {
|
|
const metadata = await stat(target);
|
|
if (metadata.isFile()) return [target];
|
|
const entries = await readdir(target, { withFileTypes: true });
|
|
const groups = await Promise.all(
|
|
entries.map((entry) => filesBelow(path.join(target, entry.name))),
|
|
);
|
|
return groups.flat();
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
const sourceFiles = (await filesBelow(sourceRoot)).filter(
|
|
(file) => fixtureMode || !file.split(path.sep).includes("fixtures"),
|
|
);
|
|
for (const file of sourceFiles) {
|
|
if (!/\.(?:js|jsx|mjs|ts|tsx|fixture|txt)$/.test(file)) continue;
|
|
const source = await readFile(file, "utf8");
|
|
facts.scannedFiles += 1;
|
|
const skipPattern =
|
|
/\b(?:test|it|describe)(?:\.describe)?\.(?:skip|fixme)\s*\(/g;
|
|
if (skipPattern.test(source)) {
|
|
const quarantine =
|
|
/quarantine\(owner=[^)]+,\s*defect=[^)]+,\s*expires=\d{4}-\d{2}-\d{2}\)/;
|
|
if (!quarantine.test(source)) {
|
|
failures.push(`${file}: skip/fixme lacks owned expiring quarantine`);
|
|
}
|
|
}
|
|
const wholeUiMask =
|
|
/\bmask\s*:\s*\[[\s\S]{0,240}(?:locator|getByRole)\s*\(\s*["'](?:html|body|main|application|document)["']/i;
|
|
if (wholeUiMask.test(source)) {
|
|
failures.push(`${file}: screenshot mask may not cover the whole UI`);
|
|
}
|
|
}
|
|
|
|
if (!fixtureMode) {
|
|
const e2eConfig = await readFile("playwright.config.js", "utf8");
|
|
for (const token of [
|
|
"pnpm build",
|
|
"pnpm preview",
|
|
"reuseExistingServer: false",
|
|
'"junit"',
|
|
'trace: "retain-on-failure"',
|
|
'"chromium-compact"',
|
|
'"firefox"',
|
|
'"webkit"',
|
|
]) {
|
|
if (!e2eConfig.includes(token)) {
|
|
failures.push(`playwright.config.js missing release evidence token ${token}`);
|
|
}
|
|
}
|
|
|
|
const e2eFiles = (await filesBelow("tests/e2e")).filter((file) =>
|
|
/\.spec\.(?:js|ts)$/.test(file),
|
|
);
|
|
for (const file of e2eFiles) {
|
|
const source = await readFile(file, "utf8");
|
|
if (!source.includes("support/browser/strict-browser-test")) {
|
|
failures.push(`${file}: bypasses strict browser fixture`);
|
|
}
|
|
}
|
|
|
|
const scenarioCatalog = await readFile(
|
|
"tests/mocks/scenarios/catalog.ts",
|
|
"utf8",
|
|
);
|
|
const scenarioIdBlock =
|
|
scenarioCatalog.match(
|
|
/HTTP_SCENARIO_IDS\s*=\s*Object\.freeze\(\[([\s\S]*?)\]\s*as const\)/,
|
|
)?.[1] ?? "";
|
|
facts.sharedScenarios = (scenarioIdBlock.match(/"[^"]+"/g) ?? []).length;
|
|
if (facts.sharedScenarios < 19) {
|
|
failures.push("shared MSW catalog must retain all 19 failure scenarios");
|
|
}
|
|
const handler = await readFile(
|
|
"tests/mocks/handlers/reference-resources.ts",
|
|
"utf8",
|
|
);
|
|
if (
|
|
!handler.includes("assertOperationScenario") ||
|
|
!handler.includes("../scenarios/catalog.js")
|
|
) {
|
|
failures.push("MSW handler bypasses shared scenario catalog");
|
|
}
|
|
|
|
const baselineFiles = (await filesBelow("tests/visual/__snapshots__")).filter(
|
|
(file) => file.endsWith(".png"),
|
|
);
|
|
facts.visualBaselines = baselineFiles.length;
|
|
if (facts.visualBaselines < 4) {
|
|
failures.push("visual baseline requires at least four risk surfaces");
|
|
}
|
|
for (const required of [
|
|
"playwright.storybook.config.js",
|
|
"playwright.visual.config.js",
|
|
"tests/storybook/workshop.spec.ts",
|
|
"artifacts/tests/storybook/results.xml",
|
|
"artifacts/tests/visual/results.xml",
|
|
]) {
|
|
if ((await filesBelow(required)).length === 0) {
|
|
failures.push(`test evidence missing ${required}`);
|
|
}
|
|
}
|
|
|
|
const requiredBuiltFiles = [
|
|
"dist/index.html",
|
|
"dist/config.json",
|
|
"dist/release-manifest.json",
|
|
"dist/runtime-config.schema.json",
|
|
"dist/.vite/manifest.json",
|
|
];
|
|
for (const required of requiredBuiltFiles) {
|
|
if ((await filesBelow(required)).length === 0) {
|
|
failures.push(`built-dist contract missing ${required}`);
|
|
}
|
|
}
|
|
const sourceMaps = (await filesBelow("dist")).filter((file) =>
|
|
file.endsWith(".map"),
|
|
);
|
|
if (sourceMaps.length > 0) {
|
|
failures.push(`production dist contains source maps: ${sourceMaps.join(", ")}`);
|
|
}
|
|
}
|
|
|
|
const report = {
|
|
schemaVersion: 1,
|
|
sourceRoot,
|
|
status: failures.length === 0 ? "PASS" : "FAIL",
|
|
facts,
|
|
failures,
|
|
};
|
|
await mkdir(path.dirname(artifactPath), { recursive: true });
|
|
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
if (failures.length > 0) {
|
|
process.stderr.write(`Test evidence failed:\n- ${failures.join("\n- ")}\n`);
|
|
process.exit(1);
|
|
}
|
|
process.stdout.write(
|
|
`Test evidence: PASS (${facts.scannedFiles} files, ${facts.visualBaselines} baselines, ${facts.sharedScenarios} scenarios)\n`,
|
|
);
|