107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import { act, cleanup, renderHook } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { RouteResult } from "@watermaps/shared";
|
|
import { useCourseAssistant, type CourseAssistantInput } from "../src/hooks/useCourseAssistant";
|
|
|
|
const originalVibrate = Object.getOwnPropertyDescriptor(navigator, "vibrate");
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.useRealTimers();
|
|
if (originalVibrate) Object.defineProperty(navigator, "vibrate", originalVibrate);
|
|
else Reflect.deleteProperty(navigator, "vibrate");
|
|
});
|
|
|
|
describe("useCourseAssistant", () => {
|
|
it("never starts automatically and calculates only after an explicit start", () => {
|
|
const { result } = renderHook(() => useCourseAssistant(input()));
|
|
|
|
expect(result.current.active).toBe(false);
|
|
expect(result.current.guidance).toBeNull();
|
|
|
|
act(() => result.current.start());
|
|
|
|
expect(result.current.active).toBe(true);
|
|
expect(result.current.guidance?.status).toBe("on-route");
|
|
expect(result.current.guidance?.desiredCourseDeg).toBeCloseTo(90, 0);
|
|
});
|
|
|
|
it("stops deliberately when a different route is selected", () => {
|
|
const first = route("first", [[0, 0], [0.02, 0]]);
|
|
const second = route("second", [[0, 0], [0, 0.02]]);
|
|
const { result, rerender } = renderHook(
|
|
({ currentRoute }) => useCourseAssistant(input(currentRoute)),
|
|
{ initialProps: { currentRoute: first } }
|
|
);
|
|
act(() => result.current.start());
|
|
expect(result.current.active).toBe(true);
|
|
|
|
rerender({ currentRoute: second });
|
|
|
|
expect(result.current.active).toBe(false);
|
|
expect(result.current.guidance).toBeNull();
|
|
});
|
|
|
|
it("pauses guidance when no fresh GPS fix arrives", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-07-20T10:00:00.000Z"));
|
|
const fixTimestampMs = Date.now();
|
|
const { result } = renderHook(() => useCourseAssistant(input(routeFixture, fixTimestampMs)));
|
|
act(() => result.current.start());
|
|
expect(result.current.guidance).not.toBeNull();
|
|
|
|
act(() => vi.advanceTimersByTime(20_000));
|
|
|
|
expect(result.current.fixStale).toBe(true);
|
|
expect(result.current.guidance).toBeNull();
|
|
});
|
|
|
|
it("vibrates once when crossing into the off-route state", () => {
|
|
const vibrate = vi.fn();
|
|
Object.defineProperty(navigator, "vibrate", { configurable: true, value: vibrate });
|
|
const { result, rerender } = renderHook(
|
|
({ lat }) => useCourseAssistant(input(routeFixture, Date.now(), lat)),
|
|
{ initialProps: { lat: 0 } }
|
|
);
|
|
act(() => result.current.start());
|
|
expect(vibrate).not.toHaveBeenCalled();
|
|
|
|
rerender({ lat: 0.003 });
|
|
expect(result.current.guidance?.status).toBe("off-route");
|
|
expect(vibrate).toHaveBeenCalledTimes(1);
|
|
|
|
rerender({ lat: 0.0031 });
|
|
expect(vibrate).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
function input(
|
|
currentRoute: RouteResult = routeFixture,
|
|
fixTimestampMs = Date.now(),
|
|
lat = 0
|
|
): CourseAssistantInput {
|
|
return {
|
|
route: currentRoute,
|
|
position: { lon: 0.001, lat },
|
|
accuracyM: 5,
|
|
speedKn: 6,
|
|
headingDeg: 90,
|
|
fixTimestampMs
|
|
};
|
|
}
|
|
|
|
function route(id: string, coordinates: [number, number][]): RouteResult {
|
|
return {
|
|
id,
|
|
name: id,
|
|
geometry: { type: "LineString", coordinates },
|
|
distanceNm: 1,
|
|
eta: null,
|
|
warnings: [],
|
|
dataSources: ["Test"],
|
|
routingMode: "fairway"
|
|
};
|
|
}
|
|
|
|
const routeFixture = route("test-route", [[0, 0], [0.02, 0]]);
|