release: WRNexusJS 0.8.0
This commit is contained in:
@@ -103,6 +103,22 @@ Remember to `await app.close()` when done.
|
||||
|
||||
## Usage
|
||||
|
||||
The CLI supports focused suites by file or directory convention:
|
||||
|
||||
```bash
|
||||
wrnexus test unit # *.unit.test.ts or test/unit/**
|
||||
wrnexus test component # *.component.test.ts or test/component/**
|
||||
wrnexus test api # *.api.test.ts or test/api/**
|
||||
wrnexus test accessibility # *.a11y.test.ts / *.accessibility.test.ts
|
||||
wrnexus test performance # *.performance.test.ts / *.benchmark.test.ts
|
||||
wrnexus test browser # Playwright project when configured
|
||||
wrnexus test visual # Playwright tests tagged @visual
|
||||
```
|
||||
|
||||
Pass the application directory after the level, for example
|
||||
`wrnexus test component examples/basic-app`. A focused command fails clearly when no matching
|
||||
suite exists instead of silently running unrelated tests.
|
||||
|
||||
```ts
|
||||
import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -190,3 +190,5 @@ export {
|
||||
MemoryCookieJar,
|
||||
} from "./advanced.ts";
|
||||
export type { TestRequestOptions, JsonResponse, Deferred, WaitForOptions } from "./advanced.ts";
|
||||
export { withDatabaseRollback, createFactory, captureBrowserArtifacts } from "./platform.ts";
|
||||
export type { TransactionalDatabase, BrowserArtifactPage } from "./platform.ts";
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
export interface TransactionalDatabase {
|
||||
tx<T>(callback: (transaction: TransactionalDatabase) => Promise<T>): Promise<T>;
|
||||
}
|
||||
|
||||
const ROLLBACK = Symbol("wrnexus-test-rollback");
|
||||
|
||||
/** Run test work in a real transaction and always force rollback. */
|
||||
export async function withDatabaseRollback<T>(
|
||||
db: TransactionalDatabase,
|
||||
run: (transaction: TransactionalDatabase) => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
let output!: T;
|
||||
try {
|
||||
await db.tx(async (transaction) => {
|
||||
output = await run(transaction);
|
||||
throw ROLLBACK;
|
||||
});
|
||||
} catch (error) {
|
||||
if (error !== ROLLBACK) throw error;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function createFactory<T extends Record<string, unknown>>(build: (sequence: number) => T) {
|
||||
let sequence = 0;
|
||||
return {
|
||||
build(overrides: Partial<T> = {}): T {
|
||||
return { ...build(++sequence), ...overrides };
|
||||
},
|
||||
buildMany(count: number, overrides: Partial<T> = {}): T[] {
|
||||
if (!Number.isInteger(count) || count < 0 || count > 10_000)
|
||||
throw new RangeError("Factory count must be between 0 and 10000");
|
||||
return Array.from({ length: count }, () => ({ ...build(++sequence), ...overrides }));
|
||||
},
|
||||
reset(): void {
|
||||
sequence = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface BrowserArtifactPage {
|
||||
screenshot(options: { path: string; fullPage?: boolean }): Promise<unknown>;
|
||||
context(): { tracing?: { stop(options: { path: string }): Promise<unknown> } };
|
||||
}
|
||||
|
||||
export async function captureBrowserArtifacts(
|
||||
page: BrowserArtifactPage,
|
||||
testName: string,
|
||||
options: { root?: string; screenshot?: boolean; trace?: boolean } = {},
|
||||
): Promise<{ screenshot?: string; trace?: string }> {
|
||||
const safe = testName.replace(/[^A-Za-z0-9_.-]+/g, "-").slice(0, 120) || "test";
|
||||
const root = resolve(options.root ?? join("test-results", "wrnexus"));
|
||||
mkdirSync(root, { recursive: true });
|
||||
const output: { screenshot?: string; trace?: string } = {};
|
||||
if (options.screenshot !== false) {
|
||||
output.screenshot = join(root, `${safe}.png`);
|
||||
await page.screenshot({ path: output.screenshot, fullPage: true });
|
||||
}
|
||||
if (options.trace && page.context().tracing) {
|
||||
output.trace = join(root, `${safe}.zip`);
|
||||
await page.context().tracing!.stop({ path: output.trace });
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { captureBrowserArtifacts, createFactory, withDatabaseRollback } from "../src/index.ts";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
test("factories are deterministic, bounded and override-safe", () => {
|
||||
const users = createFactory((sequence) => ({
|
||||
id: sequence,
|
||||
email: `user${sequence}@test.local`,
|
||||
}));
|
||||
expect(users.build().id).toBe(1);
|
||||
expect(users.build({ email: "custom@test.local" }).email).toBe("custom@test.local");
|
||||
users.reset();
|
||||
expect(users.buildMany(2).map((user) => user.id)).toEqual([1, 2]);
|
||||
expect(() => users.buildMany(10_001)).toThrow();
|
||||
});
|
||||
|
||||
test("database isolation always rolls back while returning the test value", async () => {
|
||||
let rolledBack = false;
|
||||
const db = {
|
||||
async tx<T>(run: (transaction: any) => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await run(db);
|
||||
} catch (error) {
|
||||
rolledBack = true;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
expect(await withDatabaseRollback(db, async () => 42)).toBe(42);
|
||||
expect(rolledBack).toBeTrue();
|
||||
});
|
||||
|
||||
test("browser artifact helper writes safe screenshot and trace paths", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-browser-artifacts-"));
|
||||
const calls: string[] = [];
|
||||
try {
|
||||
const result = await captureBrowserArtifacts(
|
||||
{
|
||||
async screenshot({ path }) {
|
||||
calls.push(path);
|
||||
},
|
||||
context() {
|
||||
return {
|
||||
tracing: {
|
||||
async stop({ path }) {
|
||||
calls.push(path);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
"user / unsafe name",
|
||||
{ root, trace: true },
|
||||
);
|
||||
expect(result.screenshot).toEndWith("user-unsafe-name.png");
|
||||
expect(result.trace).toEndWith("user-unsafe-name.zip");
|
||||
expect(calls).toHaveLength(2);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user