Files
govoplan-core/webui/scripts/run-component-tests.mjs
zemion 6d37aa527f fix(ui): unify heading help, table sizing and navigation contracts
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:16 +02:00

111 lines
4.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Compile the shared component contract once per invocation. Each invocation
// owns its output, so standalone aliases and concurrent agents cannot erase it.
import { spawn } from "node:child_process";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
export const componentSuites = Object.freeze({
"data-grid-actions": ["data-grid-actions", "data-grid-sizing"],
"dialog-focus": ["dialog-focus"],
"explorer-tree": ["explorer-tree"],
"icon-button": ["icon-button"],
"layout-primitives": ["layout-primitives"],
"mail-components": ["mail-components"],
"metric-card": ["metric-card"],
"page-layout": ["page-layout"],
"workspace-layout": ["workspace-layout"],
"people-picker": ["people-picker"],
"password-field": ["password-generator"],
"resource-access": ["resource-access-explanation"],
"action-blocker": ["action-blocker-hint"],
"documentation-help": ["documentation-help-link"],
"selection-list": ["selection-list"],
"wysiwyg-editor": ["wysiwyg-editor-utils"],
});
export function selectSuites(names) {
const selected = [...new Set(names.filter((name) => name !== "--"))];
if (!selected.length || (selected.length === 1 && selected[0] === "all")) return Object.keys(componentSuites);
for (const name of selected) {
if (!Object.hasOwn(componentSuites, name)) throw new Error(`Unknown component suite: ${name}`);
}
return selected;
}
function execute(argv, { cwd, signal }) {
if (signal?.aborted) return Promise.reject(new Error("Component tests interrupted"));
return new Promise((resolveCommand, reject) => {
const child = spawn(argv[0], argv.slice(1), { cwd, stdio: "inherit", shell: false });
let killTimer;
const abort = () => {
child.kill("SIGTERM");
killTimer = setTimeout(() => child.kill("SIGKILL"), 5000);
killTimer.unref();
};
signal?.addEventListener("abort", abort, { once: true });
child.once("error", reject);
child.once("close", (code, childSignal) => {
signal?.removeEventListener("abort", abort);
clearTimeout(killTimer);
if (code === 0 && !signal?.aborted) resolveCommand();
else reject(new Error(`Component command failed (${childSignal ?? code}): ${argv.slice(1).join(" ")}`));
});
});
}
export async function runComponentTests({
names = [],
webuiRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."),
run = execute,
compiler,
signal,
} = {}) {
const selected = selectSuites(names);
const require = createRequire(join(webuiRoot, "package.json"));
const typescript = compiler ?? require.resolve("typescript/bin/tsc");
const output = mkdtempSync(join(webuiRoot, ".component-test-build-"));
try {
writeFileSync(join(output, "package.json"), '{"type":"commonjs"}\n');
await run([process.execPath, typescript, "-p", "tsconfig.component-tests.json", "--outDir", output], { cwd: webuiRoot, signal });
// The SSR tests intentionally do not load browser CSS.
mkdirSync(join(output, "src", "components"), { recursive: true });
writeFileSync(join(output, "src", "components", "ProductAvailabilityState.css"), "module.exports = {};\n");
for (const name of selected) {
for (const test of componentSuites[name]) {
await run([process.execPath, join(output, "tests", `${test}.test.js`)], { cwd: webuiRoot, signal });
}
if (name === "dialog-focus") {
await run([process.execPath, join(webuiRoot, "scripts", "test-dialog-focus-structure.mjs")], { cwd: webuiRoot, signal });
}
}
return { suites: selected, compiled: 1 };
} finally {
// Only remove this invocation's freshly-created directory, never the
// legacy shared build or another invocation's artifacts.
rmSync(output, { recursive: true, force: true });
}
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const controller = new AbortController();
let interrupted;
const stop = (signal) => { interrupted = signal; controller.abort(); };
const onInterrupt = () => stop("SIGINT");
const onTerminate = () => stop("SIGTERM");
process.on("SIGINT", onInterrupt);
process.on("SIGTERM", onTerminate);
try {
const result = await runComponentTests({ names: process.argv.slice(2), signal: controller.signal });
process.stdout.write(`Component suites passed: ${result.suites.length}; compilations: ${result.compiled}.\n`);
} catch (error) {
process.stderr.write(`${error.message}\n`);
process.exitCode = interrupted === "SIGINT" ? 130 : interrupted === "SIGTERM" ? 143 : 1;
} finally {
process.removeListener("SIGINT", onInterrupt);
process.removeListener("SIGTERM", onTerminate);
}
}