Files
flights_web/tests/integration/online-board/flight-search.test.tsx
T
gnezim 84e6d265fc Align board search + details with Angular visual parity
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.
2026-04-17 23:14:59 +03:00

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);
});
});