diff --git a/src/features/online-board/components/FlightsMiniList/FlightsMiniList.test.tsx b/src/features/online-board/components/FlightsMiniList/FlightsMiniList.test.tsx new file mode 100644 index 00000000..fbc9b540 --- /dev/null +++ b/src/features/online-board/components/FlightsMiniList/FlightsMiniList.test.tsx @@ -0,0 +1,121 @@ +// @vitest-environment jsdom +import { forwardRef } from "react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { FlightsMiniList } from "./FlightsMiniList.js"; +import type { ISimpleFlight } from "../../types.js"; + +// Match the router mock convention used across the project. +// Forward the ref so parent-held refs to list items still resolve to DOM nodes. +vi.mock("@modern-js/runtime/router", () => ({ + Link: forwardRef( + ({ children, to, ...props }, ref) => ( + {children} + ), + ), +})); + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ t: (k: string) => k }), +})); + +// Mock scrollIntoView (not implemented in jsdom) +beforeEach(() => { + Element.prototype.scrollIntoView = vi.fn(); +}); + +function makeFlight(id: string, date = "20260416"): ISimpleFlight { + return { + id, + routeType: "Direct" as const, + flyingTime: "1h", + status: "Scheduled" as const, + flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date }, + operatingBy: {}, + leg: { + arrival: { + scheduled: { airport: "LED", airportCode: "LED", city: "SPB", cityCode: "LED", countryCode: "RU" }, + latest: { airport: "LED", airportCode: "LED", city: "SPB", cityCode: "LED", countryCode: "RU" }, + dispatch: "", gate: "", terminal: "", + times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "12:30", localTime: "", tzOffset: 0, utc: "" } }, + }, + dayChange: 0, + departure: { + scheduled: { airport: "SVO", airportCode: "SVO", city: "MOW", cityCode: "MOW", countryCode: "RU" }, + latest: { airport: "SVO", airportCode: "SVO", city: "MOW", cityCode: "MOW", countryCode: "RU" }, + dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "", + times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "10:00", localTime: "", tzOffset: 0, utc: "" } }, + }, + equipment: {}, + flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false }, + flyingTime: "1h", + index: 0, + operatingBy: {}, + status: "Scheduled" as const, + updated: "", + }, + }; +} + +describe("FlightsMiniList", () => { + it("returns null when flights array is empty", () => { + const current = makeFlight("SU0022-20260416"); + const { container } = render( + , + ); + expect(container.firstChild).toBeNull(); + }); + + it("returns null when flights array has only one flight", () => { + const current = makeFlight("SU0022-20260416"); + const { container } = render( + , + ); + expect(container.firstChild).toBeNull(); + }); + + it("renders one item per flight when multiple flights", () => { + const flights = [ + makeFlight("SU0022-20260416", "20260416"), + makeFlight("SU0022-20260417", "20260417"), + makeFlight("SU0022-20260418", "20260418"), + ]; + render(); + expect(screen.getByTestId("mini-list-item-SU0022-20260416")).toBeTruthy(); + expect(screen.getByTestId("mini-list-item-SU0022-20260417")).toBeTruthy(); + expect(screen.getByTestId("mini-list-item-SU0022-20260418")).toBeTruthy(); + }); + + it("highlights the item matching currentFlight.id", () => { + const flights = [ + makeFlight("SU0022-20260416", "20260416"), + makeFlight("SU0022-20260417", "20260417"), + ]; + render(); + const selected = screen.getByTestId("mini-list-item-SU0022-20260417"); + const notSelected = screen.getByTestId("mini-list-item-SU0022-20260416"); + expect(selected.className).toMatch(/--selected/); + expect(notSelected.className).not.toMatch(/--selected/); + }); + + it("calls scrollIntoView on the selected item after mount", async () => { + const flights = [ + makeFlight("SU0022-20260416", "20260416"), + makeFlight("SU0022-20260417", "20260417"), + ]; + render(); + await Promise.resolve(); + expect(Element.prototype.scrollIntoView).toHaveBeenCalledWith( + expect.objectContaining({ block: "center", behavior: "smooth" }), + ); + }); + + it("has data-testid on the container", () => { + const flights = [ + makeFlight("SU0022-20260416", "20260416"), + makeFlight("SU0022-20260417", "20260417"), + ]; + render(); + expect(screen.getByTestId("flights-mini-list")).toBeTruthy(); + }); +}); diff --git a/src/features/online-board/components/FlightsMiniList/FlightsMiniList.tsx b/src/features/online-board/components/FlightsMiniList/FlightsMiniList.tsx new file mode 100644 index 00000000..26bb459a --- /dev/null +++ b/src/features/online-board/components/FlightsMiniList/FlightsMiniList.tsx @@ -0,0 +1,49 @@ +import { type FC, useEffect, useRef } from "react"; +import type { ISimpleFlight } from "../../types.js"; +import { FlightsMiniListItem } from "./FlightsMiniListItem.js"; +import "./FlightsMiniList.scss"; + +export interface FlightsMiniListProps { + flights: ISimpleFlight[]; + currentFlight: ISimpleFlight; + lang: string; +} + +export const FlightsMiniList: FC = ({ + flights, + currentFlight, + lang, +}) => { + const itemRefs = useRef>(new Map()); + + useEffect(() => { + const selected = itemRefs.current.get(currentFlight.id); + if (selected) { + selected.scrollIntoView({ block: "center", behavior: "smooth" }); + } + }, [currentFlight.id]); + + if (flights.length <= 1) { + return null; + } + + return ( +
+ {flights.map((flight) => ( + { + if (el) { + itemRefs.current.set(flight.id, el); + } else { + itemRefs.current.delete(flight.id); + } + }} + flight={flight} + isSelected={flight.id === currentFlight.id} + lang={lang} + /> + ))} +
+ ); +}; diff --git a/src/features/online-board/components/FlightsMiniList/index.ts b/src/features/online-board/components/FlightsMiniList/index.ts new file mode 100644 index 00000000..1c0b05a7 --- /dev/null +++ b/src/features/online-board/components/FlightsMiniList/index.ts @@ -0,0 +1,4 @@ +export { FlightsMiniList } from "./FlightsMiniList.js"; +export type { FlightsMiniListProps } from "./FlightsMiniList.js"; +export { FlightsMiniListItem } from "./FlightsMiniListItem.js"; +export type { FlightsMiniListItemProps } from "./FlightsMiniListItem.js";