plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
8 changed files with 875 additions and 5 deletions
Showing only changes of commit c67686463a - Show all commits
@@ -0,0 +1,12 @@
# Phase 3E -- Schedule Parity + Integration Tests
> **Parent:** `2026-04-15-phase-3-schedule-master.md`
> **Depends on:** 3A-3D (all schedule code)
## Deliverables
1. URL parity test registered in harness
2. URL fixture corpus for schedule
3. Integration tests (~10)
4. Updated barrel (`index.ts`)
5. Updated MF expose (`Schedule.tsx`)
+54 -1
View File
@@ -1,2 +1,55 @@
// Public barrel for the schedule feature. See frozen-barrels.md.
export {};
// 3A — URL serializer/parser
export type { ScheduleParams } from "./url.js";
export {
parseScheduleUrl,
buildScheduleUrl,
parseScheduleRouteParams,
buildScheduleRouteParams,
} from "./url.js";
// 3A — Types
export type {
IScheduleRouteDirectionParams,
IScheduleFlightId,
IScheduleSearchRequest,
IScheduleResponse,
IScheduleDetailsResponse,
IScheduleCalendarParams,
IScheduleDaysResponse,
IScheduleRouteParams,
IScheduleDetailsParams,
} from "./types.js";
// Re-exported shared types
export type { IFlight, ISimpleFlight, IBoardResponse, IFlightLeg } from "./types.js";
// 3B — API functions
export { searchSchedule, getScheduleDetails, getScheduleCalendarDays } from "./api.js";
// 3B — React hooks
export { useScheduleSearch } from "./hooks/useScheduleSearch.js";
export type { UseScheduleSearchResult } from "./hooks/useScheduleSearch.js";
export { useScheduleDetails } from "./hooks/useScheduleDetails.js";
export type { UseScheduleDetailsResult, UseScheduleDetailsParams } from "./hooks/useScheduleDetails.js";
export { useScheduleCalendar } from "./hooks/useScheduleCalendar.js";
export type { UseScheduleCalendarResult } from "./hooks/useScheduleCalendar.js";
// 3D — SEO builder functions
export {
buildScheduleStartSeo,
buildScheduleSearchSeo,
buildScheduleDetailsSeo,
} from "./seo.js";
export type { TFunction, CityNames } from "./seo.js";
// 3D — JSON-LD builder functions
export { buildScheduleFlightJsonLd, buildScheduleFlightListJsonLd } from "./json-ld.js";
// 3C — Feature-specific page components
export { ScheduleStartPage } from "./components/ScheduleStartPage.js";
export { ScheduleSearchPage } from "./components/ScheduleSearchPage.js";
export type { ScheduleSearchPageProps } from "./components/ScheduleSearchPage.js";
export { ScheduleDetailsPage } from "./components/ScheduleDetailsPage.js";
export type { ScheduleDetailsPageProps } from "./components/ScheduleDetailsPage.js";
+17 -4
View File
@@ -1,9 +1,20 @@
import type { HostContract } from "@/host-contract";
/**
* MF expose wrapper for the Schedule feature.
* Phase 3 (schedule port) replaces the body with the real root.
*
* Lazy-loads the ScheduleStartPage via React.lazy + Suspense.
* Full MF integration with host routing is deferred to a later phase;
* for now this renders the start page for embedded usage.
*/
import { lazy, Suspense } from "react";
import type { HostContract } from "@/host-contract";
const ScheduleStartPage = lazy(() =>
import("@/features/schedule/components/ScheduleStartPage.js").then(
(m) => ({ default: m.ScheduleStartPage }),
),
);
export interface ScheduleRemoteProps {
hostContract: HostContract;
}
@@ -11,7 +22,9 @@ export interface ScheduleRemoteProps {
export default function ScheduleRemote(_props: ScheduleRemoteProps): JSX.Element {
return (
<div data-mf-expose="Schedule">
<p>Schedule remote stub. Populated in Phase 3.</p>
<Suspense fallback={<div aria-busy="true">Loading Schedule...</div>}>
<ScheduleStartPage />
</Suspense>
</div>
);
}
+243
View File
@@ -0,0 +1,243 @@
[
{
"url": "schedule",
"expected": { "type": "start" }
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501",
"expected": {
"type": "route",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501"
}
}
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501-06002200",
"expected": {
"type": "route",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501",
"timeFrom": "0600",
"timeTo": "2200"
}
}
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501-C1",
"expected": {
"type": "route",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501",
"connections": 1
}
}
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501-C0",
"expected": {
"type": "route",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501",
"connections": 0
}
}
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501-06002200-C0",
"expected": {
"type": "route",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501",
"timeFrom": "0600",
"timeTo": "2200",
"connections": 0
}
}
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501/KUF-MOW-20220502-20220508",
"expected": {
"type": "roundtrip",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501"
},
"inbound": {
"departure": "KUF",
"arrival": "MOW",
"dateFrom": "20220502",
"dateTo": "20220508"
}
}
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501-06002200/KUF-MOW-20220502-20220508-06002200",
"expected": {
"type": "roundtrip",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501",
"timeFrom": "0600",
"timeTo": "2200"
},
"inbound": {
"departure": "KUF",
"arrival": "MOW",
"dateFrom": "20220502",
"dateTo": "20220508",
"timeFrom": "0600",
"timeTo": "2200"
}
}
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501-C0/KUF-MOW-20220502-20220508-C0",
"expected": {
"type": "roundtrip",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501",
"connections": 0
},
"inbound": {
"departure": "KUF",
"arrival": "MOW",
"dateFrom": "20220502",
"dateTo": "20220508",
"connections": 0
}
}
},
{
"url": "schedule/route/MOW-KUF-20220425-20220501-06002200-C0/KUF-MOW-20220502-20220508-06002200-C0",
"expected": {
"type": "roundtrip",
"outbound": {
"departure": "MOW",
"arrival": "KUF",
"dateFrom": "20220425",
"dateTo": "20220501",
"timeFrom": "0600",
"timeTo": "2200",
"connections": 0
},
"inbound": {
"departure": "KUF",
"arrival": "MOW",
"dateFrom": "20220502",
"dateTo": "20220508",
"timeFrom": "0600",
"timeTo": "2200",
"connections": 0
}
}
},
{
"url": "schedule/SU0012-20220527",
"expected": {
"type": "details",
"flights": [
{ "carrier": "SU", "flightNumber": "0012", "date": "20220527" }
]
}
},
{
"url": "schedule/SU0012-20220501/SU0013-20220507",
"expected": {
"type": "details",
"flights": [
{ "carrier": "SU", "flightNumber": "0012", "date": "20220501" },
{ "carrier": "SU", "flightNumber": "0013", "date": "20220507" }
]
}
},
{
"url": "schedule/SU1765-20220307",
"expected": {
"type": "details",
"flights": [
{ "carrier": "SU", "flightNumber": "1765", "date": "20220307" }
]
}
},
{
"url": "schedule/SU1210-20220307/SU1765-20220307",
"expected": {
"type": "details",
"flights": [
{ "carrier": "SU", "flightNumber": "1210", "date": "20220307" },
{ "carrier": "SU", "flightNumber": "1765", "date": "20220307" }
]
}
},
{
"url": "schedule/route/SVO-LED-20250115-20250122",
"expected": {
"type": "route",
"outbound": {
"departure": "SVO",
"arrival": "LED",
"dateFrom": "20250115",
"dateTo": "20250122"
}
}
},
{
"url": "schedule/route/SVO-LED-20250115-20250122-08001800",
"expected": {
"type": "route",
"outbound": {
"departure": "SVO",
"arrival": "LED",
"dateFrom": "20250115",
"dateTo": "20250122",
"timeFrom": "0800",
"timeTo": "1800"
}
}
},
{
"url": "schedule/route/SVO-LED-20250115-20250122-C2",
"expected": {
"type": "route",
"outbound": {
"departure": "SVO",
"arrival": "LED",
"dateFrom": "20250115",
"dateTo": "20250122",
"connections": 2
}
}
},
{
"url": "schedule/SU0100D-20250115",
"expected": {
"type": "details",
"flights": [
{ "carrier": "SU", "flightNumber": "0100", "suffix": "D", "date": "20250115" }
]
}
}
]
+136
View File
@@ -0,0 +1,136 @@
/**
* Shared test fixtures for Schedule integration tests.
*
* Provides realistic mock data matching the API response shapes
* for schedule endpoints.
*/
import type {
ISimpleFlight,
IDirectFlight,
IFlightLeg,
IBoardResponse,
} from "@/features/online-board/types.js";
// ---------------------------------------------------------------------------
// Flight leg builder
// ---------------------------------------------------------------------------
function makeLeg(overrides: Partial<IFlightLeg> & { index: number }): IFlightLeg {
return {
index: overrides.index,
status: overrides.status ?? "Scheduled",
flyingTime: overrides.flyingTime ?? "01:30",
dayChange: overrides.dayChange ?? 0,
updated: overrides.updated ?? "2022-05-27T10:00:00Z",
equipment: overrides.equipment ?? { name: "Airbus A320", code: "320" },
flags: overrides.flags ?? {
checkinAvailable: false,
returnToAirport: false,
routeChanged: false,
},
operatingBy: overrides.operatingBy ?? { carrier: "SU", flightNumber: "0012" },
departure: overrides.departure ?? {
scheduled: {
airport: "Sheremetyevo",
airportCode: "SVO",
city: "Moscow",
cityCode: "MOW",
countryCode: "RU",
},
checkingStatus: "closed",
times: {
scheduledDeparture: {
dayChange: { value: 0, title: "" },
local: "2022-05-27T10:00:00",
localTime: "10:00",
tzOffset: 3,
utc: "2022-05-27T07:00:00Z",
},
},
},
arrival: overrides.arrival ?? {
scheduled: {
airport: "Pulkovo",
airportCode: "LED",
city: "Saint Petersburg",
cityCode: "LED",
countryCode: "RU",
},
times: {
scheduledArrival: {
dayChange: { value: 0, title: "" },
local: "2022-05-27T11:30:00",
localTime: "11:30",
tzOffset: 3,
utc: "2022-05-27T08:30:00Z",
},
},
},
};
}
// ---------------------------------------------------------------------------
// Flight fixtures
// ---------------------------------------------------------------------------
export const SCHEDULE_FLIGHT_1: IDirectFlight = {
id: "SU0012-20220527",
flightId: {
carrier: "SU",
flightNumber: "0012",
suffix: "",
date: "20220527",
},
routeType: "Direct",
status: "Scheduled",
flyingTime: "1h 30m",
operatingBy: { carrier: "SU", flightNumber: "0012" },
leg: makeLeg({ index: 0 }),
};
export const SCHEDULE_FLIGHT_2: IDirectFlight = {
id: "SU0013-20220527",
flightId: {
carrier: "SU",
flightNumber: "0013",
suffix: "",
date: "20220527",
},
routeType: "Direct",
status: "Scheduled",
flyingTime: "1h 30m",
operatingBy: { carrier: "SU", flightNumber: "0013" },
leg: makeLeg({ index: 0 }),
};
export const ALL_SCHEDULE_FLIGHTS: ISimpleFlight[] = [
SCHEDULE_FLIGHT_1,
SCHEDULE_FLIGHT_2,
];
// ---------------------------------------------------------------------------
// API response fixtures
// ---------------------------------------------------------------------------
/** Schedule search response -- IFlight[] */
export const SCHEDULE_SEARCH_RESPONSE: ISimpleFlight[] = ALL_SCHEDULE_FLIGHTS;
/** Schedule details response -- IBoardResponse */
export const SCHEDULE_DETAILS_RESPONSE: IBoardResponse = {
data: {
partners: ["SU"],
routes: ALL_SCHEDULE_FLIGHTS,
daysOfFlight: ["2022-05-27"],
},
};
export const EMPTY_DETAILS_RESPONSE: IBoardResponse = {
data: {
partners: [],
routes: [],
daysOfFlight: [],
},
};
export const CALENDAR_DAYS = ["2022-05-25", "2022-05-26", "2022-05-27", "2022-05-28"];
@@ -0,0 +1,111 @@
/**
* Integration tests for Schedule SEO + JSON-LD.
*
* Verifies SEO builders produce complete, well-formed output and
* JSON-LD schemas conform to schema.org types.
*/
import { describe, it, expect } from "vitest";
import {
buildScheduleStartSeo,
buildScheduleSearchSeo,
buildScheduleDetailsSeo,
} from "@/features/schedule/seo.js";
import {
buildScheduleFlightJsonLd,
buildScheduleFlightListJsonLd,
} from "@/features/schedule/json-ld.js";
import { SCHEDULE_FLIGHT_1, SCHEDULE_FLIGHT_2 } from "./fixtures.js";
import type { IScheduleFlightId } from "@/features/schedule/types.js";
function stubT(key: string, opts?: Record<string, unknown>): string {
if (opts && Object.keys(opts).length > 0) {
const vars = Object.entries(opts)
.map(([k, v]) => `${k}=${v}`)
.join(",");
return `${key}|${vars}`;
}
return key;
}
const CANONICAL = "https://www.aeroflot.ru";
describe("Schedule SEO integration", () => {
it("start page SEO has all required fields", () => {
const result = buildScheduleStartSeo(stubT, "ru", CANONICAL);
expect(result.title).toBeTruthy();
expect(result.description).toBeTruthy();
expect(result.canonical).toBeTruthy();
expect(result.og).toBeDefined();
expect(result.hreflang).toBeDefined();
expect(result.hreflang).toHaveLength(10);
});
it("search page SEO canonical matches route format", () => {
const params = {
type: "route" as const,
outbound: {
departure: "SVO",
arrival: "LED",
dateFrom: "20250115",
dateTo: "20250122",
},
};
const result = buildScheduleSearchSeo(stubT, params, "ru", CANONICAL);
expect(result.canonical).toBe(
"https://www.aeroflot.ru/ru/schedule/route/SVO-LED-20250115-20250122",
);
});
it("details page SEO uses article og type", () => {
const flightIds: IScheduleFlightId[] = [
{ carrier: "SU", flightNumber: "0012", date: "20220527" },
];
const result = buildScheduleDetailsSeo(
stubT,
[SCHEDULE_FLIGHT_1],
"ru",
CANONICAL,
flightIds,
);
expect(result.og.type).toBe("article");
});
});
describe("Schedule JSON-LD integration", () => {
it("single flight produces valid Flight schema", () => {
const jsonLd = buildScheduleFlightJsonLd(SCHEDULE_FLIGHT_1);
expect(jsonLd["@type"]).toBe("Flight");
expect(jsonLd.flightNumber).toBe("SU0012");
expect(jsonLd.departureAirport).toBeDefined();
expect(jsonLd.arrivalAirport).toBeDefined();
});
it("flight list produces valid ItemList schema", () => {
const jsonLd = buildScheduleFlightListJsonLd(
[SCHEDULE_FLIGHT_1, SCHEDULE_FLIGHT_2],
"Schedule SVO to LED",
);
expect(jsonLd["@type"]).toBe("ItemList");
expect(jsonLd.description).toBe("Schedule SVO to LED");
const items = jsonLd.itemListElement as unknown as Array<{ position: number }>;
expect(items).toHaveLength(2);
expect(items[0]).toHaveProperty("position", 1);
expect(items[1]).toHaveProperty("position", 2);
});
it("empty flight list produces valid ItemList", () => {
const jsonLd = buildScheduleFlightListJsonLd([], "No flights");
expect(jsonLd["@type"]).toBe("ItemList");
expect(jsonLd.itemListElement).toEqual([]);
});
});
@@ -0,0 +1,142 @@
/**
* Integration tests for Schedule URL round-tripping.
*
* Verifies: build URL from params -> parse URL -> params match original.
*/
import { describe, it, expect } from "vitest";
import {
buildScheduleUrl,
parseScheduleUrl,
type ScheduleParams,
} from "@/features/schedule/url.js";
describe("Schedule URL round-trip integration", () => {
const roundTrip = (params: ScheduleParams) => {
const url = buildScheduleUrl(params);
const parsed = parseScheduleUrl(url);
return { url, parsed };
};
it("round-trips start page", () => {
const { parsed } = roundTrip({ type: "start" });
expect(parsed).toEqual({ type: "start" });
});
it("round-trips one-way route search", () => {
const params: ScheduleParams = {
type: "route",
outbound: { departure: "MOW", arrival: "KUF", dateFrom: "20220425", dateTo: "20220501" },
};
const { url, parsed } = roundTrip(params);
expect(url).toBe("schedule/route/MOW-KUF-20220425-20220501");
expect(parsed).toEqual(params);
});
it("round-trips one-way with time range and connections", () => {
const params: ScheduleParams = {
type: "route",
outbound: {
departure: "SVO",
arrival: "LED",
dateFrom: "20250115",
dateTo: "20250122",
timeFrom: "0800",
timeTo: "1800",
connections: 1,
},
};
const { url, parsed } = roundTrip(params);
expect(url).toBe("schedule/route/SVO-LED-20250115-20250122-08001800-C1");
expect(parsed).toEqual(params);
});
it("round-trips round-trip search", () => {
const params: ScheduleParams = {
type: "roundtrip",
outbound: { departure: "MOW", arrival: "KUF", dateFrom: "20220425", dateTo: "20220501" },
inbound: { departure: "KUF", arrival: "MOW", dateFrom: "20220502", dateTo: "20220508" },
};
const { url, parsed } = roundTrip(params);
expect(url).toBe("schedule/route/MOW-KUF-20220425-20220501/KUF-MOW-20220502-20220508");
expect(parsed).toEqual(params);
});
it("round-trips single-flight details", () => {
const params: ScheduleParams = {
type: "details",
flights: [{ carrier: "SU", flightNumber: "0012", date: "20220527" }],
};
const { url, parsed } = roundTrip(params);
expect(url).toBe("schedule/SU0012-20220527");
expect(parsed).toEqual(params);
});
it("round-trips multi-flight details", () => {
const params: ScheduleParams = {
type: "details",
flights: [
{ carrier: "SU", flightNumber: "0012", date: "20220501" },
{ carrier: "SU", flightNumber: "0013", date: "20220507" },
],
};
const { url, parsed } = roundTrip(params);
expect(url).toBe("schedule/SU0012-20220501/SU0013-20220507");
expect(parsed).toEqual(params);
});
it("returns null for invalid URL", () => {
expect(parseScheduleUrl("")).toBeNull();
expect(parseScheduleUrl("not-a-schedule-url")).toBeNull();
expect(parseScheduleUrl("schedule/route/")).toBeNull();
});
it("handles leading slash in parse", () => {
const parsed = parseScheduleUrl("/schedule/route/SVO-LED-20250115-20250122");
expect(parsed).toEqual({
type: "route",
outbound: {
departure: "SVO",
arrival: "LED",
dateFrom: "20250115",
dateTo: "20250122",
},
});
});
it("build then parse preserves all search variants", () => {
const searchTypes: ScheduleParams[] = [
{ type: "start" },
{
type: "route",
outbound: { departure: "SVO", arrival: "LED", dateFrom: "20250115", dateTo: "20250122" },
},
{
type: "roundtrip",
outbound: { departure: "SVO", arrival: "LED", dateFrom: "20250115", dateTo: "20250122" },
inbound: { departure: "LED", arrival: "SVO", dateFrom: "20250125", dateTo: "20250131" },
},
{
type: "details",
flights: [{ carrier: "SU", flightNumber: "0100", date: "20250115" }],
},
];
for (const params of searchTypes) {
const url = buildScheduleUrl(params);
const parsed = parseScheduleUrl(url);
expect(parsed).not.toBeNull();
expect(parsed!.type).toBe(params.type);
}
});
it("details with flight suffix round-trips correctly", () => {
const params: ScheduleParams = {
type: "details",
flights: [{ carrier: "SU", flightNumber: "0100", suffix: "D", date: "20250115" }],
};
const { url, parsed } = roundTrip(params);
expect(url).toBe("schedule/SU0100D-20250115");
expect(parsed).toEqual(params);
});
});
+160
View File
@@ -0,0 +1,160 @@
/**
* 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,
});