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(); 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( ); 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(); 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(); 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 }; }