230 lines
8.0 KiB
TypeScript
230 lines
8.0 KiB
TypeScript
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"
|
|
};
|
|
}
|