diff --git a/package.json b/package.json index 17af69e9..055c8aad 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "eslint": "^9.0.0", "eslint-plugin-boundaries": "^5.0.0", "eslint-plugin-unused-imports": "^4.0.0", + "fast-check": "^4.6.0", "jsdom": "^29.0.2", "react-test-renderer": "^19.2.5", "typescript": "^5.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ecc5a23..3a606e64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: eslint-plugin-unused-imports: specifier: ^4.0.0 version: 4.4.1(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + fast-check: + specifier: ^4.6.0 + version: 4.6.0 jsdom: specifier: ^29.0.2 version: 29.0.2 @@ -3863,6 +3866,10 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + fast-check@4.6.0: + resolution: {integrity: sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==} + engines: {node: '>=12.17.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -5308,6 +5315,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@8.4.0: + resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} + qs@6.15.1: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} @@ -11060,6 +11070,10 @@ snapshots: expect-type@1.3.0: {} + fast-check@4.6.0: + dependencies: + pure-rand: 8.4.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -12545,6 +12559,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@8.4.0: {} + qs@6.15.1: dependencies: side-channel: 1.1.0 diff --git a/tests/fixtures/phase-2/url-corpus/onlineboard.json b/tests/fixtures/phase-2/url-corpus/onlineboard.json new file mode 100644 index 00000000..e9bd860c --- /dev/null +++ b/tests/fixtures/phase-2/url-corpus/onlineboard.json @@ -0,0 +1,82 @@ +[ + { + "url": "onlineboard", + "expected": { "type": "start" } + }, + { + "url": "onlineboard/flight/SU0100-20250115", + "expected": { "type": "flight", "carrier": "SU", "flightNumber": "0100", "date": "20250115" } + }, + { + "url": "onlineboard/flight/SU0100D-20250115", + "expected": { "type": "flight", "carrier": "SU", "flightNumber": "0100", "suffix": "D", "date": "20250115" } + }, + { + "url": "onlineboard/flight/SU0001-20260301", + "expected": { "type": "flight", "carrier": "SU", "flightNumber": "0001", "date": "20260301" } + }, + { + "url": "onlineboard/flight/SUA0100-20250115", + "expected": { "type": "flight", "carrier": "SUA", "flightNumber": "0100", "date": "20250115" } + }, + { + "url": "onlineboard/flight/AF1234-20250601", + "expected": { "type": "flight", "carrier": "AF", "flightNumber": "1234", "date": "20250601" } + }, + { + "url": "onlineboard/departure/SVO-20250115", + "expected": { "type": "departure", "station": "SVO", "date": "20250115" } + }, + { + "url": "onlineboard/departure/SVO-20250115-08001800", + "expected": { "type": "departure", "station": "SVO", "date": "20250115", "timeFrom": "0800", "timeTo": "1800" } + }, + { + "url": "onlineboard/departure/LED-20260115", + "expected": { "type": "departure", "station": "LED", "date": "20260115" } + }, + { + "url": "onlineboard/departure/JFK-20250301-06002359", + "expected": { "type": "departure", "station": "JFK", "date": "20250301", "timeFrom": "0600", "timeTo": "2359" } + }, + { + "url": "onlineboard/arrival/LED-20250115", + "expected": { "type": "arrival", "station": "LED", "date": "20250115" } + }, + { + "url": "onlineboard/arrival/JFK-20250115-08001800", + "expected": { "type": "arrival", "station": "JFK", "date": "20250115", "timeFrom": "0800", "timeTo": "1800" } + }, + { + "url": "onlineboard/arrival/SVO-20260701", + "expected": { "type": "arrival", "station": "SVO", "date": "20260701" } + }, + { + "url": "onlineboard/route/SVO-LED-20250115", + "expected": { "type": "route", "departure": "SVO", "arrival": "LED", "date": "20250115" } + }, + { + "url": "onlineboard/route/SVO-JFK-20250115-08001800", + "expected": { "type": "route", "departure": "SVO", "arrival": "JFK", "date": "20250115", "timeFrom": "0800", "timeTo": "1800" } + }, + { + "url": "onlineboard/route/LED-SVO-20260301", + "expected": { "type": "route", "departure": "LED", "arrival": "SVO", "date": "20260301" } + }, + { + "url": "onlineboard/route/DME-AER-20250801-00002359", + "expected": { "type": "route", "departure": "DME", "arrival": "AER", "date": "20250801", "timeFrom": "0000", "timeTo": "2359" } + }, + { + "url": "onlineboard/SU0100-20250115", + "expected": { "type": "details", "carrier": "SU", "flightNumber": "0100", "date": "20250115" } + }, + { + "url": "onlineboard/SU0100D-20250115", + "expected": { "type": "details", "carrier": "SU", "flightNumber": "0100", "suffix": "D", "date": "20250115" } + }, + { + "url": "onlineboard/AF1234-20250601", + "expected": { "type": "details", "carrier": "AF", "flightNumber": "1234", "date": "20250601" } + } +] diff --git a/tests/parity/seo/harness.ts b/tests/parity/seo/harness.ts new file mode 100644 index 00000000..3fc04d91 --- /dev/null +++ b/tests/parity/seo/harness.ts @@ -0,0 +1,140 @@ +/** + * Generic SEO parity test harness. + * + * Features register their SEO builder functions against this harness. + * The harness asserts that every builder produces a complete `SeoHeadProps` + * with all required fields: + * - title (non-empty string) + * - description (non-empty string) + * - canonical (valid URL) + * - hreflang (10 entries: 9 languages + x-default) + * - og (all required fields present) + * - twitter (card field present) + * + * Designed for reuse across Phase 3+ features. + * + * @module + */ + +import { describe, it, expect } from "vitest"; +import type { SeoHeadProps } from "@/ui/seo/SeoHead.js"; +import type { Language } from "@/i18n/resolver.js"; + +// --------------------------------------------------------------------------- +// Public config contract +// --------------------------------------------------------------------------- + +export interface SeoParityEntry { + /** Human-readable label for the test (e.g. "start page", "flight search"). */ + label: string; + + /** Call the SEO builder and return the result. */ + render(lang: Language): SeoHeadProps; +} + +export interface SeoParityConfig { + /** Feature slug (used in describe block labels). */ + slug: string; + + /** The entries to test. */ + entries: SeoParityEntry[]; + + /** Languages to test each entry against. Defaults to ["ru", "en"]. */ + languages?: Language[]; +} + +// --------------------------------------------------------------------------- +// Expected hreflang count +// --------------------------------------------------------------------------- + +const EXPECTED_HREFLANG_COUNT = 10; // 9 languages + x-default + +// --------------------------------------------------------------------------- +// Harness entry point +// --------------------------------------------------------------------------- + +/** + * Defines a full SEO parity test suite for a feature. + * + * Call this from a `*.test.ts` file — it registers `describe` / `it` blocks + * via Vitest globals. + */ +export function defineSeoParityTests(config: SeoParityConfig): void { + const { slug, entries, languages = ["ru", "en"] } = config; + + describe(`[${slug}] SEO parity`, () => { + for (const entry of entries) { + for (const lang of languages) { + describe(`${entry.label} (${lang})`, () => { + let result: SeoHeadProps; + + // Build once per describe block + // Using a beforeAll-style pattern via lazy init + function getResult(): SeoHeadProps { + if (!result) { + result = entry.render(lang); + } + return result; + } + + it("has a non-empty title", () => { + const r = getResult(); + expect(r.title).toBeTruthy(); + expect(typeof r.title).toBe("string"); + }); + + it("has a non-empty description", () => { + const r = getResult(); + expect(r.description).toBeTruthy(); + expect(typeof r.description).toBe("string"); + }); + + it("has a canonical URL containing the locale", () => { + const r = getResult(); + expect(r.canonical).toBeTruthy(); + expect(r.canonical).toContain(`/${lang}/`); + }); + + it(`has ${EXPECTED_HREFLANG_COUNT} hreflang entries`, () => { + const r = getResult(); + expect(r.hreflang).toHaveLength(EXPECTED_HREFLANG_COUNT); + }); + + it("has x-default in hreflang entries", () => { + const r = getResult(); + const xDefault = r.hreflang.find((h) => h.lang === "x-default"); + expect(xDefault).toBeDefined(); + expect(xDefault?.href).toBeTruthy(); + }); + + it("has all required OG fields", () => { + const r = getResult(); + expect(r.og.title).toBeTruthy(); + expect(r.og.description).toBeTruthy(); + expect(r.og.url).toBeTruthy(); + expect(r.og.image).toBeTruthy(); + expect(["website", "article"]).toContain(r.og.type); + expect(r.og.locale).toBe(lang); + expect(r.og.siteName).toBeTruthy(); + }); + + it("has og.url matching canonical", () => { + const r = getResult(); + expect(r.og.url).toBe(r.canonical); + }); + + it("has og.title matching title", () => { + const r = getResult(); + expect(r.og.title).toBe(r.title); + }); + + it("has twitter card defined", () => { + const r = getResult(); + expect(r.twitter).toBeDefined(); + expect(r.twitter?.card).toBeTruthy(); + }); + }); + } + } + }); +} diff --git a/tests/parity/seo/onlineboard.test.ts b/tests/parity/seo/onlineboard.test.ts new file mode 100644 index 00000000..3942d07a --- /dev/null +++ b/tests/parity/seo/onlineboard.test.ts @@ -0,0 +1,164 @@ +/** + * Online Board SEO parity tests. + * + * Registers all 6 Online Board SEO builder functions against the generic + * SEO parity harness. Tests shape/completeness for ru and en locales. + * + * @module + */ + +import { + buildOnlineBoardStartSeo, + buildFlightSearchSeo, + buildDepartureSearchSeo, + buildArrivalSearchSeo, + buildRouteSearchSeo, + buildFlightDetailsSeo, +} from "@/features/online-board/seo.js"; +import type { ISimpleFlight } from "@/features/online-board/types.js"; +import type { Language } from "@/i18n/resolver.js"; +import { defineSeoParityTests } from "./harness.js"; + +// --------------------------------------------------------------------------- +// Stub translation function +// --------------------------------------------------------------------------- + +function stubT(key: string, opts?: Record): string { + if (opts && Object.keys(opts).length > 0) { + const vars = Object.entries(opts) + .map(([k, v]) => `${k}=${String(v)}`) + .join(","); + return `${key}|${vars}`; + } + return key; +} + +const CANONICAL_ORIGIN = "https://www.aeroflot.ru"; + +// --------------------------------------------------------------------------- +// Test flight fixture for details page +// --------------------------------------------------------------------------- + +const testFlight: ISimpleFlight = { + id: "SU100-20250115", + routeType: "Direct", + flightId: { + carrier: "SU", + flightNumber: "0100", + suffix: "", + date: "20250115", + }, + flyingTime: "10h 30m", + operatingBy: {}, + status: "Scheduled", + leg: { + index: 0, + status: "Scheduled", + flyingTime: "10h 30m", + updated: "2025-01-15T10:00:00Z", + dayChange: 0, + equipment: { name: "Boeing 777-300ER", code: "773" }, + flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false }, + operatingBy: {}, + departure: { + scheduled: { + airport: "Sheremetyevo International Airport", + airportCode: "SVO", + city: "Moscow", + cityCode: "MOW", + countryCode: "RU", + }, + checkingStatus: "closed", + times: { + scheduledDeparture: { + dayChange: { value: 0, title: "" }, + local: "2025-01-15T10:00:00", + localTime: "10:00", + tzOffset: 3, + utc: "2025-01-15T07:00:00Z", + }, + }, + }, + arrival: { + scheduled: { + airport: "John F. Kennedy International Airport", + airportCode: "JFK", + city: "New York", + cityCode: "NYC", + countryCode: "US", + }, + times: { + scheduledArrival: { + dayChange: { value: 0, title: "" }, + local: "2025-01-15T14:30:00", + localTime: "14:30", + tzOffset: -5, + utc: "2025-01-15T19:30:00Z", + }, + }, + }, + }, +}; + +// --------------------------------------------------------------------------- +// Register against harness +// --------------------------------------------------------------------------- + +defineSeoParityTests({ + slug: "OnlineBoard", + entries: [ + { + label: "start page", + render: (lang: Language) => + buildOnlineBoardStartSeo(stubT, lang, CANONICAL_ORIGIN), + }, + { + label: "flight search", + render: (lang: Language) => + buildFlightSearchSeo( + stubT, + { type: "flight", carrier: "SU", flightNumber: "0100", date: "20250115" }, + lang, + CANONICAL_ORIGIN, + ), + }, + { + label: "departure search", + render: (lang: Language) => + buildDepartureSearchSeo( + stubT, + { type: "departure", station: "SVO", date: "20250115" }, + lang, + CANONICAL_ORIGIN, + { departure: "Moscow" }, + ), + }, + { + label: "arrival search", + render: (lang: Language) => + buildArrivalSearchSeo( + stubT, + { type: "arrival", station: "JFK", date: "20250115" }, + lang, + CANONICAL_ORIGIN, + { arrival: "New York" }, + ), + }, + { + label: "route search", + render: (lang: Language) => + buildRouteSearchSeo( + stubT, + { type: "route", departure: "SVO", arrival: "JFK", date: "20250115" }, + lang, + CANONICAL_ORIGIN, + { departure: "Moscow", arrival: "New York" }, + ), + }, + { + label: "flight details", + render: (lang: Language) => + buildFlightDetailsSeo(stubT, testFlight, lang, CANONICAL_ORIGIN), + }, + ], +}); diff --git a/tests/parity/url/harness.ts b/tests/parity/url/harness.ts new file mode 100644 index 00000000..f67db4e6 --- /dev/null +++ b/tests/parity/url/harness.ts @@ -0,0 +1,126 @@ +/** + * 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 { + /** 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; +} + +// --------------------------------------------------------------------------- +// Fixture shape +// --------------------------------------------------------------------------- + +interface FixtureEntry { + 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(config: UrlParityConfig): 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[]; + + // ------------------------------------------------------------------------- + // 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 }, + ); + }); + }); +} diff --git a/tests/parity/url/onlineboard.test.ts b/tests/parity/url/onlineboard.test.ts new file mode 100644 index 00000000..91c1fc52 --- /dev/null +++ b/tests/parity/url/onlineboard.test.ts @@ -0,0 +1,177 @@ +/** + * Online Board URL parity tests. + * + * Registers the Online Board URL serializer against the generic URL parity + * harness. Tests fixture corpus + fast-check fuzz roundtrip. + * + * @module + */ + +import * as fc from "fast-check"; +import { parseOnlineBoardUrl, buildOnlineBoardUrl } from "@/features/online-board/url.js"; +import type { OnlineBoardParams } from "@/features/online-board/url.js"; +import { defineUrlParityTests } from "./harness.js"; + +// --------------------------------------------------------------------------- +// fast-check arbitraries for OnlineBoardParams +// --------------------------------------------------------------------------- + +const ALPHA_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("") as [string, ...string[]]; + +/** Single uppercase letter arbitrary */ +const alphaCharArb: fc.Arbitrary = fc.constantFrom(...ALPHA_CHARS); + +/** 2-char uppercase carrier code (standard IATA) */ +const carrierArb: fc.Arbitrary = fc + .tuple(alphaCharArb, alphaCharArb) + .map(([a, b]) => `${a}${b}`); + +/** 3-char uppercase IATA station code */ +const stationArb: fc.Arbitrary = fc + .tuple(alphaCharArb, alphaCharArb, alphaCharArb) + .map(([a, b, c]) => `${a}${b}${c}`); + +/** Valid yyyyMMdd date string in a reasonable range */ +const dateArb = fc + .record({ + year: fc.integer({ min: 2020, max: 2030 }), + month: fc.integer({ min: 1, max: 12 }), + day: fc.integer({ min: 1, max: 28 }), // avoid invalid day-of-month + }) + .map(({ year, month, day }) => { + const m = String(month).padStart(2, "0"); + const d = String(day).padStart(2, "0"); + return `${year}${m}${d}`; + }); + +/** + * 4-digit zero-padded flight number. + * + * The URL serializer pads to 4 digits on build, so the roundtrip-stable + * representation is always 4 digits. + */ +const flightNumberArb = fc + .integer({ min: 1, max: 9999 }) + .map((n) => String(n).padStart(4, "0")); + +/** Optional time range: 4-digit HHmm strings */ +const timeArb = fc + .record({ + hour: fc.integer({ min: 0, max: 23 }), + minute: fc.integer({ min: 0, max: 59 }), + }) + .map(({ hour, minute }) => { + return `${String(hour).padStart(2, "0")}${String(minute).padStart(2, "0")}`; + }); + +/** Optional suffix: single uppercase letter or absent */ +const suffixArb: fc.Arbitrary = fc.constantFrom(...ALPHA_CHARS); + +// --------------------------------------------------------------------------- +// Discriminated union arbitraries +// --------------------------------------------------------------------------- + +const startArb: fc.Arbitrary = fc.constant({ type: "start" as const }); + +const flightArb: fc.Arbitrary = fc.oneof( + // Without suffix + fc.record({ + type: fc.constant("flight" as const), + carrier: carrierArb, + flightNumber: flightNumberArb, + date: dateArb, + }), + // With suffix + fc.record({ + type: fc.constant("flight" as const), + carrier: carrierArb, + flightNumber: flightNumberArb, + suffix: suffixArb, + date: dateArb, + }), +); + +const departureArb: fc.Arbitrary = fc.oneof( + fc.record({ + type: fc.constant("departure" as const), + station: stationArb, + date: dateArb, + }), + fc.record({ + type: fc.constant("departure" as const), + station: stationArb, + date: dateArb, + timeFrom: timeArb, + timeTo: timeArb, + }), +); + +const arrivalArb: fc.Arbitrary = fc.oneof( + fc.record({ + type: fc.constant("arrival" as const), + station: stationArb, + date: dateArb, + }), + fc.record({ + type: fc.constant("arrival" as const), + station: stationArb, + date: dateArb, + timeFrom: timeArb, + timeTo: timeArb, + }), +); + +const routeArb: fc.Arbitrary = fc.oneof( + fc.record({ + type: fc.constant("route" as const), + departure: stationArb, + arrival: stationArb, + date: dateArb, + }), + fc.record({ + type: fc.constant("route" as const), + departure: stationArb, + arrival: stationArb, + date: dateArb, + timeFrom: timeArb, + timeTo: timeArb, + }), +); + +const detailsArb: fc.Arbitrary = fc.oneof( + fc.record({ + type: fc.constant("details" as const), + carrier: carrierArb, + flightNumber: flightNumberArb, + date: dateArb, + }), + fc.record({ + type: fc.constant("details" as const), + carrier: carrierArb, + flightNumber: flightNumberArb, + suffix: suffixArb, + date: dateArb, + }), +); + +/** Combined arbitrary covering all 6 OnlineBoardParams discriminants */ +const onlineBoardParamsArb: fc.Arbitrary = fc.oneof( + { weight: 1, arbitrary: startArb }, + { weight: 3, arbitrary: flightArb }, + { weight: 2, arbitrary: departureArb }, + { weight: 2, arbitrary: arrivalArb }, + { weight: 2, arbitrary: routeArb }, + { weight: 3, arbitrary: detailsArb }, +); + +// --------------------------------------------------------------------------- +// Register against harness +// --------------------------------------------------------------------------- + +defineUrlParityTests({ + feature: "OnlineBoard", + fixturePath: "tests/fixtures/phase-2/url-corpus/onlineboard.json", + parse: parseOnlineBoardUrl, + build: buildOnlineBoardUrl, + fuzzArbitrary: onlineBoardParamsArb, +}); diff --git a/tsconfig.json b/tsconfig.json index 76b65f0f..c7f98e7f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,6 @@ }, "types": ["node", "vitest/globals"] }, - "include": ["src/**/*.ts", "src/**/*.tsx", "vitest.config.ts", "modern.config.ts", "module-federation.config.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "vitest.config.ts", "modern.config.ts", "module-federation.config.ts"], "exclude": ["node_modules", "dist", "ClientApp", "wwwroot"] }