Files
watermaps/apps/web/tests/route-events.test.ts
T
2026-07-24 11:29:24 +02:00

203 lines
5.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type {
RouteResult,
VoyageHarbour
} from "@watermaps/shared";
import {
nextRouteEventsByKind,
upcomingRouteEvents
} from "../src/routeEvents";
import type { RouteBridgeAssessment } from "../src/routeWeatherReport";
import type { RouteLock } from "../src/voyageHarbours";
const route: RouteResult = {
id: "eastbound",
geometry: {
type: "LineString",
coordinates: [
[0, 0],
[1, 0]
]
},
distanceNm: 60,
eta: null,
warnings: [],
minKnownDepthM: null,
unknownDepthRatio: 1,
dataSources: ["test"]
};
describe("upcomingRouteEvents", () => {
it("projects mixed facilities, filters passed and off-corridor entries, and orders them along the route", () => {
const events = upcomingRouteEvents({
route,
progressNm: 12,
harbours: [
harbour("passed-harbour", 0.1, 0),
harbour("next-harbour", 0.6, 0),
harbour("off-route-harbour", 0.5, 0.05)
],
locks: [
lock("next-lock", 0.4, 0, 999, 0),
lock("off-route-lock", 0.5, 0.01, 1, 0)
],
bridges: [
bridge("next-bridge", 0.3, 0.0005, 99),
bridge("off-route-bridge", 0.35, 0.01, 0.001)
]
});
expect(events.map((event) => `${event.kind}:${event.id}`)).toEqual([
"bridge:next-bridge",
"lock:next-lock",
"harbour:next-harbour"
]);
expect(events.every((event) => event.routeDistanceNm >= 12)).toBe(true);
expect(events.every((event) => event.remainingNm >= 0)).toBe(true);
});
it("reprojects locks and bridges instead of trusting their existing distance fields", () => {
const [projectedLock, projectedBridge] = upcomingRouteEvents({
route,
locks: [lock("lock", 0.25, 0, 999, 777)],
bridges: [bridge("bridge", 0.75, 0, 999)]
});
expect(projectedLock?.kind).toBe("lock");
expect(projectedLock?.routeDistanceNm).toBeCloseTo(15, 0);
expect(projectedLock?.distanceFromRouteNm).toBeCloseTo(0, 6);
expect(projectedBridge?.kind).toBe("bridge");
expect(projectedBridge?.routeDistanceNm).toBeCloseTo(45, 0);
expect(projectedBridge?.distanceFromRouteNm).toBeCloseTo(0, 6);
});
it("uses a corridor override without changing the defaults for other kinds", () => {
const events = upcomingRouteEvents({
route,
corridorsNm: { harbour: 4 },
harbours: [harbour("detour", 0.5, 0.05)],
locks: [lock("off-route-lock", 0.5, 0.01, 0, 0)]
});
expect(events.map((event) => event.id)).toEqual(["detour"]);
});
it("calculates ETA and retains both speed and reference-time provenance", () => {
const [event] = upcomingRouteEvents({
route,
progressNm: 6,
locks: [lock("lock", 0.5, 0, 0, 0)],
etaBasis: {
speedKn: 6,
speedSource: "vessel-cruise-speed",
referenceTime: "2026-07-23T08:00:00.000Z",
referenceSource: "route-departure"
}
});
expect(event).toBeDefined();
expect(event!.remainingNm).toBeCloseTo(24, 0);
expect(event!.eta).toMatchObject({
speedKn: 6,
speedSource: "vessel-cruise-speed",
referenceTime: "2026-07-23T08:00:00.000Z",
referenceSource: "route-departure"
});
expect(event!.eta!.minutesFromProgress).toBeCloseTo(
(event!.remainingNm / 6) * 60,
8
);
expect(Date.parse(event!.eta!.estimatedAt)).toBeCloseTo(
Date.parse("2026-07-23T08:00:00.000Z") +
event!.eta!.minutesFromProgress * 60_000,
0
);
});
it("omits ETA for an invalid assumption rather than inventing a speed", () => {
const [event] = upcomingRouteEvents({
route,
harbours: [harbour("harbour", 0.5, 0)],
etaBasis: {
speedKn: 0,
speedSource: "gps-sog",
referenceTime: "not-a-time",
referenceSource: "current-time"
}
});
expect(event?.eta).toBeNull();
});
it("selects the first upcoming event of each kind from an ordered list", () => {
const events = upcomingRouteEvents({
route,
harbours: [
harbour("harbour-2", 0.8, 0),
harbour("harbour-1", 0.2, 0)
],
locks: [lock("lock-1", 0.3, 0, 0, 0)]
});
const next = nextRouteEventsByKind(events);
expect(next.harbour?.id).toBe("harbour-1");
expect(next.lock?.id).toBe("lock-1");
expect(next.bridge).toBeNull();
});
});
function harbour(
id: string,
lon: number,
lat: number
): VoyageHarbour {
return {
id,
name: id,
kind: "harbour",
coordinate: { lon, lat }
};
}
function lock(
id: string,
lon: number,
lat: number,
routeDistanceNm: number,
distanceFromRouteNm: number
): RouteLock {
return {
id,
name: id,
coordinate: { lon, lat },
routeDistanceNm,
distanceFromRouteNm,
openingHours: null,
phone: null,
vhf: null,
website: null
};
}
function bridge(
id: string,
lon: number,
lat: number,
distanceNm: number
): RouteBridgeAssessment {
return {
id,
name: id,
label: id,
coordinate: { lon, lat },
distanceNm,
clearanceM: null,
clearanceLabel: null,
requiredAirDraftM: null,
marginM: null,
status: "unknown",
source: "test"
};
}