84e6d265fc
Both pages were rendering content directly on the dark-blue page background, which made the flight list and details card effectively invisible. Angular wraps the same content in a white card (section.frame) with drop shadow. Changes: - Wrap FlightList in <section class='frame'> on the search page and wrap the details body the same way. - Replace the inline numbered .calendar-day strip on the search page with the existing <DayTabs> component — the same one the details page already uses (weekday + day + month labels, ‹/› paging). - Pass onFlightClick through FlightList into FlightCard so whole rows are keyboard-accessible buttons, matching Angular's row-level click. The off-screen data-testid='flight-link-*' buttons stay for e2e. - Fix 'Leg NaN' header + the React key warning in FlightLegs when the API returns a Direct leg without an index field. - Update the existing flight-search integration test to target the DayTabs testid instead of the old ad-hoc calendar-strip one. A docs/parity-report-2026-04-17.md file captures the diffs I applied and a punch list of the remaining parity gaps (operator logo on rows, delay/day-change glyphs, Share button visibility on board details, the aircraft panel as a table). Those need per-component work against the Angular templates and will follow in a separate pass.
273 lines
8.1 KiB
TypeScript
273 lines
8.1 KiB
TypeScript
/**
|
|
* 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" }),
|
|
Link: ({ children, ...props }: Record<string, unknown>) =>
|
|
<a {...props}>{children as React.ReactNode}</a>,
|
|
}));
|
|
|
|
// Mock i18n
|
|
vi.mock("@/i18n/provider.js", () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
i18n: { language: "ru" },
|
|
}),
|
|
}));
|
|
|
|
// Mock layout components to avoid transitive dependency issues
|
|
vi.mock("@/ui/layout/PageTabs.js", () => ({
|
|
PageTabs: () => <div data-testid="page-tabs" />,
|
|
}));
|
|
|
|
vi.mock("@/features/online-board/components/OnlineBoardFilter.js", () => ({
|
|
OnlineBoardFilter: () => <div data-testid="online-board-filter" />,
|
|
}));
|
|
|
|
vi.mock("@/features/flights-map/hooks/useFeatureFlag.js", () => ({
|
|
useFeatureFlag: () => false,
|
|
}));
|
|
|
|
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 (DayTabs) with pagination arrows", () => {
|
|
setupMocksWithData();
|
|
render(<OnlineBoardSearchPage params={DEPARTURE_PARAMS} />);
|
|
// Pre-refactor the sticky strip was an ad-hoc DOM with data-testid
|
|
// "calendar-strip". We now use the shared <DayTabs> component, whose
|
|
// root testid is "day-tabs". Assert its structural markers instead.
|
|
expect(screen.getByTestId("day-tabs")).toBeTruthy();
|
|
expect(screen.getByTestId("day-tabs-prev")).toBeTruthy();
|
|
expect(screen.getByTestId("day-tabs-next")).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: "2025-01-15T00:00:00",
|
|
dateTo: "2025-01-16T00:00:00",
|
|
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: "2025-01-15T00:00:00",
|
|
dateTo: "2025-01-16T00:00:00",
|
|
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: "2025-01-15T00:00:00",
|
|
dateTo: "2025-01-16T00:00:00",
|
|
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);
|
|
});
|
|
});
|