diff --git a/src/features/online-board/hooks/useFlightDetails.test.ts b/src/features/online-board/hooks/useFlightDetails.test.ts new file mode 100644 index 00000000..30cb6f03 --- /dev/null +++ b/src/features/online-board/hooks/useFlightDetails.test.ts @@ -0,0 +1,154 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { useFlightDetails } from "./useFlightDetails.js"; +import type { IBoardResponse, ISimpleFlight } from "../types.js"; + +const mockClient = { get: vi.fn() }; + +vi.mock("@/shared/api/provider.js", () => ({ + useApiClient: () => mockClient, +})); + +vi.mock("../api.js", async () => { + const actual = await vi.importActual("../api.js"); + return { + ...actual, + getFlightDetails: (_client: unknown, _params: unknown) => mockClient.get(), + }; +}); + +function makeFlight(id: string): ISimpleFlight { + return { + id, + routeType: "Direct", + flyingTime: "1h", + status: "Scheduled", + flightId: { + carrier: "SU", + flightNumber: "0022", + suffix: "", + date: "20260416", + }, + operatingBy: {}, + leg: { + arrival: { + scheduled: { + airport: "", + airportCode: "", + city: "", + cityCode: "", + countryCode: "", + }, + latest: { + airport: "", + airportCode: "", + city: "", + cityCode: "", + countryCode: "", + }, + dispatch: "", + gate: "", + terminal: "", + times: { + scheduledArrival: { + dayChange: { value: 0, title: "" }, + local: "", + localTime: "", + tzOffset: 0, + utc: "", + }, + }, + }, + dayChange: 0, + departure: { + scheduled: { + airport: "", + airportCode: "", + city: "", + cityCode: "", + countryCode: "", + }, + latest: { + airport: "", + airportCode: "", + city: "", + cityCode: "", + countryCode: "", + }, + dispatch: "", + gate: "", + terminal: "", + checkingStatus: "Scheduled", + parkingStand: "", + times: { + scheduledDeparture: { + dayChange: { value: 0, title: "" }, + local: "", + localTime: "", + tzOffset: 0, + utc: "", + }, + }, + }, + equipment: {}, + flags: { + checkinAvailable: false, + returnToAirport: false, + routeChanged: false, + }, + flyingTime: "1h", + index: 0, + operatingBy: {}, + status: "Scheduled", + updated: "", + }, + } as unknown as ISimpleFlight; +} + +describe("useFlightDetails", () => { + beforeEach(() => { + mockClient.get.mockReset(); + }); + + it("returns first flight and full array in allFlights", async () => { + const response: IBoardResponse = { + data: { + partners: [], + routes: [ + makeFlight("SU0022-20260416"), + makeFlight("SU0022-20260417"), + makeFlight("SU0022-20260418"), + ], + daysOfFlight: [], + }, + }; + mockClient.get.mockResolvedValue(response); + + const { result } = renderHook(() => + useFlightDetails({ flights: "SU0022", dates: "2026-04-16" }), + ); + + await waitFor(() => expect(result.current.loading).toBe(false)); + + expect(result.current.flight?.id).toBe("SU0022-20260416"); + expect(result.current.allFlights).toHaveLength(3); + expect(result.current.allFlights[0]?.id).toBe("SU0022-20260416"); + }); + + it("returns empty allFlights array when response has no routes", async () => { + const response: IBoardResponse = { + data: { partners: [], routes: [], daysOfFlight: [] }, + }; + mockClient.get.mockResolvedValue(response); + + const { result } = renderHook(() => + useFlightDetails({ flights: "SU0022", dates: "2026-04-16" }), + ); + + await waitFor(() => expect(result.current.loading).toBe(false)); + + expect(result.current.flight).toBeNull(); + expect(result.current.allFlights).toEqual([]); + }); +}); diff --git a/src/features/online-board/hooks/useFlightDetails.ts b/src/features/online-board/hooks/useFlightDetails.ts index c96c9325..453ed156 100644 --- a/src/features/online-board/hooks/useFlightDetails.ts +++ b/src/features/online-board/hooks/useFlightDetails.ts @@ -2,7 +2,8 @@ * React hook for the flight details page. * * Calls `getFlightDetails` on param change, manages loading/error/data state. - * Thin wrapper — integration-tested by 2E/2H, not unit-tested here. + * Returns both the first flight (for primary display) and the full routes + * array (for the mini-list sidebar). * * @module */ @@ -16,19 +17,20 @@ import type { ApiError } from "@/shared/api/errors.js"; export interface UseFlightDetailsResult { flight: ISimpleFlight | null; + allFlights: ISimpleFlight[]; loading: boolean; error: ApiError | null; } /** - * Hook for the flight details page. Fetches a single flight's details - * based on flight ID params. + * Hook for the flight details page. Fetches the details response and exposes + * both the first route (primary flight) and the full routes array. */ export function useFlightDetails( params: FlightDetailsParams, ): UseFlightDetailsResult { const client = useApiClient(); - const [flight, setFlight] = useState(null); + const [allFlights, setAllFlights] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -43,9 +45,7 @@ export function useFlightDetails( getFlightDetails(client, paramsRef.current) .then((response) => { if (!cancelled) { - // Details endpoint returns a board response; take the first route - const firstFlight = response.data.routes[0] ?? null; - setFlight(firstFlight); + setAllFlights(response.data.routes); setLoading(false); } }) @@ -61,5 +61,10 @@ export function useFlightDetails( }; }, [client, params.flights, params.dates]); - return { flight, loading, error }; + return { + flight: allFlights[0] ?? null, + allFlights, + loading, + error, + }; }