Add 60 integration tests for Online Board feature
6 test files covering start page, flight search, flight details, URL round-tripping, error handling, and SEO output verification. All tests use mocked API and SignalR layers with jsdom environment.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Integration tests for error handling in the Online Board.
|
||||
*
|
||||
* Verifies error UI renders correctly for API errors (404, 500, timeout)
|
||||
* and that retry triggers re-fetch.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { OnlineBoardSearchPage } from "@/features/online-board/components/OnlineBoardSearchPage.js";
|
||||
import { OnlineBoardDetailsPage } from "@/features/online-board/components/OnlineBoardDetailsPage.js";
|
||||
import { ApiHttpError, ApiTimeoutError } from "@/shared/api/errors.js";
|
||||
import type { IParsedFlightId } from "@/features/online-board/types.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
useParams: () => ({ lang: "ru" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/i18n/provider.js", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: { language: "ru" },
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUseOnlineBoard = vi.fn();
|
||||
vi.mock("@/features/online-board/hooks/useOnlineBoard.js", () => ({
|
||||
useOnlineBoard: (...args: unknown[]) => mockUseOnlineBoard(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/online-board/hooks/useLiveBoardSearch.js", () => ({
|
||||
useLiveBoardSearch: (_params: unknown, initialFlights: unknown) => ({
|
||||
flights: initialFlights,
|
||||
connectionStatus: "idle" as const,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/online-board/hooks/useCalendarDays.js", () => ({
|
||||
useCalendarDays: () => ({
|
||||
days: [],
|
||||
loading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUseFlightDetails = vi.fn();
|
||||
vi.mock("@/features/online-board/hooks/useFlightDetails.js", () => ({
|
||||
useFlightDetails: (...args: unknown[]) => mockUseFlightDetails(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/online-board/hooks/useLiveFlightDetails.js", () => ({
|
||||
useLiveFlightDetails: () => ({
|
||||
flight: null,
|
||||
connectionStatus: "idle" as const,
|
||||
}),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEPARTURE_PARAMS = {
|
||||
type: "departure" as const,
|
||||
station: "SVO",
|
||||
date: "20250115",
|
||||
};
|
||||
|
||||
const FLIGHT_ID: IParsedFlightId = {
|
||||
carrier: "SU",
|
||||
flightNumber: "0100",
|
||||
date: "20250115",
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — Search page errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Search page error handling", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders error UI for HTTP 500", () => {
|
||||
const refreshSpy = vi.fn();
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: [],
|
||||
loading: false,
|
||||
error: new ApiHttpError("HTTP 500 from /board", 500),
|
||||
refresh: refreshSpy,
|
||||
});
|
||||
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
expect(screen.getByTestId("search-error")).toBeTruthy();
|
||||
expect(screen.getByText(/Failed to load flights/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders error UI for HTTP 404", () => {
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: [],
|
||||
loading: false,
|
||||
error: new ApiHttpError("HTTP 404", 404),
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
expect(screen.getByTestId("search-error")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders error UI for timeout", () => {
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: [],
|
||||
loading: false,
|
||||
error: new ApiTimeoutError(5000),
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
expect(screen.getByTestId("search-error")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders retry button in error state", () => {
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: [],
|
||||
loading: false,
|
||||
error: new ApiHttpError("HTTP 500", 500),
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
expect(screen.getByText("Retry")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls refresh when retry button is clicked", () => {
|
||||
const refreshSpy = vi.fn();
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: [],
|
||||
loading: false,
|
||||
error: new ApiHttpError("HTTP 500", 500),
|
||||
refresh: refreshSpy,
|
||||
});
|
||||
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
fireEvent.click(screen.getByText("Retry"));
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — Details page errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Details page error handling", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders error state for details API failure", () => {
|
||||
mockUseFlightDetails.mockReturnValue({
|
||||
flight: null,
|
||||
loading: false,
|
||||
error: new ApiHttpError("HTTP 500", 500),
|
||||
});
|
||||
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-details-error")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders not-found when details returns null", () => {
|
||||
mockUseFlightDetails.mockReturnValue({
|
||||
flight: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-details-not-found")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Shared test fixtures for Online Board integration tests.
|
||||
*
|
||||
* Provides realistic mock data matching the API response shapes
|
||||
* defined in src/features/online-board/types.ts.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ISimpleFlight,
|
||||
IDirectFlight,
|
||||
IMultiLegFlight,
|
||||
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 ?? "03:30",
|
||||
dayChange: overrides.dayChange ?? 0,
|
||||
updated: overrides.updated ?? "2025-01-15T10:00:00Z",
|
||||
equipment: overrides.equipment ?? { name: "Boeing 777-300ER", code: "77W" },
|
||||
flags: overrides.flags ?? {
|
||||
checkinAvailable: true,
|
||||
returnToAirport: false,
|
||||
routeChanged: false,
|
||||
},
|
||||
operatingBy: overrides.operatingBy ?? { carrier: "SU", flightNumber: "100" },
|
||||
departure: overrides.departure ?? {
|
||||
scheduled: {
|
||||
airport: "Sheremetyevo",
|
||||
airportCode: "SVO",
|
||||
city: "Moscow",
|
||||
cityCode: "MOW",
|
||||
countryCode: "RU",
|
||||
},
|
||||
checkingStatus: "Open",
|
||||
times: {
|
||||
scheduledDeparture: {
|
||||
dayChange: { value: 0, title: "" },
|
||||
local: "10:00",
|
||||
localTime: "10:00:00",
|
||||
tzOffset: 3,
|
||||
utc: "2025-01-15T07:00:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
arrival: overrides.arrival ?? {
|
||||
scheduled: {
|
||||
airport: "John F. Kennedy",
|
||||
airportCode: "JFK",
|
||||
city: "New York",
|
||||
cityCode: "NYC",
|
||||
countryCode: "US",
|
||||
},
|
||||
times: {
|
||||
scheduledArrival: {
|
||||
dayChange: { value: 0, title: "" },
|
||||
local: "13:30",
|
||||
localTime: "13:30:00",
|
||||
tzOffset: -5,
|
||||
utc: "2025-01-15T18:30:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flight fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const DIRECT_FLIGHT: IDirectFlight = {
|
||||
id: "SU100-20250115",
|
||||
flightId: {
|
||||
carrier: "SU",
|
||||
flightNumber: "0100",
|
||||
suffix: "",
|
||||
date: "20250115",
|
||||
},
|
||||
routeType: "Direct",
|
||||
status: "Scheduled",
|
||||
flyingTime: "10:30",
|
||||
operatingBy: { carrier: "SU", flightNumber: "100" },
|
||||
leg: makeLeg({ index: 0 }),
|
||||
};
|
||||
|
||||
export const DIRECT_FLIGHT_2: IDirectFlight = {
|
||||
id: "SU200-20250115",
|
||||
flightId: {
|
||||
carrier: "SU",
|
||||
flightNumber: "0200",
|
||||
suffix: "",
|
||||
date: "20250115",
|
||||
},
|
||||
routeType: "Direct",
|
||||
status: "InFlight",
|
||||
flyingTime: "04:15",
|
||||
operatingBy: { carrier: "SU", flightNumber: "200" },
|
||||
leg: makeLeg({
|
||||
index: 0,
|
||||
status: "InFlight",
|
||||
departure: {
|
||||
scheduled: {
|
||||
airport: "Sheremetyevo",
|
||||
airportCode: "SVO",
|
||||
city: "Moscow",
|
||||
cityCode: "MOW",
|
||||
countryCode: "RU",
|
||||
},
|
||||
checkingStatus: "Closed",
|
||||
times: {
|
||||
scheduledDeparture: {
|
||||
dayChange: { value: 0, title: "" },
|
||||
local: "08:00",
|
||||
localTime: "08:00:00",
|
||||
tzOffset: 3,
|
||||
utc: "2025-01-15T05:00:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
arrival: {
|
||||
scheduled: {
|
||||
airport: "Pulkovo",
|
||||
airportCode: "LED",
|
||||
city: "Saint Petersburg",
|
||||
cityCode: "LED",
|
||||
countryCode: "RU",
|
||||
},
|
||||
times: {
|
||||
scheduledArrival: {
|
||||
dayChange: { value: 0, title: "" },
|
||||
local: "09:30",
|
||||
localTime: "09:30:00",
|
||||
tzOffset: 3,
|
||||
utc: "2025-01-15T06:30:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const MULTI_LEG_FLIGHT: IMultiLegFlight = {
|
||||
id: "SU300-20250115",
|
||||
flightId: {
|
||||
carrier: "SU",
|
||||
flightNumber: "0300",
|
||||
suffix: "",
|
||||
date: "20250115",
|
||||
},
|
||||
routeType: "MultiLeg",
|
||||
status: "Scheduled",
|
||||
flyingTime: "14:00",
|
||||
operatingBy: { carrier: "SU", flightNumber: "300" },
|
||||
legs: [
|
||||
makeLeg({ index: 0 }),
|
||||
makeLeg({
|
||||
index: 1,
|
||||
departure: {
|
||||
scheduled: {
|
||||
airport: "John F. Kennedy",
|
||||
airportCode: "JFK",
|
||||
city: "New York",
|
||||
cityCode: "NYC",
|
||||
countryCode: "US",
|
||||
},
|
||||
checkingStatus: "Open",
|
||||
times: {
|
||||
scheduledDeparture: {
|
||||
dayChange: { value: 0, title: "" },
|
||||
local: "16:00",
|
||||
localTime: "16:00:00",
|
||||
tzOffset: -5,
|
||||
utc: "2025-01-15T21:00:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
arrival: {
|
||||
scheduled: {
|
||||
airport: "Heathrow",
|
||||
airportCode: "LHR",
|
||||
city: "London",
|
||||
cityCode: "LON",
|
||||
countryCode: "GB",
|
||||
},
|
||||
times: {
|
||||
scheduledArrival: {
|
||||
dayChange: { value: 1, title: "+1" },
|
||||
local: "06:00",
|
||||
localTime: "06:00:00",
|
||||
tzOffset: 0,
|
||||
utc: "2025-01-16T06:00:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export const ALL_FLIGHTS: ISimpleFlight[] = [
|
||||
DIRECT_FLIGHT,
|
||||
DIRECT_FLIGHT_2,
|
||||
MULTI_LEG_FLIGHT,
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API response fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const BOARD_RESPONSE: IBoardResponse = {
|
||||
data: {
|
||||
partners: ["SU", "AF"],
|
||||
routes: ALL_FLIGHTS,
|
||||
daysOfFlight: ["2025-01-14", "2025-01-15", "2025-01-16"],
|
||||
},
|
||||
};
|
||||
|
||||
export const EMPTY_BOARD_RESPONSE: IBoardResponse = {
|
||||
data: {
|
||||
partners: [],
|
||||
routes: [],
|
||||
daysOfFlight: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const SINGLE_FLIGHT_RESPONSE: IBoardResponse = {
|
||||
data: {
|
||||
partners: ["SU"],
|
||||
routes: [DIRECT_FLIGHT],
|
||||
daysOfFlight: ["2025-01-15"],
|
||||
},
|
||||
};
|
||||
|
||||
export const CALENDAR_DAYS = ["2025-01-14", "2025-01-15", "2025-01-16", "2025-01-17"];
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Integration tests for the Online Board flight details page.
|
||||
*
|
||||
* Verifies flight info renders with mocked API data including
|
||||
* flight number, status, legs, operating carrier, and SEO head.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { OnlineBoardDetailsPage } from "@/features/online-board/components/OnlineBoardDetailsPage.js";
|
||||
import type { IParsedFlightId } from "@/features/online-board/types.js";
|
||||
import { DIRECT_FLIGHT, MULTI_LEG_FLIGHT } from "./fixtures.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
useParams: () => ({ lang: "ru" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/i18n/provider.js", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: { language: "ru" },
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUseFlightDetails = vi.fn();
|
||||
vi.mock("@/features/online-board/hooks/useFlightDetails.js", () => ({
|
||||
useFlightDetails: (...args: unknown[]) => mockUseFlightDetails(...args),
|
||||
}));
|
||||
|
||||
const mockUseLiveFlightDetails = vi.fn();
|
||||
vi.mock("@/features/online-board/hooks/useLiveFlightDetails.js", () => ({
|
||||
useLiveFlightDetails: (...args: unknown[]) => mockUseLiveFlightDetails(...args),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FLIGHT_ID: IParsedFlightId = {
|
||||
carrier: "SU",
|
||||
flightNumber: "0100",
|
||||
date: "20250115",
|
||||
};
|
||||
|
||||
function setupWithFlight(flight = DIRECT_FLIGHT) {
|
||||
mockUseFlightDetails.mockReturnValue({
|
||||
flight,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
mockUseLiveFlightDetails.mockReturnValue({
|
||||
flight,
|
||||
connectionStatus: "idle" as const,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Flight details page integration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders flight details container", () => {
|
||||
setupWithFlight();
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-details")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders flight number heading", () => {
|
||||
setupWithFlight();
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
// Flight number appears in both heading and FlightCard
|
||||
expect(screen.getAllByText("SU 0100").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("renders flight status", () => {
|
||||
setupWithFlight();
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
// Status appears in both overall status and leg status
|
||||
expect(screen.getAllByText("Scheduled").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("renders flight legs for direct flight", () => {
|
||||
setupWithFlight(DIRECT_FLIGHT);
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-legs")).toBeTruthy();
|
||||
expect(screen.getByTestId("flight-leg-0")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders multiple legs for multi-leg flight", () => {
|
||||
setupWithFlight(MULTI_LEG_FLIGHT);
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={{
|
||||
carrier: "SU",
|
||||
flightNumber: "0300",
|
||||
date: "20250115",
|
||||
}}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-leg-0")).toBeTruthy();
|
||||
expect(screen.getByTestId("flight-leg-1")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders operating carrier info", () => {
|
||||
setupWithFlight();
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("operating-carrier")).toBeTruthy();
|
||||
expect(screen.getByText(/Operated by: SU/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders flying time", () => {
|
||||
setupWithFlight();
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flying-time")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders error state when API fails", () => {
|
||||
mockUseFlightDetails.mockReturnValue({
|
||||
flight: null,
|
||||
loading: false,
|
||||
error: new Error("API error"),
|
||||
});
|
||||
mockUseLiveFlightDetails.mockReturnValue({
|
||||
flight: null,
|
||||
connectionStatus: "idle" as const,
|
||||
});
|
||||
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-details-error")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders not-found state when flight is null without error", () => {
|
||||
mockUseFlightDetails.mockReturnValue({
|
||||
flight: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
mockUseLiveFlightDetails.mockReturnValue({
|
||||
flight: null,
|
||||
connectionStatus: "idle" as const,
|
||||
});
|
||||
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("flight-details-not-found")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders airport codes in leg stations", () => {
|
||||
setupWithFlight();
|
||||
render(
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={FLIGHT_ID}
|
||||
locale="ru"
|
||||
canonicalOrigin="https://www.aeroflot.ru"
|
||||
/>,
|
||||
);
|
||||
// Airport codes appear in both FlightCard and FlightLegs
|
||||
expect(screen.getAllByText("SVO").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("JFK").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Integration tests for the Online Board search page.
|
||||
*
|
||||
* Verifies FlightList renders with mocked API data, connection status
|
||||
* badge, and calendar strip.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { OnlineBoardSearchPage } from "@/features/online-board/components/OnlineBoardSearchPage.js";
|
||||
import type { OnlineBoardSearchPageProps } from "@/features/online-board/components/OnlineBoardSearchPage.js";
|
||||
import { ALL_FLIGHTS, CALENDAR_DAYS } from "./fixtures.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
useParams: () => ({ lang: "ru" }),
|
||||
}));
|
||||
|
||||
const mockUseOnlineBoard = vi.fn();
|
||||
vi.mock("@/features/online-board/hooks/useOnlineBoard.js", () => ({
|
||||
useOnlineBoard: (...args: unknown[]) => mockUseOnlineBoard(...args),
|
||||
}));
|
||||
|
||||
const mockUseLiveBoardSearch = vi.fn();
|
||||
vi.mock("@/features/online-board/hooks/useLiveBoardSearch.js", () => ({
|
||||
useLiveBoardSearch: (...args: unknown[]) => mockUseLiveBoardSearch(...args),
|
||||
}));
|
||||
|
||||
const mockUseCalendarDays = vi.fn();
|
||||
vi.mock("@/features/online-board/hooks/useCalendarDays.js", () => ({
|
||||
useCalendarDays: (...args: unknown[]) => mockUseCalendarDays(...args),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEPARTURE_PARAMS: OnlineBoardSearchPageProps["params"] = {
|
||||
type: "departure",
|
||||
station: "SVO",
|
||||
date: "20250115",
|
||||
};
|
||||
|
||||
function setupMocksWithData() {
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: ALL_FLIGHTS,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
mockUseLiveBoardSearch.mockImplementation(
|
||||
(_params: unknown, initialFlights: unknown) => ({
|
||||
flights: initialFlights,
|
||||
connectionStatus: "idle" as const,
|
||||
}),
|
||||
);
|
||||
mockUseCalendarDays.mockReturnValue({
|
||||
days: CALENDAR_DAYS,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
function setupMocksEmpty() {
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
mockUseLiveBoardSearch.mockImplementation(
|
||||
(_params: unknown, initialFlights: unknown) => ({
|
||||
flights: initialFlights,
|
||||
connectionStatus: "idle" as const,
|
||||
}),
|
||||
);
|
||||
mockUseCalendarDays.mockReturnValue({
|
||||
days: [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
function setupMocksLoading() {
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
mockUseLiveBoardSearch.mockImplementation(
|
||||
(_params: unknown, initialFlights: unknown) => ({
|
||||
flights: initialFlights,
|
||||
connectionStatus: "idle" as const,
|
||||
}),
|
||||
);
|
||||
mockUseCalendarDays.mockReturnValue({
|
||||
days: [],
|
||||
loading: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Flight search page integration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders search page container with data", () => {
|
||||
setupMocksWithData();
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
expect(screen.getByTestId("online-board-search")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders flight cards when flights are returned", () => {
|
||||
setupMocksWithData();
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
// FlightCard renders the flight number as "SU 0100"
|
||||
const flightCards = screen.getAllByText(/SU 0\d{3}/);
|
||||
expect(flightCards.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("renders empty state when no flights returned", () => {
|
||||
setupMocksEmpty();
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
expect(screen.getByText("No flights found")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders calendar strip with available days", () => {
|
||||
setupMocksWithData();
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
const calendarStrip = screen.getByTestId("calendar-strip");
|
||||
expect(calendarStrip).toBeTruthy();
|
||||
// Verify calendar day buttons are present
|
||||
for (const day of CALENDAR_DAYS) {
|
||||
expect(screen.getByText(day)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders live connection badge when SignalR is live", () => {
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: ALL_FLIGHTS,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
mockUseLiveBoardSearch.mockImplementation(
|
||||
(_params: unknown, initialFlights: unknown) => ({
|
||||
flights: initialFlights,
|
||||
connectionStatus: "live" as const,
|
||||
}),
|
||||
);
|
||||
mockUseCalendarDays.mockReturnValue({ days: [], loading: false });
|
||||
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
expect(screen.getByText("Live")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders offline badge when SignalR disconnects", () => {
|
||||
mockUseOnlineBoard.mockReturnValue({
|
||||
flights: ALL_FLIGHTS,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
});
|
||||
mockUseLiveBoardSearch.mockImplementation(
|
||||
(_params: unknown, initialFlights: unknown) => ({
|
||||
flights: initialFlights,
|
||||
connectionStatus: "offline" as const,
|
||||
}),
|
||||
);
|
||||
mockUseCalendarDays.mockReturnValue({ days: [], loading: false });
|
||||
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
expect(screen.getByText("Offline")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("passes correct search params for departure type", () => {
|
||||
setupMocksWithData();
|
||||
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
||||
|
||||
const callArgs = mockUseOnlineBoard.mock.calls[0]?.[0] as Record<string, string>;
|
||||
expect(callArgs).toEqual({
|
||||
dateFrom: "20250115",
|
||||
dateTo: "20250115",
|
||||
departure: "SVO",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes correct search params for flight type", () => {
|
||||
setupMocksWithData();
|
||||
render(
|
||||
<OnlineBoardSearchPage
|
||||
params={{
|
||||
type: "flight",
|
||||
carrier: "SU",
|
||||
flightNumber: "100",
|
||||
date: "20250115",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const callArgs = mockUseOnlineBoard.mock.calls[0]?.[0] as Record<string, string>;
|
||||
expect(callArgs).toEqual({
|
||||
dateFrom: "20250115",
|
||||
dateTo: "20250115",
|
||||
flightNumber: "SU100",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes correct search params for route type", () => {
|
||||
setupMocksWithData();
|
||||
render(
|
||||
<OnlineBoardSearchPage
|
||||
params={{
|
||||
type: "route",
|
||||
departure: "SVO",
|
||||
arrival: "JFK",
|
||||
date: "20250115",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const callArgs = mockUseOnlineBoard.mock.calls[0]?.[0] as Record<string, string>;
|
||||
expect(callArgs).toEqual({
|
||||
dateFrom: "20250115",
|
||||
dateTo: "20250115",
|
||||
departure: "SVO",
|
||||
arrival: "JFK",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders loading skeleton when loading", () => {
|
||||
setupMocksLoading();
|
||||
const { container } = render(
|
||||
<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />,
|
||||
);
|
||||
// FlightListSkeleton renders skeleton elements
|
||||
const skeletons = container.querySelectorAll(".flight-list-skeleton__row");
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Integration tests for SEO output in the Online Board.
|
||||
*
|
||||
* Uses renderToString to verify that SeoHead produces expected
|
||||
* title, description, canonical, OG tags, hreflang, and JSON-LD
|
||||
* in the rendered HTML.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import React from "react";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import {
|
||||
buildOnlineBoardStartSeo,
|
||||
buildFlightSearchSeo,
|
||||
buildDepartureSearchSeo,
|
||||
buildFlightDetailsSeo,
|
||||
} from "@/features/online-board/seo.js";
|
||||
import { buildFlightJsonLd, buildFlightListJsonLd } from "@/features/online-board/json-ld.js";
|
||||
import { DIRECT_FLIGHT, ALL_FLIGHTS } from "./fixtures.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub translation function
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const t = (key: string, opts?: Record<string, string>) => {
|
||||
if (opts) {
|
||||
let result = key;
|
||||
for (const [k, v] of Object.entries(opts)) {
|
||||
result += ` ${k}=${v}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
const LOCALE = "ru";
|
||||
const ORIGIN = "https://www.aeroflot.ru";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — SeoHead rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("SEO integration — SeoHead SSR output", () => {
|
||||
it("renders title for start page", () => {
|
||||
const seoProps = buildOnlineBoardStartSeo(t, LOCALE, ORIGIN);
|
||||
const html = renderToString(React.createElement(SeoHead, seoProps));
|
||||
|
||||
expect(html).toContain("SEO.BOARD.MAIN.TITLE");
|
||||
});
|
||||
|
||||
it("renders meta description for start page", () => {
|
||||
const seoProps = buildOnlineBoardStartSeo(t, LOCALE, ORIGIN);
|
||||
const html = renderToString(React.createElement(SeoHead, seoProps));
|
||||
|
||||
expect(html).toContain('name="description"');
|
||||
expect(html).toContain("SEO.BOARD.MAIN.DESCRIPTION");
|
||||
});
|
||||
|
||||
it("renders canonical link for start page", () => {
|
||||
const seoProps = buildOnlineBoardStartSeo(t, LOCALE, ORIGIN);
|
||||
const html = renderToString(React.createElement(SeoHead, seoProps));
|
||||
|
||||
expect(html).toContain('rel="canonical"');
|
||||
expect(html).toContain("https://www.aeroflot.ru/ru/onlineboard");
|
||||
});
|
||||
|
||||
it("renders OG tags for flight search page", () => {
|
||||
const seoProps = buildFlightSearchSeo(
|
||||
t,
|
||||
{ type: "flight", carrier: "SU", flightNumber: "100", date: "20250115" },
|
||||
LOCALE,
|
||||
ORIGIN,
|
||||
);
|
||||
const html = renderToString(React.createElement(SeoHead, seoProps));
|
||||
|
||||
expect(html).toContain('property="og:title"');
|
||||
expect(html).toContain('property="og:description"');
|
||||
expect(html).toContain('property="og:url"');
|
||||
expect(html).toContain('property="og:image"');
|
||||
expect(html).toContain('property="og:type" content="website"');
|
||||
expect(html).toContain('property="og:locale" content="ru"');
|
||||
expect(html).toContain('property="og:site_name" content="Aeroflot"');
|
||||
});
|
||||
|
||||
it("renders hreflang links for departure search page", () => {
|
||||
const seoProps = buildDepartureSearchSeo(
|
||||
t,
|
||||
{ type: "departure", station: "SVO", date: "20250115" },
|
||||
LOCALE,
|
||||
ORIGIN,
|
||||
);
|
||||
const html = renderToString(React.createElement(SeoHead, seoProps));
|
||||
|
||||
// React renders the JSX prop `hrefLang` as the HTML attribute `hrefLang` (camelCase) in SSR
|
||||
expect(html).toContain('hrefLang="ru"');
|
||||
expect(html).toContain('hrefLang="en"');
|
||||
expect(html).toContain('hrefLang="x-default"');
|
||||
});
|
||||
|
||||
it("renders twitter card meta tag", () => {
|
||||
const seoProps = buildOnlineBoardStartSeo(t, LOCALE, ORIGIN);
|
||||
const html = renderToString(React.createElement(SeoHead, seoProps));
|
||||
|
||||
expect(html).toContain('name="twitter:card" content="summary"');
|
||||
});
|
||||
|
||||
it("renders canonical with correct flight URL for details", () => {
|
||||
const seoProps = buildFlightDetailsSeo(t, DIRECT_FLIGHT, LOCALE, ORIGIN);
|
||||
const html = renderToString(React.createElement(SeoHead, seoProps));
|
||||
|
||||
expect(html).toContain('rel="canonical"');
|
||||
expect(html).toContain("https://www.aeroflot.ru/ru/onlineboard/SU0100-20250115");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests — JSON-LD rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("SEO integration — JSON-LD output", () => {
|
||||
it("produces valid JSON-LD for a single flight", () => {
|
||||
const jsonLd = buildFlightJsonLd(DIRECT_FLIGHT);
|
||||
const json = JSON.stringify(jsonLd);
|
||||
|
||||
expect(json).toContain("@type");
|
||||
expect(json).toContain("Flight");
|
||||
});
|
||||
|
||||
it("produces valid JSON-LD for flight list", () => {
|
||||
const jsonLd = buildFlightListJsonLd(ALL_FLIGHTS, "Departure flights from SVO");
|
||||
const json = JSON.stringify(jsonLd);
|
||||
|
||||
expect(json).toContain("@type");
|
||||
expect(json).toContain("ItemList");
|
||||
});
|
||||
|
||||
it("includes flight number in single flight JSON-LD", () => {
|
||||
const jsonLd = buildFlightJsonLd(DIRECT_FLIGHT);
|
||||
const json = JSON.stringify(jsonLd);
|
||||
|
||||
// Should contain the flight number somewhere in the JSON-LD
|
||||
expect(json).toContain("SU");
|
||||
});
|
||||
|
||||
it("includes departure and arrival airports in JSON-LD", () => {
|
||||
const jsonLd = buildFlightJsonLd(DIRECT_FLIGHT);
|
||||
const json = JSON.stringify(jsonLd);
|
||||
|
||||
expect(json).toContain("SVO");
|
||||
expect(json).toContain("JFK");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Integration tests for the Online Board start page.
|
||||
*
|
||||
* Verifies the search form renders with all mode tabs and
|
||||
* correct fields per search type.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { OnlineBoardStartPage } from "@/features/online-board/components/OnlineBoardStartPage.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const navigateSpy = vi.fn();
|
||||
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useNavigate: () => navigateSpy,
|
||||
useParams: () => ({ lang: "ru" }),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Start page integration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders the search form with data-testid", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
expect(screen.getByTestId("search-form")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders all 4 search mode tabs", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
const radios = screen.getAllByRole("radio");
|
||||
expect(radios).toHaveLength(4);
|
||||
|
||||
const labels = radios.map((r) => (r as HTMLInputElement).value);
|
||||
expect(labels).toEqual(["flight", "departure", "arrival", "route"]);
|
||||
});
|
||||
|
||||
it("defaults to flight search mode", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
const flightRadio = screen.getByDisplayValue("flight") as HTMLInputElement;
|
||||
expect(flightRadio.checked).toBe(true);
|
||||
});
|
||||
|
||||
it("shows flight number input in flight mode", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
expect(screen.getByTestId("flight-number-input")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows departure airport input in departure mode", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
fireEvent.click(screen.getByDisplayValue("departure"));
|
||||
expect(screen.getByTestId("departure-airport-input")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows arrival airport input in arrival mode", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
fireEvent.click(screen.getByDisplayValue("arrival"));
|
||||
expect(screen.getByTestId("arrival-airport-input")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows both departure and arrival inputs in route mode", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
fireEvent.click(screen.getByDisplayValue("route"));
|
||||
expect(screen.getByTestId("departure-airport-input")).toBeTruthy();
|
||||
expect(screen.getByTestId("arrival-airport-input")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("navigates to correct URL on flight search submit", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
const input = screen.getByTestId("flight-number-input");
|
||||
fireEvent.change(input, { target: { value: "SU100" } });
|
||||
|
||||
const form = screen.getByTestId("search-form");
|
||||
fireEvent.submit(form);
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
||||
const url = navigateSpy.mock.calls[0]?.[0] as string;
|
||||
expect(url).toMatch(/^\/ru\/onlineboard\/flight\/SU0100-\d{8}$/);
|
||||
});
|
||||
|
||||
it("navigates to correct URL on departure search submit", () => {
|
||||
render(<OnlineBoardStartPage />);
|
||||
fireEvent.click(screen.getByDisplayValue("departure"));
|
||||
const input = screen.getByTestId("departure-airport-input");
|
||||
fireEvent.change(input, { target: { value: "SVO" } });
|
||||
|
||||
const form = screen.getByTestId("search-form");
|
||||
fireEvent.submit(form);
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
||||
const url = navigateSpy.mock.calls[0]?.[0] as string;
|
||||
expect(url).toMatch(/^\/ru\/onlineboard\/departure\/SVO-\d{8}$/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Integration tests for URL round-tripping.
|
||||
*
|
||||
* Verifies: build URL from params -> parse URL -> params match original.
|
||||
* Also verifies that parsed params produce correct API call parameters.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
buildOnlineBoardUrl,
|
||||
parseOnlineBoardUrl,
|
||||
type OnlineBoardParams,
|
||||
} from "@/features/online-board/url.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("URL round-trip integration", () => {
|
||||
const roundTrip = (params: OnlineBoardParams) => {
|
||||
const url = buildOnlineBoardUrl(params);
|
||||
const parsed = parseOnlineBoardUrl(url);
|
||||
return { url, parsed };
|
||||
};
|
||||
|
||||
it("round-trips start page", () => {
|
||||
const params: OnlineBoardParams = { type: "start" };
|
||||
const { parsed } = roundTrip(params);
|
||||
expect(parsed).toEqual({ type: "start" });
|
||||
});
|
||||
|
||||
it("round-trips flight search", () => {
|
||||
const params: OnlineBoardParams = {
|
||||
type: "flight",
|
||||
carrier: "SU",
|
||||
flightNumber: "100",
|
||||
date: "20250115",
|
||||
};
|
||||
const { url, parsed } = roundTrip(params);
|
||||
expect(url).toBe("onlineboard/flight/SU0100-20250115");
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.type).toBe("flight");
|
||||
if (parsed!.type === "flight") {
|
||||
expect(parsed!.carrier).toBe("SU");
|
||||
// Flight number gets zero-padded to 4 digits during build
|
||||
expect(parsed!.flightNumber).toBe("0100");
|
||||
expect(parsed!.date).toBe("20250115");
|
||||
}
|
||||
});
|
||||
|
||||
it("round-trips flight search with suffix", () => {
|
||||
const params: OnlineBoardParams = {
|
||||
type: "flight",
|
||||
carrier: "SU",
|
||||
flightNumber: "100",
|
||||
suffix: "A",
|
||||
date: "20250115",
|
||||
};
|
||||
const { url, parsed } = roundTrip(params);
|
||||
expect(url).toBe("onlineboard/flight/SU0100A-20250115");
|
||||
expect(parsed).not.toBeNull();
|
||||
if (parsed!.type === "flight") {
|
||||
expect(parsed!.suffix).toBe("A");
|
||||
}
|
||||
});
|
||||
|
||||
it("round-trips departure search", () => {
|
||||
const params: OnlineBoardParams = {
|
||||
type: "departure",
|
||||
station: "SVO",
|
||||
date: "20250115",
|
||||
};
|
||||
const { url, parsed } = roundTrip(params);
|
||||
expect(url).toBe("onlineboard/departure/SVO-20250115");
|
||||
expect(parsed).toEqual(params);
|
||||
});
|
||||
|
||||
it("round-trips departure search with time range", () => {
|
||||
const params: OnlineBoardParams = {
|
||||
type: "departure",
|
||||
station: "SVO",
|
||||
date: "20250115",
|
||||
timeFrom: "0800",
|
||||
timeTo: "1200",
|
||||
};
|
||||
const { url, parsed } = roundTrip(params);
|
||||
expect(url).toBe("onlineboard/departure/SVO-20250115-08001200");
|
||||
expect(parsed).toEqual(params);
|
||||
});
|
||||
|
||||
it("round-trips arrival search", () => {
|
||||
const params: OnlineBoardParams = {
|
||||
type: "arrival",
|
||||
station: "JFK",
|
||||
date: "20250115",
|
||||
};
|
||||
const { url, parsed } = roundTrip(params);
|
||||
expect(url).toBe("onlineboard/arrival/JFK-20250115");
|
||||
expect(parsed).toEqual(params);
|
||||
});
|
||||
|
||||
it("round-trips route search", () => {
|
||||
const params: OnlineBoardParams = {
|
||||
type: "route",
|
||||
departure: "SVO",
|
||||
arrival: "JFK",
|
||||
date: "20250115",
|
||||
};
|
||||
const { url, parsed } = roundTrip(params);
|
||||
expect(url).toBe("onlineboard/route/SVO-JFK-20250115");
|
||||
expect(parsed).toEqual(params);
|
||||
});
|
||||
|
||||
it("round-trips route search with time range", () => {
|
||||
const params: OnlineBoardParams = {
|
||||
type: "route",
|
||||
departure: "SVO",
|
||||
arrival: "JFK",
|
||||
date: "20250115",
|
||||
timeFrom: "0600",
|
||||
timeTo: "1800",
|
||||
};
|
||||
const { url, parsed } = roundTrip(params);
|
||||
expect(url).toBe("onlineboard/route/SVO-JFK-20250115-06001800");
|
||||
expect(parsed).toEqual(params);
|
||||
});
|
||||
|
||||
it("round-trips details URL", () => {
|
||||
const params: OnlineBoardParams = {
|
||||
type: "details",
|
||||
carrier: "SU",
|
||||
flightNumber: "100",
|
||||
date: "20250115",
|
||||
};
|
||||
const { url, parsed } = roundTrip(params);
|
||||
expect(url).toBe("onlineboard/SU0100-20250115");
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.type).toBe("details");
|
||||
});
|
||||
|
||||
it("returns null for invalid URL", () => {
|
||||
expect(parseOnlineBoardUrl("")).toBeNull();
|
||||
expect(parseOnlineBoardUrl("not-a-board-url")).toBeNull();
|
||||
expect(parseOnlineBoardUrl("onlineboard/invalid/")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for malformed flight params", () => {
|
||||
expect(parseOnlineBoardUrl("onlineboard/flight/")).toBeNull();
|
||||
expect(parseOnlineBoardUrl("onlineboard/flight/SU-notadate")).toBeNull();
|
||||
});
|
||||
|
||||
it("handles leading slash in parse", () => {
|
||||
const parsed = parseOnlineBoardUrl("/onlineboard/departure/SVO-20250115");
|
||||
expect(parsed).toEqual({
|
||||
type: "departure",
|
||||
station: "SVO",
|
||||
date: "20250115",
|
||||
});
|
||||
});
|
||||
|
||||
it("build then parse preserves search semantics for all 4 search types", () => {
|
||||
const searchTypes: OnlineBoardParams[] = [
|
||||
{ type: "flight", carrier: "SU", flightNumber: "0100", date: "20250115" },
|
||||
{ type: "departure", station: "SVO", date: "20250115" },
|
||||
{ type: "arrival", station: "JFK", date: "20250115" },
|
||||
{ type: "route", departure: "SVO", arrival: "JFK", date: "20250115" },
|
||||
];
|
||||
|
||||
for (const params of searchTypes) {
|
||||
const url = buildOnlineBoardUrl(params);
|
||||
const parsed = parseOnlineBoardUrl(url);
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.type).toBe(params.type);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user