208 lines
6.8 KiB
TypeScript
208 lines
6.8 KiB
TypeScript
import "@testing-library/jest-dom/vitest";
|
||
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||
import type { UpcomingRouteEvent } from "../src/routeEvents";
|
||
import { UpcomingEventsPanel } from "../src/components/UpcomingEventsPanel";
|
||
|
||
afterEach(cleanup);
|
||
|
||
describe("UpcomingEventsPanel", () => {
|
||
it("shows the nearest event first, preserves chronological list order and filters by kind", () => {
|
||
render(<UpcomingEventsPanel events={events} hasRoute />);
|
||
|
||
const hero = screen.getByText("Nächstes Ereignis").closest("article");
|
||
expect(hero).not.toBeNull();
|
||
expect(within(hero!).getByText("Schleuse Nah")).toBeVisible();
|
||
expect(within(hero!).getByText("3,0 sm")).toBeVisible();
|
||
expect(within(hero!).getByText(/ETA/)).toBeVisible();
|
||
|
||
const list = screen.getByRole("list");
|
||
expect(
|
||
within(list)
|
||
.getAllByRole("button")
|
||
.map((button) => button.textContent)
|
||
).toEqual([
|
||
expect.stringContaining("Schleuse Nah"),
|
||
expect.stringContaining("Brücke Mitte"),
|
||
expect.stringContaining("Hafen Weit")
|
||
]);
|
||
expect(within(list).getAllByText("Infos")).toHaveLength(3);
|
||
|
||
fireEvent.click(screen.getByRole("button", { name: /Brücken/ }));
|
||
expect(screen.getByRole("button", { name: /Brücken/ })).toHaveAttribute(
|
||
"aria-pressed",
|
||
"true"
|
||
);
|
||
expect(screen.getAllByText("Brücke Mitte")).toHaveLength(2);
|
||
expect(screen.queryByText("Schleuse Nah")).not.toBeInTheDocument();
|
||
expect(screen.queryByText("Hafen Weit")).not.toBeInTheDocument();
|
||
expect(within(screen.getByRole("list")).getAllByRole("button")).toHaveLength(1);
|
||
});
|
||
|
||
it("opens a lock drilldown with direct contact, VHF, hours and map actions", () => {
|
||
const onShowOnMap = vi.fn();
|
||
render(
|
||
<UpcomingEventsPanel
|
||
events={events}
|
||
hasRoute
|
||
onShowOnMap={onShowOnMap}
|
||
/>
|
||
);
|
||
|
||
const hero = screen.getByText("Nächstes Ereignis").closest("article");
|
||
fireEvent.click(within(hero!).getByRole("button", { name: /Schleuse Nah/ }));
|
||
|
||
const detail = screen.getByRole("region", { name: "Schleuse Nah" });
|
||
expect(within(detail).getByText("Mo–Fr 08:00–18:00")).toBeVisible();
|
||
expect(within(detail).getByText("Kanal 12")).toBeVisible();
|
||
expect(within(detail).getByRole("link", { name: "Schleuse Nah anrufen" })).toHaveAttribute(
|
||
"href",
|
||
"tel:+4949123456"
|
||
);
|
||
expect(
|
||
within(detail).getByRole("link", { name: "Website von Schleuse Nah öffnen" })
|
||
).toHaveAttribute("href", "https://lock.example/");
|
||
|
||
fireEvent.click(within(detail).getByRole("button", { name: "Auf Karte zeigen" }));
|
||
expect(onShowOnMap).toHaveBeenCalledWith(events[0]);
|
||
|
||
fireEvent.click(within(detail).getByRole("button", { name: "Zurück zur Ereignisliste" }));
|
||
expect(screen.getByText("Nächstes Ereignis")).toBeVisible();
|
||
});
|
||
|
||
it("shows bridge clearance, required height and reserve in its drilldown", () => {
|
||
render(<UpcomingEventsPanel events={events} hasRoute />);
|
||
|
||
fireEvent.click(screen.getByRole("button", { name: /Brücken/ }));
|
||
const hero = screen.getByText("Nächstes Ereignis").closest("article");
|
||
fireEvent.click(within(hero!).getByRole("button", { name: /Brücke Mitte/ }));
|
||
|
||
const detail = screen.getByRole("region", { name: "Brücke Mitte" });
|
||
expect(within(detail).getByText("H 4,2 m")).toBeVisible();
|
||
expect(within(detail).getByText("3.8 m")).toBeVisible();
|
||
const reserve = within(detail).getByText("Reserve").closest("div");
|
||
expect(reserve).toHaveTextContent("0.4 m Reserve");
|
||
expect(
|
||
within(detail).getByRole("link", { name: "Brücke Mitte anrufen" })
|
||
).toHaveAttribute("href", "tel:+4949123457");
|
||
expect(
|
||
within(detail).getByRole("link", { name: "Website von Brücke Mitte öffnen" })
|
||
).toHaveAttribute("href", "https://bridge.example/");
|
||
expect(within(detail).queryByRole("button", { name: "Auf Karte zeigen" })).not.toBeInTheDocument();
|
||
});
|
||
|
||
it.each([
|
||
{
|
||
props: { hasRoute: false, events: [] as UpcomingRouteEvent[] },
|
||
text: "Noch keine Route",
|
||
role: undefined
|
||
},
|
||
{
|
||
props: { hasRoute: true, events: [] as UpcomingRouteEvent[], loading: true },
|
||
text: "Ereignisse werden geladen",
|
||
role: "status"
|
||
},
|
||
{
|
||
props: {
|
||
hasRoute: true,
|
||
events: [] as UpcomingRouteEvent[],
|
||
error: "Datenquelle antwortet nicht"
|
||
},
|
||
text: "Ereignisse nicht erreichbar",
|
||
role: "alert"
|
||
},
|
||
{
|
||
props: { hasRoute: true, events: [] as UpcomingRouteEvent[] },
|
||
text: "Keine bevorstehenden Ereignisse",
|
||
role: undefined
|
||
}
|
||
])("renders the state '$text'", ({ props, text, role }) => {
|
||
render(<UpcomingEventsPanel {...props} />);
|
||
|
||
expect(screen.getByText(text)).toBeVisible();
|
||
if (role) {
|
||
expect(screen.getByRole(role)).toBeVisible();
|
||
}
|
||
});
|
||
});
|
||
|
||
const events: UpcomingRouteEvent[] = [
|
||
{
|
||
kind: "lock",
|
||
id: "lock-near",
|
||
name: "Schleuse Nah",
|
||
coordinate: { lat: 53.1, lon: 7.1 },
|
||
routeDistanceNm: 8,
|
||
distanceFromRouteNm: 0.02,
|
||
remainingNm: 3,
|
||
eta: eta("2026-07-23T12:30:00.000Z", 30),
|
||
feature: {
|
||
id: "lock-near",
|
||
name: "Schleuse Nah",
|
||
coordinate: { lat: 53.1, lon: 7.1 },
|
||
routeDistanceNm: 8,
|
||
distanceFromRouteNm: 0.02,
|
||
openingHours: "Mo–Fr 08:00–18:00",
|
||
phone: "+49 49 123456",
|
||
vhf: "Kanal 12",
|
||
website: "lock.example"
|
||
}
|
||
},
|
||
{
|
||
kind: "harbour",
|
||
id: "harbour-far",
|
||
name: "Hafen Weit",
|
||
coordinate: { lat: 53.3, lon: 7.3 },
|
||
routeDistanceNm: 17,
|
||
distanceFromRouteNm: 0.4,
|
||
remainingNm: 12,
|
||
eta: eta("2026-07-23T14:00:00.000Z", 120),
|
||
feature: {
|
||
id: "harbour-far",
|
||
name: "Hafen Weit",
|
||
coordinate: { lat: 53.3, lon: 7.3 },
|
||
kind: "marina",
|
||
amenities: { water: "available", electricity: true },
|
||
phone: null,
|
||
website: null
|
||
}
|
||
},
|
||
{
|
||
kind: "bridge",
|
||
id: "bridge-middle",
|
||
name: "Brücke Mitte",
|
||
coordinate: { lat: 53.2, lon: 7.2 },
|
||
routeDistanceNm: 11,
|
||
distanceFromRouteNm: 0.01,
|
||
remainingNm: 6,
|
||
eta: eta("2026-07-23T13:00:00.000Z", 60),
|
||
feature: {
|
||
id: "bridge-middle",
|
||
name: "Brücke Mitte",
|
||
label: "Brücke Mitte H 4,2 m",
|
||
coordinate: { lat: 53.2, lon: 7.2 },
|
||
distanceNm: 0.01,
|
||
clearanceM: 4.2,
|
||
clearanceLabel: "H 4,2 m",
|
||
requiredAirDraftM: 3.8,
|
||
marginM: 0.4,
|
||
status: "tight",
|
||
source: "Test",
|
||
phone: "+49 49 123457",
|
||
website: "bridge.example",
|
||
operator: "Brückenamt"
|
||
}
|
||
}
|
||
];
|
||
|
||
function eta(estimatedAt: string, minutesFromProgress: number) {
|
||
return {
|
||
estimatedAt,
|
||
minutesFromProgress,
|
||
speedKn: 6,
|
||
speedSource: "vessel-cruise-speed" as const,
|
||
referenceTime: "2026-07-23T12:00:00.000Z",
|
||
referenceSource: "current-time" as const
|
||
};
|
||
}
|