import { describe, it, expect } from "vitest"; import { generateGpx } from "./generate.ts"; import { parseGpxAsync } from "./parse.ts"; describe("generateGpx", () => { it("generates valid GPX with name", () => { const gpx = generateGpx({ name: "Test Route" }); expect(gpx).toContain('Test Route"); expect(gpx).toContain(""); }); it("generates waypoints", () => { const gpx = generateGpx({ waypoints: [ { lat: 52.52, lon: 13.405, name: "Berlin" }, { lat: 48.137, lon: 11.576 }, ], }); expect(gpx).toContain('wpt lat="52.52" lon="13.405"'); expect(gpx).toContain("Berlin"); expect(gpx).toContain('wpt lat="48.137" lon="11.576"'); }); it("generates track points with elevation", () => { const gpx = generateGpx({ tracks: [[{ lat: 52.52, lon: 13.405, ele: 34 }, { lat: 48.137, lon: 11.576, ele: 519 }]], }); expect(gpx).toContain(""); expect(gpx).toContain(''); expect(gpx).toContain("34"); }); it("escapes XML special characters in names", () => { const gpx = generateGpx({ name: "Route & B" }); expect(gpx).toContain("Route <A> & B"); }); it("produces round-trippable GPX", async () => { const original = generateGpx({ name: "Round Trip", waypoints: [{ lat: 52.52, lon: 13.405, name: "Start" }], tracks: [[{ lat: 52.52, lon: 13.405, ele: 34 }, { lat: 48.137, lon: 11.576, ele: 519 }]], }); const parsed = await parseGpxAsync(original); expect(parsed.name).toBe("Round Trip"); expect(parsed.waypoints).toHaveLength(1); expect(parsed.waypoints[0]!.name).toBe("Start"); expect(parsed.tracks[0]).toHaveLength(2); }); it("emits overnight for isDayBreak waypoints", () => { const gpx = generateGpx({ waypoints: [ { lat: 52.52, lon: 13.405, name: "Berlin" }, { lat: 51.84, lon: 12.243, name: "Dessau", isDayBreak: true }, { lat: 50.98, lon: 11.028, name: "Erfurt" }, ], }); expect(gpx).toContain("overnight"); // Only Dessau should have the type element expect(gpx.match(/overnight<\/type>/g)).toHaveLength(1); }); it("round-trips isDayBreak through generate and parse", async () => { const gpx = generateGpx({ waypoints: [ { lat: 52.52, lon: 13.405, name: "Berlin" }, { lat: 51.84, lon: 12.243, name: "Dessau", isDayBreak: true }, { lat: 50.98, lon: 11.028, name: "Erfurt" }, ], tracks: [[ { lat: 52.52, lon: 13.405, ele: 34 }, { lat: 51.84, lon: 12.243, ele: 80 }, { lat: 50.98, lon: 11.028, ele: 195 }, ]], }); const parsed = await parseGpxAsync(gpx); expect(parsed.waypoints).toHaveLength(3); expect(parsed.waypoints[0]!.isDayBreak).toBeUndefined(); expect(parsed.waypoints[1]!.isDayBreak).toBe(true); expect(parsed.waypoints[2]!.isDayBreak).toBeUndefined(); }); it("splits tracks by day with splitByDay option", () => { const gpx = generateGpx({ waypoints: [ { lat: 0, lon: 0, name: "A" }, { lat: 1, lon: 0, name: "B", isDayBreak: true }, { lat: 2, lon: 0, name: "C" }, ], tracks: [[ { lat: 0, lon: 0, ele: 0 }, { lat: 0.5, lon: 0, ele: 50 }, { lat: 1, lon: 0, ele: 100 }, { lat: 1.5, lon: 0, ele: 150 }, { lat: 2, lon: 0, ele: 200 }, ]], splitByDay: true, }); // Should have two elements expect(gpx.match(//g)).toHaveLength(2); expect(gpx).toContain("Day 1: A - B"); expect(gpx).toContain("Day 2: B - C"); }); it("encodes POI metadata as trails:poi extensions", () => { const gpx = generateGpx({ waypoints: [ { lat: 52.52, lon: 13.405, name: "Bike Shop", osmId: 123456, poiTags: { phone: "+49 30 123", website: "https://example.com", opening_hours: "Mo-Fr 09:00-18:00" }, }, ], }); expect(gpx).toContain('xmlns:trails="https://trails.cool/gpx/1"'); expect(gpx).toContain(''); expect(gpx).toContain(''); expect(gpx).toContain(''); expect(gpx).toContain(''); }); it("roundtrips POI metadata through generate + parse", async () => { const gpx = generateGpx({ waypoints: [ { lat: 52.52, lon: 13.405, name: "Campsite", osmId: 987, poiTags: { phone: "+49 30 999", website: "https://camp.example" }, }, { lat: 48.0, lon: 11.0, name: "Plain waypoint" }, ], }); const parsed = await parseGpxAsync(gpx); expect(parsed.waypoints).toHaveLength(2); const poi = parsed.waypoints[0]!; expect(poi.osmId).toBe(987); expect(poi.poiTags?.phone).toBe("+49 30 999"); expect(poi.poiTags?.website).toBe("https://camp.example"); const plain = parsed.waypoints[1]!; expect(plain.osmId).toBeUndefined(); expect(plain.poiTags).toBeUndefined(); }); it("emits inside when waypoint has a note", () => { const gpx = generateGpx({ waypoints: [{ lat: 52.52, lon: 13.405, name: "Berlin", note: "Water refill here" }], }); // Should be inside , not expect(gpx).toContain("Water refill here"); // Confirm it's inside the wpt block, not metadata const wptBlock = gpx.slice(gpx.indexOf("") + 6); expect(wptBlock).toContain("Water refill here"); }); it("roundtrips waypoint note through generate + parse", async () => { const gpx = generateGpx({ name: "Test route", description: "Route-level desc", waypoints: [ { lat: 52.52, lon: 13.405, name: "Berlin", note: "Bike shop – open weekdays" }, { lat: 48.0, lon: 11.0, name: "Munich" }, ], }); const parsed = await parseGpxAsync(gpx); expect(parsed.waypoints[0]!.note).toBe("Bike shop – open weekdays"); expect(parsed.waypoints[1]!.note).toBeUndefined(); // Route-level description must not bleed into waypoint notes expect(parsed.description).toBe("Route-level desc"); }); it("escapes XML special chars in notes", () => { const gpx = generateGpx({ waypoints: [{ lat: 0, lon: 0, note: 'Water & food "maybe"' }], }); expect(gpx).toContain("Water & food <here> "maybe""); }); it("omits extensions element when waypoint has no POI data", () => { const gpx = generateGpx({ waypoints: [{ lat: 52.0, lon: 13.0, name: "Plain" }], }); expect(gpx).not.toContain(""); expect(gpx).not.toContain("trails:poi"); }); });