import { describe, it, expect } from "vitest"; import { generateGpx } from "@trails-cool/gpx"; import { processGpx, validateGpx, GpxValidationError } from "./gpx-save.server.ts"; const VALID_GPX = ` 500 520 510 `; const ONE_POINT_GPX = ` 500 `; const EMPTY_GPX = ` `; const OUT_OF_RANGE_LAT_GPX = ` `; const OUT_OF_RANGE_LON_GPX = ` `; describe("validateGpx", () => { it("returns parsed GpxData for valid GPX", async () => { const result = await validateGpx(VALID_GPX); expect(result.tracks.flat().length).toBe(3); expect(result.tracks[0]![0]!.lat).toBeCloseTo(47.0); }); it("throws GpxValidationError for GPX with fewer than 2 track points", async () => { await expect(validateGpx(ONE_POINT_GPX)).rejects.toThrow(GpxValidationError); await expect(validateGpx(ONE_POINT_GPX)).rejects.toThrow("at least 2 track points"); }); it("throws GpxValidationError for GPX with zero track points", async () => { await expect(validateGpx(EMPTY_GPX)).rejects.toThrow(GpxValidationError); await expect(validateGpx(EMPTY_GPX)).rejects.toThrow("at least 2 track points"); }); it("throws GpxValidationError for out-of-range latitude", async () => { await expect(validateGpx(OUT_OF_RANGE_LAT_GPX)).rejects.toThrow(GpxValidationError); await expect(validateGpx(OUT_OF_RANGE_LAT_GPX)).rejects.toThrow("out-of-range coordinates"); }); it("throws GpxValidationError for out-of-range longitude", async () => { await expect(validateGpx(OUT_OF_RANGE_LON_GPX)).rejects.toThrow(GpxValidationError); await expect(validateGpx(OUT_OF_RANGE_LON_GPX)).rejects.toThrow("out-of-range coordinates"); }); it("throws GpxValidationError for unparseable XML", async () => { await expect(validateGpx("not xml at all")).rejects.toThrow(GpxValidationError); await expect(validateGpx(" { const err = await validateGpx(EMPTY_GPX).catch((e) => e); expect(err).toBeInstanceOf(GpxValidationError); expect(err.name).toBe("GpxValidationError"); }); }); const TIMED_GPX = ` An alpine loop 500 520 `; describe("processGpx", () => { it("extracts coords in [lon, lat] PostGIS axis order", async () => { const { coords } = await processGpx(VALID_GPX); expect(coords).toEqual([ [8.0, 47.0], [8.1, 47.1], [8.2, 47.2], ]); }); it("derives distance, elevation, description, and start time", async () => { const { stats } = await processGpx(TIMED_GPX); expect(stats.distance).toBeGreaterThan(0); expect(stats.elevationGain).not.toBeNull(); expect(stats.elevationLoss).not.toBeNull(); expect(stats.description).toBe("An alpine loop"); expect(stats.startTime).toEqual(new Date("2026-06-01T08:00:00Z")); }); it("returns null startTime and empty dayBreaks when absent", async () => { const { stats } = await processGpx(VALID_GPX); expect(stats.startTime).toBeNull(); expect(stats.dayBreaks).toEqual([]); }); it("derives dayBreaks from day-break waypoints (round-trip via generateGpx)", async () => { const gpx = generateGpx({ name: "Multi-day", waypoints: [ { lat: 47.0, lon: 8.0, name: "Start" }, { lat: 47.1, lon: 8.1, name: "Hut", isDayBreak: true }, { lat: 47.2, lon: 8.2, name: "End" }, ], tracks: [[ { lat: 47.0, lon: 8.0 }, { lat: 47.1, lon: 8.1 }, { lat: 47.2, lon: 8.2 }, ]], }); const { stats } = await processGpx(gpx); expect(stats.dayBreaks).toEqual([1]); }); it("propagates GpxValidationError from validation", async () => { await expect(processGpx(ONE_POINT_GPX)).rejects.toThrow(GpxValidationError); }); it("returns the parsed GpxData so callers never re-parse", async () => { const { parsed } = await processGpx(VALID_GPX); expect(parsed.tracks.flat()).toHaveLength(3); }); });