200 lines
6.3 KiB
TypeScript
200 lines
6.3 KiB
TypeScript
import { cleanup, renderHook, waitFor } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { RouteResult } from "@watermaps/shared";
|
|
import { useRouteEventFeatures } from "../src/hooks/useRouteEventFeatures";
|
|
|
|
const apiMocks = vi.hoisted(() => ({
|
|
getMapFeatures: vi.fn()
|
|
}));
|
|
|
|
vi.mock("../src/api", () => ({
|
|
getMapFeatures: apiMocks.getMapFeatures
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
apiMocks.getMapFeatures.mockReset();
|
|
});
|
|
|
|
afterEach(cleanup);
|
|
|
|
describe("useRouteEventFeatures", () => {
|
|
it("loads harbours, locks and bridges once and independently", async () => {
|
|
apiMocks.getMapFeatures.mockImplementation(
|
|
async ({ layers }: { layers: string[] }) =>
|
|
featureCollection(layers[0]!)
|
|
);
|
|
|
|
const activeRoute = route("first", 0);
|
|
const { result } = renderHook(() =>
|
|
useRouteEventFeatures(activeRoute, { airDraftM: 3 })
|
|
);
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(apiMocks.getMapFeatures).toHaveBeenCalledTimes(3);
|
|
expect(
|
|
apiMocks.getMapFeatures.mock.calls.map(([request]) => request.layers)
|
|
).toEqual(expect.arrayContaining([["harbours"], ["locks"], ["bridges"]]));
|
|
expect(result.current.harbours.map((harbour) => harbour.name)).toEqual([
|
|
"Testhafen"
|
|
]);
|
|
expect(result.current.locks.map((lock) => lock.name)).toEqual([
|
|
"Testschleuse"
|
|
]);
|
|
expect(result.current.bridgeReport?.bridges.map((bridge) => bridge.name)).toEqual([
|
|
"Testbrücke"
|
|
]);
|
|
expect(result.current.error).toBeNull();
|
|
});
|
|
|
|
it("keeps working categories when one feature request fails", async () => {
|
|
apiMocks.getMapFeatures.mockImplementation(
|
|
async ({ layers }: { layers: string[] }) => {
|
|
if (layers[0] === "harbours") {
|
|
throw new Error("Feature-Datenbank fehlt");
|
|
}
|
|
return featureCollection(layers[0]!);
|
|
}
|
|
);
|
|
|
|
const activeRoute = route("partial", 0);
|
|
const { result } = renderHook(() =>
|
|
useRouteEventFeatures(activeRoute, { airDraftM: 3 })
|
|
);
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.harbours).toEqual([]);
|
|
expect(result.current.locks).toHaveLength(1);
|
|
expect(result.current.bridgeReport?.bridges).toHaveLength(1);
|
|
expect(result.current.errors.harbours).toContain(
|
|
"Häfen nicht erreichbar: Feature-Datenbank fehlt"
|
|
);
|
|
expect(result.current.errors.locks).toBeNull();
|
|
expect(result.current.errors.bridges).toBeNull();
|
|
});
|
|
|
|
it("reassesses only bridges when the boat height changes", async () => {
|
|
apiMocks.getMapFeatures.mockImplementation(
|
|
async ({ layers }: { layers: string[] }) =>
|
|
featureCollection(layers[0]!)
|
|
);
|
|
const activeRoute = route("height", 0);
|
|
const { result, rerender } = renderHook(
|
|
({ airDraftM }) =>
|
|
useRouteEventFeatures(activeRoute, { airDraftM }),
|
|
{ initialProps: { airDraftM: 3 } }
|
|
);
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
apiMocks.getMapFeatures.mockClear();
|
|
|
|
rerender({ airDraftM: 3.5 });
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
expect(apiMocks.getMapFeatures).toHaveBeenCalledTimes(1);
|
|
expect(apiMocks.getMapFeatures).toHaveBeenCalledWith(
|
|
expect.objectContaining({ layers: ["bridges"] })
|
|
);
|
|
expect(result.current.harbours).toHaveLength(1);
|
|
expect(result.current.locks).toHaveLength(1);
|
|
expect(result.current.bridgeReport?.requiredAirDraftM).toBe(3.5);
|
|
});
|
|
|
|
it("aborts stale category requests on a route change and exposes only the new route", async () => {
|
|
const staleSignals: AbortSignal[] = [];
|
|
const currentSignals: AbortSignal[] = [];
|
|
let staleCalls = 0;
|
|
apiMocks.getMapFeatures.mockImplementation(
|
|
({ layers, signal }: { layers: string[]; signal: AbortSignal }) => {
|
|
if (staleCalls < 3) {
|
|
staleCalls += 1;
|
|
staleSignals.push(signal);
|
|
return new Promise(() => undefined);
|
|
}
|
|
currentSignals.push(signal);
|
|
return Promise.resolve(featureCollection(layers[0]!, 0.5));
|
|
}
|
|
);
|
|
const firstRoute = route("first", 0);
|
|
const secondRoute = route("second", 0.5);
|
|
const { result, rerender, unmount } = renderHook(
|
|
({ activeRoute }) =>
|
|
useRouteEventFeatures(activeRoute, { airDraftM: 3 }),
|
|
{ initialProps: { activeRoute: firstRoute } }
|
|
);
|
|
|
|
await waitFor(() => expect(apiMocks.getMapFeatures).toHaveBeenCalledTimes(3));
|
|
rerender({ activeRoute: secondRoute });
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
expect(apiMocks.getMapFeatures).toHaveBeenCalledTimes(6);
|
|
expect(staleSignals).toHaveLength(3);
|
|
expect(staleSignals.every((signal) => signal.aborted)).toBe(true);
|
|
expect(result.current.harbours[0]?.coordinate.lon).toBeCloseTo(0.7);
|
|
expect(result.current.locks[0]?.coordinate.lon).toBeCloseTo(0.8);
|
|
expect(result.current.bridgeReport?.bridges[0]?.coordinate.lon).toBeCloseTo(0.9);
|
|
|
|
unmount();
|
|
expect(currentSignals).toHaveLength(3);
|
|
expect(currentSignals.every((signal) => signal.aborted)).toBe(true);
|
|
});
|
|
});
|
|
|
|
function featureCollection(layer: string, offset = 0) {
|
|
const fixtures = {
|
|
harbours: {
|
|
type: "Feature" as const,
|
|
id: `harbour-${offset}`,
|
|
properties: { layer: "harbours", name: "Testhafen" },
|
|
geometry: {
|
|
type: "Point" as const,
|
|
coordinates: [0.2 + offset, 0]
|
|
}
|
|
},
|
|
locks: {
|
|
type: "Feature" as const,
|
|
id: `lock-${offset}`,
|
|
properties: { layer: "locks", name: "Testschleuse" },
|
|
geometry: {
|
|
type: "Point" as const,
|
|
coordinates: [0.3 + offset, 0]
|
|
}
|
|
},
|
|
bridges: {
|
|
type: "Feature" as const,
|
|
id: `bridge-${offset}`,
|
|
properties: {
|
|
layer: "bridges",
|
|
name: "Testbrücke",
|
|
clearance_m: 4
|
|
},
|
|
geometry: {
|
|
type: "Point" as const,
|
|
coordinates: [0.4 + offset, 0]
|
|
}
|
|
}
|
|
};
|
|
return {
|
|
type: "FeatureCollection" as const,
|
|
features: [fixtures[layer as keyof typeof fixtures]]
|
|
};
|
|
}
|
|
|
|
function route(id: string, offset: number): RouteResult {
|
|
return {
|
|
id,
|
|
geometry: {
|
|
type: "LineString",
|
|
coordinates: [
|
|
[offset, 0],
|
|
[offset + 1, 0]
|
|
]
|
|
},
|
|
distanceNm: 60,
|
|
eta: null,
|
|
warnings: [],
|
|
dataSources: ["test"],
|
|
routingMode: "fairway"
|
|
};
|
|
}
|