Files
watermaps/apps/web/tests/route-weather-report.test.ts
T
BuTzZ 593dbd5f85
Test and publish container images / test (push) Successful in 2m26s
Test and publish container images / publish (push) Failing after 3s
optimized events
2026-07-28 14:36:47 +02:00

244 lines
7.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { MarineForecast, RouteResult } from "@watermaps/shared";
import {
createRouteBridgeReport,
createRouteWeatherReport,
summarizeRouteWeather
} from "../src/routeWeatherReport";
describe("route weather report", () => {
it("samples start, middle and destination forecasts", async () => {
const requested: Array<{ lat: number; lon: number }> = [];
const report = await createRouteWeatherReport(routeFixture, async (coordinate) => {
requested.push(coordinate);
return forecast({
windSpeed: coordinate.lon > 7.1 ? 18 : 10,
waveHeightM: coordinate.lon > 7.1 ? 1.2 : 0.4
});
});
expect(requested).toHaveLength(3);
expect(report.maxWindSpeedKn).toBe(18);
expect(report.maxWaveHeightM).toBe(1.2);
expect(report.severity).toBe("caution");
expect(report.summary).toContain("Aufmerksam fahren");
expect(report.bridgeReport).toBeNull();
});
it("projects forecasts along the route timeline and adjusts ETA for along-route current", async () => {
const requestedTimes: Array<string | undefined> = [];
const departureTime = "2026-07-20T06:00:00.000Z";
const timedRoute: RouteResult = {
...routeFixture,
geometry: {
type: "LineString",
coordinates: [[7, 52], [7.2, 52], [7.4, 52]]
},
distanceNm: 20,
departureTime,
durationMinutes: 240,
eta: "2026-07-20T10:00:00.000Z"
};
const report = await createRouteWeatherReport(
timedRoute,
{ draughtM: 1.4, safetyReserveM: 0.5, cruiseSpeedKn: 5 },
async (_coordinate, at) => {
requestedTimes.push(at);
return forecast({
oceanCurrentSpeedKn: 1,
oceanCurrentDirectionDeg: 90,
forecastTime: at
});
}
);
expect(requestedTimes).toEqual([
"2026-07-20T06:00:00.000Z",
"2026-07-20T08:00:00.000Z",
"2026-07-20T10:00:00.000Z"
]);
expect(report.samples.map((sample) => sample.plannedTime)).toEqual(requestedTimes);
expect(report.samples.map((sample) => sample.currentAlongRouteKn)).toEqual([1, 1, 1]);
expect(report.departureTime).toBe(departureTime);
expect(report.averageAlongRouteCurrentKn).toBe(1);
expect(report.currentAdjustmentMinutes).toBe(-40);
expect(report.adjustedEta).toBe("2026-07-20T09:20:00.000Z");
});
it("adds a bridge report and marks a route as blocked by a low bridge", async () => {
const report = await createRouteWeatherReport(
routeFixture,
{ draughtM: 1.4, safetyReserveM: 0.5, airDraftM: 3 },
async () => forecast({}),
async (params) => {
expect(params.layers).toEqual(["bridges"]);
return {
type: "FeatureCollection",
features: [
{
type: "Feature",
id: "bridge-low",
properties: {
layer: "bridges",
name: "Niedrige Brücke",
clearance_m: 2.7,
clearance_label: "H 2.7 m",
label: "Niedrige Brücke H 2.7 m",
source: "OSM/Geofabrik",
phone: "+49 491 234",
website: "https://bridge.example"
},
geometry: {
type: "LineString",
coordinates: [
[6.99, 53.5],
[7.21, 53.5]
]
}
},
{
type: "Feature",
id: "bridge-high",
properties: {
layer: "bridges",
name: "Hohe Brücke",
clearance_m: 4.2,
clearance_label: "H 4.2 m",
label: "Hohe Brücke H 4.2 m",
source: "OSM/Geofabrik"
},
geometry: {
type: "LineString",
coordinates: [
[7.18, 53.68],
[7.22, 53.68]
]
}
}
]
};
}
);
expect(report.bridgeReport?.severity).toBe("critical");
expect(report.bridgeReport?.tooLowCount).toBe(1);
expect(report.bridgeReport?.checkedCount).toBe(2);
expect(report.bridgeReport?.summary).toContain("Nicht passierbar");
expect(report.bridgeReport?.bridges[0]?.name).toBe("Niedrige Brücke");
expect(report.bridgeReport?.bridges[0]?.phone).toBe("+49 491 234");
expect(report.bridgeReport?.bridges[0]?.website).toBe("https://bridge.example");
});
it("deduplicates only conservatively matching bridge ways and keeps the richest safe assessment", async () => {
const route: RouteResult = {
...routeFixture,
geometry: {
type: "LineString",
coordinates: [[0, 0], [1, 0]]
},
distanceNm: 60
};
const point = (
id: string,
lon: number,
lat: number,
properties: Record<string, unknown> = {}
) => ({
type: "Feature" as const,
id,
properties: { layer: "bridges", ...properties },
geometry: { type: "Point" as const, coordinates: [lon, lat] }
});
const report = await createRouteBridgeReport(
route,
{ airDraftM: 3 },
async () => ({
type: "FeatureCollection",
features: [
point("named-1", 0.2, 0, {
name: "Am Tonnenhof",
clearance_m: 4,
phone: "+49 491 111"
}),
point("named-2", 0.2, 0.0003, {
name: "Am Tonnenhof",
clearance_m: 3.5,
website: "https://tonnenhof.example"
}),
point("distinct-east", 0.4, 0, { name: "Klappbrücke Ost" }),
point("distinct-west", 0.4, 0.0001, { name: "Klappbrücke West" }),
point("unnamed-tight-1", 0.6, 0),
point("unnamed-tight-2", 0.6, 0.00005),
point("unnamed-separate-1", 0.8, 0),
point("unnamed-separate-2", 0.8, 0.0001),
point("named-with-way", 0.9, 0, { name: "Auricher Straße" }),
point("unnamed-with-name", 0.9, 0.0001)
]
})
);
expect(report.bridges).toHaveLength(7);
const tonnenhof = report.bridges.find((bridge) => bridge.name === "Am Tonnenhof");
expect(tonnenhof).toMatchObject({
clearanceM: 3.5,
clearanceLabel: "H 3.5 m",
label: "Am Tonnenhof H 3.5 m",
marginM: 0.5,
phone: "+49 491 111",
website: "https://tonnenhof.example"
});
expect(
report.bridges.filter((bridge) => bridge.name?.startsWith("Klappbrücke"))
).toHaveLength(2);
expect(
report.bridges.filter((bridge) => bridge.name === null)
).toHaveLength(3);
});
it("marks critical weather when wind or wave thresholds are exceeded", () => {
const report = summarizeRouteWeather([
{
label: "Mitte",
coordinate: { lat: 53.5, lon: 7.1 },
forecast: forecast({ windSpeed: 28, waveHeightM: 1.1 })
}
]);
expect(report.severity).toBe("critical");
expect(report.summary).toContain("Kritische Bedingungen");
});
});
const routeFixture: RouteResult = {
geometry: {
type: "LineString",
coordinates: [
[7, 53.3],
[7.1, 53.5],
[7.2, 53.7]
]
},
distanceNm: 25,
eta: null,
warnings: [],
dataSources: [],
routingMode: "fairway"
};
function forecast(overrides: Partial<MarineForecast>): MarineForecast {
return {
waveHeightM: 0.4,
waveDirectionDeg: 280,
wavePeriodS: 4,
windSpeed: 10,
windDirectionDeg: 260,
weatherCode: 2,
temperatureC: 18,
source: "Test",
updatedAt: "2026-07-13T12:00:00.000Z",
...overrides
};
}