import { beforeEach, describe, expect, it } from "vitest"; import type { RouteResult } from "@watermaps/shared"; import { OFFLINE_VOYAGES_STORAGE_KEY, createOfflineVoyageRecord, deleteOfflineVoyage, listOfflineVoyages, loadOfflineVoyage, saveOfflineVoyage } from "../src/lib/offline-route"; beforeEach(() => localStorage.clear()); describe("offline voyage storage", () => { it("stores and restores a validated route and voyage plan", () => { const saved = saveOfflineVoyage( { route: routeFixture, name: "Törn Emden – Hamm", plan: { vesselProfile: { draughtM: 1.4, safetyReserveM: 0.5, airDraftM: 2.8, beamM: 3.2, cruiseSpeedKn: 6 }, waypoints: [{ lat: 51.65, lon: 7.34 }], departureAt: "2026-07-20T06:00:00.000Z", notes: "Schleusen vor Abfahrt prüfen" } }, localStorage, { id: "voyage-test", savedAt: "2026-07-19T12:00:00.000Z" } ); expect(saved.plan.start).toEqual({ lat: 53.344167, lon: 7.186111 }); expect(saved.plan.destination).toEqual({ lat: 51.6814536, lon: 7.8042615 }); expect(saved.route.departureTime).toBe("2026-07-20T06:00:00.000Z"); expect(saved.route.durationMinutes).toBe(120); expect(saved.route.routeSnaps).toEqual(routeFixture.routeSnaps); expect(saved.route.alternatives).toBeUndefined(); expect(listOfflineVoyages(localStorage)).toHaveLength(1); const restored = loadOfflineVoyage("voyage-test", localStorage); expect(restored).toEqual(saved); expect(restored?.route).toEqual( expect.objectContaining({ departureTime: "2026-07-20T06:00:00.000Z", durationMinutes: 120 }) ); expect(deleteOfflineVoyage("voyage-test", localStorage)).toBe(true); expect(loadOfflineVoyage("voyage-test", localStorage)).toBeNull(); }); it("ignores corrupted or untrusted persisted values", () => { localStorage.setItem(OFFLINE_VOYAGES_STORAGE_KEY, "not-json"); expect(listOfflineVoyages(localStorage)).toEqual([]); localStorage.setItem(OFFLINE_VOYAGES_STORAGE_KEY, JSON.stringify([{ schemaVersion: 1, id: "bad", name: "Bad", savedAt: "no date", plan: {}, route: { geometry: { type: "LineString", coordinates: [[999, 0], [0, 0]] } } }])); expect(listOfflineVoyages(localStorage)).toEqual([]); }); it("removes retired warnings and depth fields from legacy offline routes", () => { const record = createOfflineVoyageRecord( { route: routeFixture }, { id: "legacy-voyage", savedAt: "2026-07-19T12:00:00.000Z" } ); const legacyRecord = { ...record, route: { ...record.route, minKnownDepthM: null, unknownDepthRatio: 1, warnings: [ { code: "FAIRWAY_ROUTE", severity: "info", message: "Alter Graph-Hinweis" }, { code: "FAIRWAY_DATA_NOT_OFFICIAL", severity: "caution", message: "Alter Daten-Hinweis" }, { code: "DEPTH_UNKNOWN", severity: "caution", message: "Alter Tiefenpunkt-Hinweis" }, { code: "DEPTH_PARTIAL", severity: "caution", message: "Alter Teil-Tiefenhinweis" }, { code: "NO_KNOWN_DEPTH", severity: "caution", message: "Alter Unbekannt-Hinweis" }, { code: "DEPTH_TOO_SHALLOW", severity: "critical", message: "Alter Flachwasser-Hinweis" }, { code: "ROUTE_SEARCH_LIMITED", severity: "caution", message: "Suche begrenzt" } ] } }; localStorage.setItem(OFFLINE_VOYAGES_STORAGE_KEY, JSON.stringify([legacyRecord])); const restored = loadOfflineVoyage("legacy-voyage", localStorage); expect(restored?.route.warnings).toEqual([ { code: "ROUTE_SEARCH_LIMITED", severity: "caution", message: "Suche begrenzt" } ]); expect(restored?.route).not.toHaveProperty("minKnownDepthM"); expect(restored?.route).not.toHaveProperty("unknownDepthRatio"); }); it("rejects unsafe route coordinates before writing", () => { expect(() => createOfflineVoyageRecord({ route: { ...routeFixture, geometry: { type: "LineString", coordinates: [[7, 53], [Number.NaN, 52]] } } })).toThrow(/ungültige Koordinaten|Zahlenwert/); expect(localStorage.getItem(OFFLINE_VOYAGES_STORAGE_KEY)).toBeNull(); }); }); const routeFixture: RouteResult = { id: "emden-hamm", name: "Emden – Hamm", geometry: { type: "LineString", coordinates: [[7.186, 53.344], [7.1, 52.4], [7.8042, 51.6817]] }, distanceNm: 153.69, departureTime: "2026-07-20T06:00:00.000Z", durationMinutes: 120, eta: "2026-07-20T08:00:00.000Z", warnings: [{ code: "NOT_OFFICIAL", severity: "caution", message: "Nicht amtlich" }], dataSources: ["OSM"], routingMode: "fairway", routeSnaps: { start: { requested: { lat: 53.344167, lon: 7.186111 }, snapped: { lat: 53.344, lon: 7.186 }, distanceM: 20 }, waypoints: [], destination: { requested: { lat: 51.6814536, lon: 7.8042615 }, snapped: { lat: 51.6817, lon: 7.8042 }, distanceM: 28 } }, alternatives: [{ id: "unused", name: "Alternative", geometry: { type: "LineString", coordinates: [[7, 53], [7.1, 52]] }, distanceNm: 160, eta: null, warnings: [], dataSources: ["OSM"] }] };