Wire DayTabs into OnlineBoardDetailsPage stickyContent

This commit is contained in:
2026-04-17 00:30:53 +03:00
parent bd147dabe1
commit 50e3f1b961
4 changed files with 90 additions and 9 deletions
@@ -77,6 +77,7 @@ const mockFlight: IDirectFlight = {
let mockState = {
flight: mockFlight as IDirectFlight | null,
allFlights: [mockFlight] as IDirectFlight[],
daysOfFlight: ["20250115"] as string[],
loading: false,
error: null as Error | null,
};
@@ -85,6 +86,17 @@ vi.mock("../hooks/useFlightDetails.js", () => ({
useFlightDetails: () => mockState,
}));
vi.mock("@/shared/hooks/useAppSettings.js", () => ({
useAppSettings: () => ({
onlineboardSearchFrom: 2,
onlineboardSearchTo: 14,
scheduleSearchFrom: 30,
scheduleSearchTo: 30,
loading: false,
error: null,
}),
}));
vi.mock("../hooks/useLiveFlightDetails.js", () => ({
useLiveFlightDetails: (_id: unknown, initialFlight: unknown) => ({
flight: initialFlight,
@@ -120,6 +132,7 @@ describe("OnlineBoardDetailsPage", () => {
mockState = {
flight: mockFlight,
allFlights: [mockFlight],
daysOfFlight: ["20250115"],
loading: false,
error: null,
};
@@ -133,19 +146,19 @@ describe("OnlineBoardDetailsPage", () => {
});
it("renders loading skeleton", () => {
mockState = { flight: null, allFlights: [], loading: true, error: null };
mockState = { flight: null, allFlights: [], daysOfFlight: [], loading: true, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
expect(screen.queryByTestId("flight-details")).toBeNull();
});
it("renders error state", () => {
mockState = { flight: null, allFlights: [], loading: false, error: new Error("fail") };
mockState = { flight: null, allFlights: [], daysOfFlight: [], loading: false, error: new Error("fail") };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
expect(screen.getByTestId("flight-details-error")).toBeTruthy();
});
it("renders not-found state", () => {
mockState = { flight: null, allFlights: [], loading: false, error: null };
mockState = { flight: null, allFlights: [], daysOfFlight: [], loading: false, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
expect(screen.getByTestId("flight-details-not-found")).toBeTruthy();
});
@@ -208,7 +221,7 @@ describe("OnlineBoardDetailsPage", () => {
},
},
};
mockState = { flight: flightWithTransition, allFlights: [flightWithTransition], loading: false, error: null };
mockState = { flight: flightWithTransition, allFlights: [flightWithTransition], daysOfFlight: ["20250115"], loading: false, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(screen.getByTestId("flight-details-accordion")).toBeTruthy();
});
@@ -218,21 +231,29 @@ describe("OnlineBoardDetailsPage", () => {
it("renders mini-list when multiple flights are returned", () => {
const first = mockFlight;
const second = { ...mockFlight, id: "SU0022-20260417", flightId: { ...mockFlight.flightId, date: "20260417" } };
mockState = { flight: first, allFlights: [first, second], loading: false, error: null };
mockState = { flight: first, allFlights: [first, second], daysOfFlight: ["20250115", "20260417"], loading: false, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(screen.getByTestId("flights-mini-list")).toBeTruthy();
});
it("does not render mini-list when only one flight is returned", () => {
mockState = { flight: mockFlight, allFlights: [mockFlight], loading: false, error: null };
mockState = { flight: mockFlight, allFlights: [mockFlight], daysOfFlight: ["20250115"], loading: false, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(screen.queryByTestId("flights-mini-list")).toBeNull();
});
it("renders inside PageLayout (has page-layout class)", () => {
mockState = { flight: mockFlight, allFlights: [mockFlight], loading: false, error: null };
mockState = { flight: mockFlight, allFlights: [mockFlight], daysOfFlight: ["20250115"], loading: false, error: null };
const { container } = render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(container.querySelector(".page-layout")).toBeTruthy();
});
});
describe("day tabs integration", () => {
it("renders DayTabs as sticky content", () => {
mockState = { flight: mockFlight, allFlights: [mockFlight], daysOfFlight: ["20260416"], loading: false, error: null };
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://example.com" />);
expect(screen.getByTestId("day-tabs")).toBeTruthy();
});
});
});
@@ -7,7 +7,8 @@
* @module
*/
import type { FC } from "react";
import { useCallback, type FC } from "react";
import { useNavigate } from "@modern-js/runtime/router";
import { useTranslation } from "@/i18n/provider.js";
import "./OnlineBoardDetailsPage.scss";
import { FlightCard } from "@/ui/flights/FlightCard.js";
@@ -16,12 +17,15 @@ import { SeoHead } from "@/ui/seo/SeoHead.js";
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
import { PageLayout } from "@/ui/layout/PageLayout.js";
import { PageTabs } from "@/ui/layout/PageTabs.js";
import { useAppSettings } from "@/shared/hooks/useAppSettings.js";
import { useFlightDetails } from "../hooks/useFlightDetails.js";
import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js";
import { buildFlightDetailsSeo } from "../seo.js";
import { buildFlightJsonLd } from "../json-ld.js";
import { buildOnlineBoardUrl } from "../url.js";
import { FlightDetailsAccordion } from "./details-panels/FlightDetailsAccordion.js";
import { FlightsMiniList } from "./FlightsMiniList/index.js";
import { DayTabs } from "./DayTabs/index.js";
import type { IParsedFlightId, IFlightLeg } from "../types.js";
export interface OnlineBoardDetailsPageProps {
@@ -152,7 +156,7 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
flights: `${flightId.carrier}${flightId.flightNumber}${flightId.suffix ?? ""}`,
dates: `${flightId.date.slice(0, 4)}-${flightId.date.slice(4, 6)}-${flightId.date.slice(6, 8)}`,
};
const { flight: firstFlight, allFlights, loading, error } = useFlightDetails(detailsParams);
const { flight: firstFlight, allFlights, daysOfFlight, loading, error } = useFlightDetails(detailsParams);
// Pick the flight matching the URL's flightId (date-based match). The API
// response may contain multiple flights with the same flight number on
@@ -168,6 +172,23 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
const displayFlight = connectionStatus === "live" && liveFlight ? liveFlight : flight;
const { onlineboardSearchFrom, onlineboardSearchTo } = useAppSettings();
const navigate = useNavigate();
const handleNavigateDate = useCallback(
(newDate: string) => {
const url = buildOnlineBoardUrl({
type: "details",
carrier: flightId.carrier,
flightNumber: flightId.flightNumber,
...(flightId.suffix ? { suffix: flightId.suffix } : {}),
date: newDate,
});
void navigate(`/${locale}/${url}`);
},
[flightId.carrier, flightId.flightNumber, flightId.suffix, locale, navigate],
);
const onlineboardHref = `/${locale}/onlineboard`;
const commonLayoutProps = {
headerLeft: <PageTabs viewType="onlineboard" />,
@@ -228,6 +249,16 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
lang={locale}
/>
}
stickyContent={
<DayTabs
selectedDate={flightId.date}
availableDates={daysOfFlight}
daysBefore={onlineboardSearchFrom}
daysAfter={onlineboardSearchTo}
locale={locale}
onNavigate={handleNavigateDate}
/>
}
>
<div className="flight-details" data-testid="flight-details">
{/* Connection status */}
@@ -76,6 +76,17 @@ vi.mock("@/features/online-board/hooks/useLiveFlightDetails.js", () => ({
}),
}));
vi.mock("@/shared/hooks/useAppSettings.js", () => ({
useAppSettings: () => ({
onlineboardSearchFrom: 2,
onlineboardSearchTo: 14,
scheduleSearchFrom: 30,
scheduleSearchTo: 30,
loading: false,
error: null,
}),
}));
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
@@ -178,6 +189,8 @@ describe("Details page error handling", () => {
it("renders error state for details API failure", () => {
mockUseFlightDetails.mockReturnValue({
flight: null,
allFlights: [],
daysOfFlight: [],
loading: false,
error: new ApiHttpError("HTTP 500", 500),
});
@@ -195,6 +208,8 @@ describe("Details page error handling", () => {
it("renders not-found when details returns null", () => {
mockUseFlightDetails.mockReturnValue({
flight: null,
allFlights: [],
daysOfFlight: [],
loading: false,
error: null,
});
@@ -50,6 +50,17 @@ vi.mock("@/features/online-board/hooks/useLiveFlightDetails.js", () => ({
useLiveFlightDetails: (...args: unknown[]) => mockUseLiveFlightDetails(...args),
}));
vi.mock("@/shared/hooks/useAppSettings.js", () => ({
useAppSettings: () => ({
onlineboardSearchFrom: 2,
onlineboardSearchTo: 14,
scheduleSearchFrom: 30,
scheduleSearchTo: 30,
loading: false,
error: null,
}),
}));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -64,6 +75,7 @@ function setupWithFlight(flight: ISimpleFlight = DIRECT_FLIGHT) {
mockUseFlightDetails.mockReturnValue({
flight,
allFlights: [flight],
daysOfFlight: ["20250115"],
loading: false,
error: null,
});
@@ -179,6 +191,7 @@ describe("Flight details page integration", () => {
mockUseFlightDetails.mockReturnValue({
flight: null,
allFlights: [],
daysOfFlight: [],
loading: false,
error: new Error("API error"),
});
@@ -201,6 +214,7 @@ describe("Flight details page integration", () => {
mockUseFlightDetails.mockReturnValue({
flight: null,
allFlights: [],
daysOfFlight: [],
loading: false,
error: null,
});