Files
flights_web/tests/parity/url/schedule.test.ts
T
gnezim c67686463a Add schedule parity harness, integration tests, barrel, and MF expose (Phase 3E)
Registers schedule URL serializer in parity harness with 18-entry fixture
corpus and fast-check fuzz. 10 URL round-trip integration tests, 6 SEO/
JSON-LD integration tests. Updates barrel and MF expose from stub to real.
2026-04-15 09:31:19 +03:00

161 lines
4.8 KiB
TypeScript

/**
* Schedule URL parity tests.
*
* Registers the Schedule URL serializer against the generic URL parity
* harness. Tests fixture corpus + fast-check fuzz roundtrip.
*
* @module
*/
import * as fc from "fast-check";
import { parseScheduleUrl, buildScheduleUrl } from "@/features/schedule/url.js";
import type { ScheduleParams } from "@/features/schedule/url.js";
import { defineUrlParityTests } from "./harness.js";
// ---------------------------------------------------------------------------
// fast-check arbitraries for ScheduleParams
// ---------------------------------------------------------------------------
const ALPHA_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("") as [string, ...string[]];
const alphaCharArb: fc.Arbitrary<string> = fc.constantFrom(...ALPHA_CHARS);
/** 2-char uppercase carrier code */
const carrierArb: fc.Arbitrary<string> = fc
.tuple(alphaCharArb, alphaCharArb)
.map(([a, b]) => `${a}${b}`);
/** 3-char uppercase IATA station code */
const stationArb: fc.Arbitrary<string> = fc
.tuple(alphaCharArb, alphaCharArb, alphaCharArb)
.map(([a, b, c]) => `${a}${b}${c}`);
/** Valid yyyyMMdd date string */
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 }),
})
.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 */
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 }) =>
`${String(hour).padStart(2, "0")}${String(minute).padStart(2, "0")}`,
);
// ---------------------------------------------------------------------------
// Route direction params arbitrary
// ---------------------------------------------------------------------------
import type { IScheduleRouteDirectionParams } from "@/features/schedule/types.js";
const directionBaseArb: fc.Arbitrary<IScheduleRouteDirectionParams> = fc.record({
departure: stationArb,
arrival: stationArb,
dateFrom: dateArb,
dateTo: dateArb,
});
const directionWithTimeArb: fc.Arbitrary<IScheduleRouteDirectionParams> = fc.record({
departure: stationArb,
arrival: stationArb,
dateFrom: dateArb,
dateTo: dateArb,
timeFrom: timeArb,
timeTo: timeArb,
});
const directionWithConnArb: fc.Arbitrary<IScheduleRouteDirectionParams> = fc.record({
departure: stationArb,
arrival: stationArb,
dateFrom: dateArb,
dateTo: dateArb,
connections: fc.integer({ min: 0, max: 5 }),
});
const directionWithAllArb: fc.Arbitrary<IScheduleRouteDirectionParams> = fc.record({
departure: stationArb,
arrival: stationArb,
dateFrom: dateArb,
dateTo: dateArb,
timeFrom: timeArb,
timeTo: timeArb,
connections: fc.integer({ min: 0, max: 5 }),
});
const directionArb: fc.Arbitrary<IScheduleRouteDirectionParams> = fc.oneof(
directionBaseArb,
directionWithTimeArb,
directionWithConnArb,
directionWithAllArb,
);
// ---------------------------------------------------------------------------
// Discriminated union arbitraries
// ---------------------------------------------------------------------------
const startArb: fc.Arbitrary<ScheduleParams> = fc.constant({ type: "start" as const });
const routeArb: fc.Arbitrary<ScheduleParams> = directionArb.map((d) => ({
type: "route" as const,
outbound: d,
}));
const roundtripArb: fc.Arbitrary<ScheduleParams> = fc
.tuple(directionArb, directionArb)
.map(([outbound, inbound]) => ({
type: "roundtrip" as const,
outbound,
inbound,
}));
import type { IScheduleFlightId } from "@/features/schedule/types.js";
const flightIdArb: fc.Arbitrary<IScheduleFlightId> = fc.record({
carrier: carrierArb,
flightNumber: flightNumberArb,
date: dateArb,
});
const detailsArb: fc.Arbitrary<ScheduleParams> = fc
.array(flightIdArb, { minLength: 1, maxLength: 4 })
.map((flights) => ({
type: "details" as const,
flights,
}));
/** Combined arbitrary covering all ScheduleParams discriminants */
const scheduleParamsArb: fc.Arbitrary<ScheduleParams> = fc.oneof(
{ weight: 1, arbitrary: startArb },
{ weight: 3, arbitrary: routeArb },
{ weight: 3, arbitrary: roundtripArb },
{ weight: 3, arbitrary: detailsArb },
);
// ---------------------------------------------------------------------------
// Register against harness
// ---------------------------------------------------------------------------
defineUrlParityTests<ScheduleParams>({
feature: "Schedule",
fixturePath: "tests/fixtures/phase-3/url-corpus/schedule.json",
parse: parseScheduleUrl,
build: buildScheduleUrl,
fuzzArbitrary: scheduleParamsArb,
});