134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
import "@testing-library/jest-dom/vitest";
|
|
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { RouteResult } from "@watermaps/shared";
|
|
import { VoyageNavigationTools } from "../src/components/VoyageNavigationTools";
|
|
|
|
const originalGeolocation = Object.getOwnPropertyDescriptor(navigator, "geolocation");
|
|
const originalVibrate = Object.getOwnPropertyDescriptor(navigator, "vibrate");
|
|
|
|
beforeEach(() => localStorage.clear());
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
localStorage.clear();
|
|
restoreNavigatorProperty("geolocation", originalGeolocation);
|
|
restoreNavigatorProperty("vibrate", originalVibrate);
|
|
});
|
|
|
|
describe("VoyageNavigationTools", () => {
|
|
it("does not request geolocation before the skipper starts the alarm", () => {
|
|
const watchPosition = vi.fn(() => 17);
|
|
installGeolocation(watchPosition, vi.fn());
|
|
|
|
render(<VoyageNavigationTools route={routeFixture} />);
|
|
|
|
expect(watchPosition).not.toHaveBeenCalled();
|
|
expect(screen.getByText(/GPS wird erst nach/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("does not open a second GPS watch while the course assistant is active", () => {
|
|
const watchPosition = vi.fn(() => 17);
|
|
installGeolocation(watchPosition, vi.fn());
|
|
|
|
render(<VoyageNavigationTools route={routeFixture} courseAssistantActive />);
|
|
|
|
expect(screen.getByRole("button", { name: "Kursalarm ist im Kursassistenten enthalten" })).toBeDisabled();
|
|
expect(screen.getByText(/Querabweichung.*Kursassistenten/)).toBeVisible();
|
|
expect(watchPosition).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("starts explicitly and warns when the vessel leaves the route", () => {
|
|
let success: PositionCallback | undefined;
|
|
const watchPosition = vi.fn((next: PositionCallback) => {
|
|
success = next;
|
|
return 42;
|
|
});
|
|
const clearWatch = vi.fn();
|
|
const vibrate = vi.fn();
|
|
installGeolocation(watchPosition, clearWatch);
|
|
Object.defineProperty(navigator, "vibrate", { configurable: true, value: vibrate });
|
|
|
|
render(<VoyageNavigationTools route={routeFixture} />);
|
|
fireEvent.click(screen.getByRole("button", { name: "Abweichungsalarm starten" }));
|
|
|
|
expect(watchPosition).toHaveBeenCalledTimes(1);
|
|
expect(screen.getByRole("button", { name: "Abweichungsalarm stoppen" })).toBeInTheDocument();
|
|
|
|
act(() => success?.(positionAt(53.01, 7.005, 5)));
|
|
|
|
expect(screen.getByRole("alert")).toHaveTextContent(/von der Route entfernt/);
|
|
expect(vibrate).toHaveBeenCalledWith([200, 100, 200]);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Abweichungsalarm stoppen" }));
|
|
expect(clearWatch).toHaveBeenCalledWith(42);
|
|
});
|
|
|
|
it("saves and restores the route and plan on this device", () => {
|
|
const onLoad = vi.fn();
|
|
render(
|
|
<VoyageNavigationTools
|
|
route={routeFixture}
|
|
plan={{
|
|
vesselProfile: { draughtM: 1.4, safetyReserveM: 0.5, cruiseSpeedKn: 6 },
|
|
departureAt: "2026-07-20T06:00:00.000Z"
|
|
}}
|
|
onLoadOfflineVoyage={onLoad}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Route offline speichern" }));
|
|
expect(screen.getByText(/ist auf diesem Gerät offline verfügbar/)).toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("button", { name: "Offline-Route laden" }));
|
|
|
|
expect(onLoad).toHaveBeenCalledTimes(1);
|
|
expect(onLoad.mock.calls[0]?.[0].route.geometry).toEqual(routeFixture.geometry);
|
|
expect(onLoad.mock.calls[0]?.[0].plan.vesselProfile.cruiseSpeedKn).toBe(6);
|
|
});
|
|
});
|
|
|
|
function installGeolocation(watchPosition: typeof navigator.geolocation.watchPosition, clearWatch: typeof navigator.geolocation.clearWatch) {
|
|
Object.defineProperty(navigator, "geolocation", {
|
|
configurable: true,
|
|
value: { watchPosition, clearWatch, getCurrentPosition: vi.fn() }
|
|
});
|
|
}
|
|
|
|
function restoreNavigatorProperty(name: "geolocation" | "vibrate", descriptor: PropertyDescriptor | undefined) {
|
|
if (descriptor) {
|
|
Object.defineProperty(navigator, name, descriptor);
|
|
} else {
|
|
Reflect.deleteProperty(navigator, name);
|
|
}
|
|
}
|
|
|
|
function positionAt(lat: number, lon: number, accuracy: number): GeolocationPosition {
|
|
return {
|
|
coords: {
|
|
latitude: lat,
|
|
longitude: lon,
|
|
accuracy,
|
|
altitude: null,
|
|
altitudeAccuracy: null,
|
|
heading: null,
|
|
speed: null,
|
|
toJSON: () => ({})
|
|
},
|
|
timestamp: Date.now(),
|
|
toJSON: () => ({})
|
|
};
|
|
}
|
|
|
|
const routeFixture: RouteResult = {
|
|
id: "test-route",
|
|
name: "Testfahrt",
|
|
geometry: { type: "LineString", coordinates: [[7, 53], [7.01, 53]] },
|
|
distanceNm: 0.4,
|
|
eta: null,
|
|
warnings: [],
|
|
minKnownDepthM: null,
|
|
unknownDepthRatio: 1,
|
|
dataSources: ["Test"],
|
|
routingMode: "fairway"
|
|
};
|