9bd3697a17
Generic URL parity harness (table-driven + property-based roundtrip) and SEO parity harness (shape/completeness validation) registered for Online Board. 170 tests covering all 6 route types with 200-iteration fast-check fuzz runs ensuring no serialization asymmetry.
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
/**
|
|
* Generic URL parity test harness.
|
|
*
|
|
* Features register their URL serializer/parser against this harness.
|
|
* The harness runs:
|
|
* 1. Table-driven fixture tests (parse + build parity against corpus).
|
|
* 2. fast-check property-based fuzz tests (roundtrip: parse(build(x)) === x).
|
|
*
|
|
* Designed for reuse across Phase 3+ features.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import * as fc from "fast-check";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public config contract
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface UrlParityConfig<TQuery> {
|
|
/** Feature name (used in describe block labels). */
|
|
feature: string;
|
|
|
|
/** Path to the JSON fixture file relative to the project root. */
|
|
fixturePath: string;
|
|
|
|
/** Parse a raw URL string into the typed query object. Returns null on failure. */
|
|
parse(raw: string): TQuery | null;
|
|
|
|
/** Build a URL string from a typed query object. */
|
|
build(query: TQuery): string;
|
|
|
|
/** fast-check arbitrary for generating random valid query objects. */
|
|
fuzzArbitrary: fc.Arbitrary<TQuery>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fixture shape
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface FixtureEntry<T> {
|
|
url: string;
|
|
expected: T;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Harness entry point
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Defines a full URL parity test suite for a feature.
|
|
*
|
|
* Call this from a `*.test.ts` file — it registers `describe` / `it` blocks
|
|
* via Vitest globals.
|
|
*/
|
|
export function defineUrlParityTests<T>(config: UrlParityConfig<T>): void {
|
|
const { feature, fixturePath, parse, build, fuzzArbitrary } = config;
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Load fixtures
|
|
// -------------------------------------------------------------------------
|
|
|
|
const fixtureAbsPath = resolve(process.cwd(), fixturePath);
|
|
const raw = readFileSync(fixtureAbsPath, "utf-8");
|
|
const fixtures = JSON.parse(raw) as FixtureEntry<T>[];
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 1. Table-driven fixture tests
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe(`[${feature}] URL parity — fixture corpus (${fixtures.length} entries)`, () => {
|
|
for (const entry of fixtures) {
|
|
it(`parse("${entry.url}") matches expected`, () => {
|
|
const parsed = parse(entry.url);
|
|
expect(parsed).toEqual(entry.expected);
|
|
});
|
|
|
|
it(`build(expected) === "${entry.url}"`, () => {
|
|
const built = build(entry.expected);
|
|
expect(built).toBe(entry.url);
|
|
});
|
|
|
|
it(`roundtrip: build(parse("${entry.url}")) === "${entry.url}"`, () => {
|
|
const parsed = parse(entry.url);
|
|
expect(parsed).not.toBeNull();
|
|
if (parsed === null) return;
|
|
const rebuilt = build(parsed);
|
|
expect(rebuilt).toBe(entry.url);
|
|
});
|
|
}
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 2. fast-check fuzz roundtrip tests
|
|
// -------------------------------------------------------------------------
|
|
|
|
describe(`[${feature}] URL parity — fast-check fuzz roundtrip`, () => {
|
|
it("parse(build(params)) deep-equals params for all generated inputs", () => {
|
|
fc.assert(
|
|
fc.property(fuzzArbitrary, (params) => {
|
|
const url = build(params);
|
|
const parsed = parse(url);
|
|
expect(parsed).toEqual(params);
|
|
}),
|
|
{ numRuns: 200 },
|
|
);
|
|
});
|
|
|
|
it("build(parse(build(params))) === build(params) for all generated inputs", () => {
|
|
fc.assert(
|
|
fc.property(fuzzArbitrary, (params) => {
|
|
const url = build(params);
|
|
const parsed = parse(url);
|
|
expect(parsed).not.toBeNull();
|
|
if (parsed === null) return;
|
|
const rebuilt = build(parsed);
|
|
expect(rebuilt).toBe(url);
|
|
}),
|
|
{ numRuns: 200 },
|
|
);
|
|
});
|
|
});
|
|
}
|