Initial Watermaps import

This commit is contained in:
BuTzZ
2026-07-24 11:29:24 +02:00
commit 57f7b4dedb
129 changed files with 43136 additions and 0 deletions
@@ -0,0 +1,197 @@
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")
]);
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("MoFr 08:0018: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).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: "MoFr 08:0018: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"
}
}
];
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
};
}