Initial Watermaps import
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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(
|
||||
<ConditionsPanel
|
||||
forecast={forecast()}
|
||||
tide={null}
|
||||
positionSource={{ kind: "gps" }}
|
||||
now={NOW}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<ConditionsPanel
|
||||
forecast={null}
|
||||
tide={tideSummary("Pegel Emden", 4.2, 1.72, 0.31)}
|
||||
positionSource={{ kind: "gps", label: "Außenhafen" }}
|
||||
now={NOW}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<ConditionsPanel
|
||||
forecast={null}
|
||||
tide={null}
|
||||
positionSource={{ kind: "fallback", label: "Routenstart" }}
|
||||
routeWeatherReport={routeWeatherReport}
|
||||
routeTides={{
|
||||
start: tideSummary("Startpegel", 2.1, 1.1, 0.2),
|
||||
middle: tideSummary("Mittelplate", 7.5, 1.48, 0.18),
|
||||
destination: tideSummary("Zielpegel", 3.4, 1.25, 0.14)
|
||||
}}
|
||||
now={NOW}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<ConditionsPanel
|
||||
forecast={forecast({ windSpeed: 9 })}
|
||||
tide={null}
|
||||
positionSource={{ kind: "fallback", label: "Letzte GPS-Position" }}
|
||||
currentError="Tidendaten nicht erreichbar."
|
||||
routeWeatherReport={partialReport}
|
||||
routeTides={{ start: null, middle: null, destination: null }}
|
||||
routeError="Streckentiden nur teilweise erreichbar."
|
||||
now={NOW}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<ConditionsPanel
|
||||
forecast={null}
|
||||
tide={null}
|
||||
positionSource={{ kind: "unknown" }}
|
||||
now={NOW}
|
||||
/>
|
||||
);
|
||||
|
||||
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<MarineForecast>,
|
||||
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> = {}): 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"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user