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.
111 lines
4.7 KiB
JavaScript
Executable File
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);
|
|
}
|
|
}
|