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();
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();
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();
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(
);
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: [],
dataSources: ["Test"],
routingMode: "fairway"
};