Files
WRNexusJS/packages/cli/src/test.ts
T

179 lines
5.6 KiB
TypeScript

/** Level-aware `wrnexus test` runner with Bun and optional Playwright backends. */
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { spawn, type ChildProcess } from "node:child_process";
import { join, relative, resolve } from "node:path";
export const TEST_LEVELS = [
"unit",
"component",
"api",
"browser",
"visual",
"accessibility",
"performance",
] as const;
export type TestLevel = (typeof TEST_LEVELS)[number];
export interface TestCommandPlan {
command: string;
args: string[];
cwd: string;
level?: TestLevel;
files: string[];
setup?: { command: string; args: string[] };
env?: Record<string, string>;
}
function shardFiles(files: string[], value?: string): string[] {
if (!value) return files;
const match = /^(\d+)\/(\d+)$/.exec(value);
if (!match) throw new Error("WRN-TEST-SHARD: expected --shard=<index>/<total>");
const index = Number(match[1]);
const total = Number(match[2]);
if (index < 1 || total < 1 || index > total)
throw new Error("WRN-TEST-SHARD: index must be between 1 and total");
return files.filter((_file, position) => position % total === index - 1);
}
function testFiles(root: string): string[] {
const files: string[] = [];
const walk = (directory: string): void => {
if (!existsSync(directory)) return;
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (["node_modules", "dist", ".wrnexus", "coverage"].includes(entry.name)) continue;
const path = join(directory, entry.name);
if (entry.isDirectory()) walk(path);
else if (/\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(entry.name)) files.push(path);
}
};
for (const directory of ["app", "test", "tests"]) walk(join(root, directory));
return files.sort();
}
function hasPlaywright(root: string): boolean {
if (
["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"].some((file) =>
existsSync(join(root, file)),
)
)
return true;
try {
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
return Boolean(
manifest.dependencies?.["@playwright/test"] || manifest.devDependencies?.["@playwright/test"],
);
} catch {
return false;
}
}
export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan {
const root = resolve(appRoot);
const level = args.find((value): value is TestLevel => TEST_LEVELS.includes(value as TestLevel));
const watch = args.includes("--watch");
const profile = args.find((value) => value.startsWith("--profile="))?.slice(10) || "test";
const setupSource = join(import.meta.dir, "test-setup.ts");
const setupModule = existsSync(setupSource)
? setupSource
: join(import.meta.dir, "test-setup.js");
const shard = args.find((value) => value.startsWith("--shard="))?.slice(8);
const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
.split(",")
.filter(Boolean);
const passthrough = args.filter(
(value) =>
!value.startsWith("--profile=") &&
value !== "--watch" &&
value !== appRoot &&
value !== level &&
!value.startsWith("--browsers=") &&
!value.startsWith("--shard=") &&
value !== "--install-browsers",
);
if ((level === "browser" || level === "visual") && hasPlaywright(root)) {
return {
command: process.execPath,
args: [
"x",
"playwright",
"test",
...(level === "visual" ? ["--grep", "@visual"] : []),
...browsers.flatMap((browser) => ["--project", browser]),
...(shard ? [`--shard=${shard}`] : []),
"--reporter=line,html",
...passthrough,
],
cwd: root,
level,
files: [],
...(args.includes("--install-browsers")
? {
setup: { command: process.execPath, args: ["x", "playwright", "install", ...browsers] },
}
: {}),
};
}
const pattern =
level === "accessibility"
? /(?:accessibility|a11y)/i
: level === "performance"
? /(?:performance|benchmark)/i
: level
? new RegExp(level, "i")
: null;
const files = shardFiles(
pattern ? testFiles(root).filter((file) => pattern.test(relative(root, file))) : [],
shard,
);
return {
command: process.execPath,
args: [
"test",
"--preload",
setupModule,
...(watch ? ["--watch"] : []),
...(pattern ? files : []),
...passthrough,
],
cwd: root,
level,
files,
env: { WRNEXUS_TEST_ROOT: root, WRNEXUS_PROFILE: profile },
};
}
export function runTests(appRoot: string, args: string[]): ChildProcess | null {
const plan = createTestPlan(appRoot, args);
if (plan.level && !plan.files.length && !plan.args.includes("playwright")) {
console.error(
`WRN-TEST-NO-FILES: no ${plan.level} tests found. Name a file or directory with '${plan.level}' under app/, test/, or tests/.`,
);
process.exitCode = 1;
return null;
}
const launch = () =>
spawn(plan.command, plan.args, {
stdio: "inherit",
cwd: plan.cwd,
env: { ...process.env, ...plan.env },
});
if (plan.setup) {
const setup = spawn(plan.setup.command, plan.setup.args, { stdio: "inherit", cwd: plan.cwd });
setup.on("exit", (code, signal) => {
if (signal || code !== 0) {
process.exitCode = code ?? 1;
return;
}
const test = launch();
test.on("exit", (testCode, testSignal) => {
if (!testSignal) process.exitCode = testCode ?? 0;
});
});
return setup;
}
const child = launch();
child.on("exit", (code, signal) => {
if (!signal) process.exitCode = code ?? 0;
});
return child;
}