Files
gnezim ad8367c203 Refine Angular parity: titles, airline header, labels
Search page:
- Title and breadcrumb now read the station dictionaries and render the
  human-friendly route heading (e.g. 'Маршрут: Шереметьево - Пулково')
  for route/departure/arrival/flight search URLs, mirroring Angular.

Details page:
- Main H1 becomes 'Информация о рейсе: SU 6805, Москва - Санкт-Петербург'
  (carrier + flight number + origin/destination cities), not a bare
  flight number.
- Add 'Детали рейса' section header above the accordion to match
  Angular's flight-details-wrapper layout.
- Promote the airline block in BoardDetailsHeader: drop the legacy
  OperatorLogo copy with broken asset paths and hand off to the shared
  <OperatorLogo> under src/ui/flights. Render it with the
  'авиакомпания' caption beside the enlarged flight number.
- Replace hardcoded English 'Leg' / 'Total flying time' / 'Aircraft:'
  with i18n keys, added to all nine locale files.

Test harness:
- Add vi.mock for useDictionaries in the three suites that render
  OnlineBoardSearchPage (the new heading helper calls the hook and
  crashed without ApiClientProvider). 1256 tests passing.
2026-04-17 23:48:06 +03:00

193 lines
6.7 KiB
TypeScript

/**
* Validates that the API layer can parse real responses captured from the
* test environment on 2026-04-17. Each test wires a real fixture into a
* mock fetch, then runs the production API function and asserts the
* parsed value has the structure the UI relies on.
*
* These tests catch drift between the declared TS types and the actual
* API contract. To refresh fixtures: `AEROFLOT_API_AUTH=user:pass
* ./scripts/fetch-api-fixtures.sh`.
*/
import { describe, it, expect, vi } from "vitest";
import { ApiClient } from "@/shared/api/client";
import { searchFlights, getFlightDetails, getCalendarDays } from "@/features/online-board/api";
import { searchSchedule, getScheduleDetails, getScheduleCalendarDays } from "@/features/schedule/api";
import { searchDestinations, getFlightsMapCalendar } from "@/features/flights-map/api";
import { getPopularRequests } from "@/features/popular-requests/api";
import { fetchDictionaries } from "@/shared/dictionaries/api";
import { fixtures } from "../fixtures/api";
function clientWith(body: unknown, status = 200): { client: ApiClient; fetchMock: ReturnType<typeof vi.fn> } {
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
}),
);
const client = new ApiClient({
baseUrl: "https://api.test.com",
locale: "ru",
fetchImpl: fetchMock,
retry: { maxRetries: 0 },
});
return { client, fetchMock };
}
describe("online-board API against real fixtures", () => {
it("searchFlights parses a by-route response with multiple flights", async () => {
const { client } = clientWith(fixtures.boardByRoute);
const result = await searchFlights(client, {
dateFrom: "2026-04-17",
dateTo: "2026-04-18",
departure: "SVO",
arrival: "LED",
});
expect(result.data.routes.length).toBeGreaterThan(0);
expect(result.data.partners).toBeInstanceOf(Array);
const first = result.data.routes[0]!;
expect(first.routeType).toMatch(/Direct|MultiLeg/);
expect(first.flightId.carrier).toBe("SU");
});
it("searchFlights parses a by-flight-number response", async () => {
const { client } = clientWith(fixtures.boardByFlight);
const result = await searchFlights(client, {
dateFrom: "2026-04-17",
dateTo: "2026-04-18",
flightNumber: "SU6951",
});
expect(result.data.routes[0]!.flightId.flightNumber).toBe("6951");
});
it("getFlightDetails parses the onlineboard/details response", async () => {
const { client } = clientWith(fixtures.onlineboardDetails);
const result = await getFlightDetails(client, {
flights: "SU6951",
dates: "2026-04-17",
});
expect(result.data.routes.length).toBeGreaterThan(0);
});
it("getCalendarDays parses bitmask into date list", async () => {
const { client } = clientWith(fixtures.boardDaysFlight);
const days = await getCalendarDays(client, {
date: "2026-04-17",
searchType: "flight",
flightNumber: "SU6951",
});
expect(days.length).toBeGreaterThan(0);
expect(days[0]).toMatch(/^\d{8}$/);
});
});
describe("schedule API against real fixtures", () => {
it("searchSchedule parses an array response", async () => {
const { client } = clientWith(fixtures.scheduleSearch);
const result = await searchSchedule(client, {
departure: "SVO",
arrival: "LED",
dateFrom: "2026-04-18",
dateTo: "2026-04-22",
});
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
});
it("getScheduleDetails parses a details response", async () => {
const { client } = clientWith(fixtures.scheduleDetails);
const result = await getScheduleDetails(client, {
flights: ["SU6951"],
dates: ["2026-04-17"],
departure: "SVO",
arrival: "LED",
});
expect(result.data).toBeDefined();
});
it("getScheduleCalendarDays parses the days response", async () => {
const { client } = clientWith(fixtures.scheduleDaysRoute);
const days = await getScheduleCalendarDays(client, {
date: "20260417",
departure: "SVO",
arrival: "LED",
connections: false,
});
expect(days).toBeInstanceOf(Array);
});
});
describe("flights-map API against real fixtures", () => {
it("searchDestinations parses the destinations response", async () => {
const { client } = clientWith(fixtures.destinationsFrom);
const result = await searchDestinations(client, {
departure: "SVO",
dateFrom: "2026-04-18",
dateTo: "2026-04-22",
connections: 0,
});
expect(result.data.routes).toBeInstanceOf(Array);
expect(result.data.routes.length).toBeGreaterThan(0);
const first = result.data.routes[0]!;
expect(first.route).toBeInstanceOf(Array);
expect(typeof first.isDirect).toBe("boolean");
});
it("getFlightsMapCalendar parses the days bitmap", async () => {
const { client } = clientWith(fixtures.mapDaysRoute);
const days = await getFlightsMapCalendar(client, {
date: "20260417",
departure: "SVO",
arrival: "LED",
connections: false,
});
expect(days).toBeInstanceOf(Array);
});
});
describe("popular-requests API against real fixture", () => {
it("getPopularRequests returns a list of discriminated requests", async () => {
const { client } = clientWith(fixtures.popularRequests);
const result = await getPopularRequests(client);
expect(result.length).toBeGreaterThan(0);
for (const req of result) {
expect(["FlightNumber", "Route", "RouteWithBack", "Departure", "Arrival"]).toContain(req.mode);
expect(["Schedule", "Onlineboard"]).toContain(req.type);
}
});
});
describe("dictionaries API against real fixtures", () => {
it("fetchDictionaries parses all four endpoints", async () => {
const calls = [
fixtures.dictionaryRegions,
fixtures.dictionaryCountries,
fixtures.dictionaryCities,
fixtures.dictionaryAirports,
];
let i = 0;
const fetchMock = vi.fn<typeof fetch>().mockImplementation(() =>
Promise.resolve(
new Response(JSON.stringify(calls[i++]!), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
),
);
const client = new ApiClient({
baseUrl: "https://api.test.com",
locale: "ru",
fetchImpl: fetchMock,
retry: { maxRetries: 0 },
});
const result = await fetchDictionaries(client);
expect(result.regions.length).toBeGreaterThan(0);
expect(result.countries.length).toBeGreaterThan(0);
expect(result.cities.length).toBeGreaterThan(0);
expect(result.airports.length).toBeGreaterThan(0);
expect(result.regions[0]!.title).toBeTypeOf("object");
expect(result.airports[0]!.code).toBeTypeOf("string");
});
});