Add FlightsMiniList container with scroll-into-view behavior
This commit is contained in:
@@ -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<HTMLAnchorElement, { children: React.ReactNode; to: string; [k: string]: unknown }>(
|
||||
({ children, to, ...props }, ref) => (
|
||||
<a ref={ref} href={to} {...props}>{children}</a>
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
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(
|
||||
<FlightsMiniList flights={[]} currentFlight={current} lang="ru" />,
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when flights array has only one flight", () => {
|
||||
const current = makeFlight("SU0022-20260416");
|
||||
const { container } = render(
|
||||
<FlightsMiniList flights={[current]} currentFlight={current} lang="ru" />,
|
||||
);
|
||||
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(<FlightsMiniList flights={flights} currentFlight={flights[0]!} lang="ru" />);
|
||||
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(<FlightsMiniList flights={flights} currentFlight={flights[1]!} lang="ru" />);
|
||||
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(<FlightsMiniList flights={flights} currentFlight={flights[1]!} lang="ru" />);
|
||||
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(<FlightsMiniList flights={flights} currentFlight={flights[0]!} lang="ru" />);
|
||||
expect(screen.getByTestId("flights-mini-list")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -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<FlightsMiniListProps> = ({
|
||||
flights,
|
||||
currentFlight,
|
||||
lang,
|
||||
}) => {
|
||||
const itemRefs = useRef<Map<string, HTMLAnchorElement>>(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 (
|
||||
<section className="mini-list" data-testid="flights-mini-list">
|
||||
{flights.map((flight) => (
|
||||
<FlightsMiniListItem
|
||||
key={flight.id}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
itemRefs.current.set(flight.id, el);
|
||||
} else {
|
||||
itemRefs.current.delete(flight.id);
|
||||
}
|
||||
}}
|
||||
flight={flight}
|
||||
isSelected={flight.id === currentFlight.id}
|
||||
lang={lang}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user