import "@testing-library/jest-dom/vitest"; import { cleanup, render, screen, within } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; import type { MarineForecast, TideSummary } from "@watermaps/shared"; import { ConditionsPanel, type ConditionsRouteWeatherReport, type ConditionsRouteWeatherSample } from "../src/components/ConditionsPanel"; afterEach(cleanup); const NOW = Date.parse("2026-07-23T10:00:00.000Z"); describe("ConditionsPanel", () => { it("labels GPS as the source and shows the current marine weather metrics", () => { render( ); expect(screen.getByRole("complementary", { name: "Wetter & Tide" })).toHaveAttribute( "data-position-source", "gps" ); expect(screen.getByText("Aktuelle GPS-Position")).toBeVisible(); const currentConditions = screen .getByRole("heading", { name: "Aktuelle Bedingungen" }) .closest("section"); expect(currentConditions).not.toBeNull(); expect(within(currentConditions!).getByText("12 kn · 270° W")).toBeVisible(); expect(within(currentConditions!).getByText("0.8 m · 6 s · 225° SW")).toBeVisible(); expect(within(currentConditions!).getByText("0.7 kn · 090° O")).toBeVisible(); expect(within(currentConditions!).getByText("14.2 °C")).toBeVisible(); expect(within(currentConditions!).getByText("vor 15 Min.")).toBeVisible(); expect(within(currentConditions!).getByText("Quelle: Test-Meteo")).toBeVisible(); }); it("shows the nearest tide station and both high- and low-water events", () => { render( ); expect(screen.getByText("GPS · Außenhafen")).toBeVisible(); expect(screen.getByText("Pegel Emden")).toBeVisible(); expect(screen.getByText("4,2 km entfernt")).toBeVisible(); const tideEvents = document.querySelector(".conditions-tide-events"); expect(tideEvents).not.toBeNull(); expect(tideEvents).toHaveTextContent(/HW.*1\.72 m/); expect(tideEvents).toHaveTextContent(/NW.*0\.31 m/); expect(within(tideEvents!).getByTitle("Nächstes Hochwasser")).toHaveTextContent("HW"); expect(within(tideEvents!).getByTitle("Nächstes Niedrigwasser")).toHaveTextContent("NW"); }); it("renders Start, Mitte and Ziel with weather and the middle tide", () => { const routeWeatherReport: ConditionsRouteWeatherReport = { severity: "caution", summary: "Wind nimmt zur Mitte der Strecke zu.", source: "Streckenprognose", updatedAt: "2026-07-23T09:40:00.000Z", unavailableSamples: 0, samples: [ routeSample("Start", { windSpeed: 8, waveHeightM: 0.3 }), routeSample("Mitte", { windSpeed: 17, waveHeightM: 0.9 }, -0.2), routeSample("Ziel", { windSpeed: 11, waveHeightM: 0.5 }) ] }; render( ); expect(screen.getByText("Fallback · Routenstart")).toBeVisible(); expect(screen.getByText("Wind nimmt zur Mitte der Strecke zu.")).toBeVisible(); const start = routeCard("Start"); const middle = routeCard("Mitte"); const destination = routeCard("Ziel"); expect(start).toHaveTextContent("Startpegel · 2,1 km"); expect(middle).toHaveTextContent("17 kn"); expect(middle).toHaveTextContent("-0.2 kn entlang Route"); expect(middle).toHaveTextContent("Mittelplate · 7,5 km"); expect(middle).toHaveTextContent(/HW.*1\.48 m/); expect(middle).toHaveTextContent(/NW.*0\.18 m/); expect(destination).toHaveTextContent("Zielpegel · 3,4 km"); }); it("keeps partial data visible while reporting current and route failures", () => { const partialReport: ConditionsRouteWeatherReport = { severity: "caution", summary: "Streckenprognose ist unvollständig.", unavailableSamples: 2, samples: [routeSample("Start", { windSpeed: 9, waveHeightM: 0.4 })] }; render( ); expect(screen.getAllByRole("alert")).toHaveLength(2); expect(screen.getByText("Tidendaten nicht erreichbar.")).toBeVisible(); expect(screen.getByText("Streckentiden nur teilweise erreichbar.")).toBeVisible(); const currentConditions = screen .getByRole("heading", { name: "Aktuelle Bedingungen" }) .closest("section"); expect(currentConditions).not.toBeNull(); expect(within(currentConditions!).getByText("9 kn · 270° W")).toBeVisible(); expect(screen.getByText("Für diese Position fehlt eine passende Tidenstation.")).toBeVisible(); expect(screen.getByText("Für 2 Streckenpunkte fehlt die Prognose.")).toBeVisible(); expect(routeCard("Start")).toHaveTextContent("9 kn"); expect(routeCard("Mitte")).toHaveTextContent("Keine Wetterprognose für diesen Streckenpunkt."); expect(routeCard("Ziel")).toHaveTextContent("Keine Wetterprognose für diesen Streckenpunkt."); expect(screen.getAllByText(/Keine passende Tide für/)).toHaveLength(3); }); it("shows useful empty states when neither position nor route conditions exist", () => { render( ); expect(screen.getByText("Positionsquelle noch offen")).toBeVisible(); expect( screen.getByText("Für diese Position liegen noch keine Wetter- oder Tidendaten vor.") ).toBeVisible(); expect( screen.getByText("Nach der Routenberechnung erscheinen hier Start, Mitte und Ziel.") ).toBeVisible(); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); }); }); function routeCard(label: "Start" | "Mitte" | "Ziel") { const card = screen.getByRole("heading", { name: label, level: 4 }).closest("article"); expect(card).not.toBeNull(); return card!; } function routeSample( label: ConditionsRouteWeatherSample["label"], overrides: Partial, currentAlongRouteKn: number | null = 0.3 ): ConditionsRouteWeatherSample { return { label, plannedTime: `2026-07-23T${label === "Start" ? "10" : label === "Mitte" ? "12" : "14"}:00:00.000Z`, currentAlongRouteKn, forecast: forecast(overrides) }; } function forecast(overrides: Partial = {}): MarineForecast { return { waveHeightM: 0.8, waveDirectionDeg: 225, wavePeriodS: 5.6, windSpeed: 12.4, windDirectionDeg: 270, temperatureC: 14.2, oceanCurrentSpeedKn: 0.7, oceanCurrentDirectionDeg: 90, forecastTime: "2026-07-23T10:00:00.000Z", source: "Test-Meteo", updatedAt: "2026-07-23T09:45:00.000Z", ...overrides }; } function tideSummary( station: string, distanceKm: number, highWaterM: number, lowWaterM: number ): TideSummary { return { station, distanceKm, nextHigh: { type: "high", time: "2026-07-23T12:30:00.000Z", heightM: highWaterM }, nextLow: { type: "low", time: "2026-07-23T18:45:00.000Z", heightM: lowWaterM }, waterLevelCurve: [], source: "Test-Tide", updatedAt: "2026-07-23T09:50:00.000Z" }; }