release: WRNexusJS 0.8.0
This commit is contained in:
+157
-24
@@ -1,26 +1,159 @@
|
||||
/**
|
||||
* `wrnexus test [app-dir] [--watch] [--profile=test]` — run the app's test files
|
||||
* with `bun test`. Defaults to the `test` profile (config + .env.test). Extra
|
||||
* args after `--` (or bun test flags) pass straight through.
|
||||
*/
|
||||
/** 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";
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
export function runTests(appRoot: string, args: string[]): void {
|
||||
const root = resolve(appRoot);
|
||||
const watch = args.includes("--watch");
|
||||
const passthrough = args.filter(
|
||||
(a) => !a.startsWith("--profile=") && a !== "--watch" && a !== appRoot,
|
||||
);
|
||||
|
||||
const child = spawn(
|
||||
process.execPath, // the Bun binary
|
||||
["test", ...(watch ? ["--watch"] : []), ...passthrough],
|
||||
{ stdio: "inherit", cwd: root },
|
||||
);
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) return;
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
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[] };
|
||||
}
|
||||
|
||||
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 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", ...(watch ? ["--watch"] : []), ...(pattern ? files : []), ...passthrough],
|
||||
cwd: root,
|
||||
level,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user