#!/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); } }