trails/apps/planner/app/lib/waypoint-ymap.test.ts
Ullrich Schäfer 3b9672e0ff
planner: give the Yjs document a typed schema seam
The routeData Y.Map's ~15 string keys (geojson, coordinates,
segmentBoundaries, road metadata, profile, colorMode, baseLayer,
overlays, poiCategories) were read and written raw at ~30 call sites,
each with its own JSON parsing and casts; parseJsonArray existed twice
and waypoint extraction four times. GPX assembly was duplicated between
SaveToJournalButton and ExportButton, so the saved plan and the
exported file could silently diverge.

- new lib/route-data.ts owns the routeData (+ noGoAreas) schema:
  typed read/write, JSON encoding internal, ColorMode moves here
  (re-exported from ColoredRoute for existing importers)
- new lib/gpx-export.ts owns GPX assembly: buildRouteGpx /
  buildPlanGpx / buildDayGpxFiles / hasDayBreaks; multi-day splitting
  becomes a pure, tested function
- waypoint-ymap.ts gains extractWaypoints / extractWaypointData; the
  four hand-rolled copies (use-routing, use-waypoint-manager,
  WaypointSidebar, use-days) now share it, and WaypointSidebar's
  moveWaypoint reuses the round-trip helpers instead of re-listing
  every waypoint field
- all hooks/components consume the seam; no raw routeData key strings
  remain outside route-data.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 01:35:04 +02:00

77 lines
2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import * as Y from "yjs";
import type { Waypoint } from "@trails-cool/types";
import {
extractWaypointData,
extractWaypoints,
waypointFromYMap,
waypointToYMap,
} from "./waypoint-ymap.ts";
function createArray(): Y.Array<Y.Map<unknown>> {
return new Y.Doc().getArray<Y.Map<unknown>>("waypoints");
}
/** Y.Maps only expose values once integrated into a document. */
function roundTrip(wp: Waypoint): Waypoint {
const arr = createArray();
arr.push([waypointToYMap(wp)]);
return waypointFromYMap(arr.get(0)!);
}
describe("waypoint round-trip", () => {
it("preserves all fields through toYMap → fromYMap", () => {
const wp: Waypoint = {
lat: 52.5,
lon: 13.4,
name: "Hut",
note: "Book ahead",
isDayBreak: true,
osmId: 12345,
poiTags: { tourism: "alpine_hut" },
};
expect(roundTrip(wp)).toEqual(wp);
});
it("leaves optional fields undefined", () => {
const wp = roundTrip({ lat: 1, lon: 2 });
expect(wp).toEqual({
lat: 1,
lon: 2,
name: undefined,
note: undefined,
isDayBreak: undefined,
osmId: undefined,
poiTags: undefined,
});
});
});
describe("extractWaypoints", () => {
it("reads the whole list in order", () => {
const arr = createArray();
arr.push([
waypointToYMap({ lat: 1, lon: 2, name: "A" }),
waypointToYMap({ lat: 3, lon: 4, isDayBreak: true }),
]);
const wps = extractWaypoints(arr);
expect(wps).toHaveLength(2);
expect(wps[0]!.name).toBe("A");
expect(wps[1]!.isDayBreak).toBe(true);
});
});
describe("extractWaypointData", () => {
it("flattens isDayBreak to a plain overnight boolean", () => {
const arr = createArray();
arr.push([
waypointToYMap({ lat: 1, lon: 2 }),
waypointToYMap({ lat: 3, lon: 4, isDayBreak: true, note: "n" }),
]);
const data = extractWaypointData(arr);
expect(data[0]!.overnight).toBe(false);
expect(data[1]!).toEqual({ lat: 3, lon: 4, name: undefined, note: "n", overnight: true });
});
});