Add unit tests for computeDays and GPX overnight roundtrip
- compute-days.test.ts: 8 tests covering single day, two days, three days, ascent/descent, edge cases (first/last overnight, single waypoint, empty) - generate.test.ts: 4 new tests for isDayBreak emission, GPX roundtrip with isDayBreak preserved, and splitByDay multi-track output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
774e2b1ae3
commit
77174f192d
3 changed files with 196 additions and 2 deletions
|
|
@ -42,9 +42,9 @@
|
|||
|
||||
## 8. Testing
|
||||
|
||||
- [ ] 8.1 Unit tests for `computeDays()`: single day (no overnight), two days, three days, empty route, single waypoint, overnight on first/last waypoint edge cases
|
||||
- [x] 8.1 Unit tests for `computeDays()`: single day (no overnight), two days, three days, empty route, single waypoint, overnight on first/last waypoint edge cases
|
||||
- [ ] 8.2 Unit tests for `overnight.ts` helpers: set/clear/check overnight on Y.Map
|
||||
- [ ] 8.3 Unit tests for GPX roundtrip: generate with `isDayBreak`, parse back, verify `isDayBreak` preserved
|
||||
- [x] 8.3 Unit tests for GPX roundtrip: generate with `isDayBreak`, parse back, verify `isDayBreak` preserved
|
||||
- [ ] 8.4 Unit tests for `dayBreaks` extraction in route update logic
|
||||
- [ ] 8.5 E2E test: add waypoints, toggle overnight on one, verify sidebar shows day breakdown with correct stats
|
||||
- [ ] 8.6 E2E test: export GPX with day breaks, verify downloaded file contains overnight metadata
|
||||
|
|
|
|||
139
packages/gpx/src/compute-days.test.ts
Normal file
139
packages/gpx/src/compute-days.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { computeDays } from "./compute-days.ts";
|
||||
import type { Waypoint } from "@trails-cool/types";
|
||||
import type { TrackPoint } from "./types.ts";
|
||||
|
||||
function makeTrack(coords: [number, number, number][]): TrackPoint[] {
|
||||
return coords.map(([lat, lon, ele]) => ({ lat, lon, ele }));
|
||||
}
|
||||
|
||||
describe("computeDays", () => {
|
||||
it("returns empty array for no waypoints", () => {
|
||||
expect(computeDays([], [[{ lat: 0, lon: 0 }]])).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array for no tracks", () => {
|
||||
expect(computeDays([{ lat: 0, lon: 0 }], [])).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns single day when no overnight waypoints", () => {
|
||||
const waypoints: Waypoint[] = [
|
||||
{ lat: 52.52, lon: 13.405, name: "Berlin" },
|
||||
{ lat: 51.34, lon: 12.375, name: "Leipzig" },
|
||||
];
|
||||
const track = makeTrack([
|
||||
[52.52, 13.405, 34],
|
||||
[52.0, 13.0, 50],
|
||||
[51.34, 12.375, 113],
|
||||
]);
|
||||
const days = computeDays(waypoints, [track]);
|
||||
expect(days).toHaveLength(1);
|
||||
expect(days[0]!.dayNumber).toBe(1);
|
||||
expect(days[0]!.startName).toBe("Berlin");
|
||||
expect(days[0]!.endName).toBe("Leipzig");
|
||||
expect(days[0]!.distance).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("splits into two days at overnight waypoint", () => {
|
||||
const waypoints: Waypoint[] = [
|
||||
{ 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" },
|
||||
];
|
||||
const track = makeTrack([
|
||||
[52.52, 13.405, 34],
|
||||
[52.2, 12.8, 60],
|
||||
[51.84, 12.243, 80],
|
||||
[51.4, 11.6, 200],
|
||||
[50.98, 11.028, 195],
|
||||
]);
|
||||
const days = computeDays(waypoints, [track]);
|
||||
expect(days).toHaveLength(2);
|
||||
|
||||
expect(days[0]!.dayNumber).toBe(1);
|
||||
expect(days[0]!.startName).toBe("Berlin");
|
||||
expect(days[0]!.endName).toBe("Dessau");
|
||||
|
||||
expect(days[1]!.dayNumber).toBe(2);
|
||||
expect(days[1]!.startName).toBe("Dessau");
|
||||
expect(days[1]!.endName).toBe("Erfurt");
|
||||
|
||||
// Total distance should roughly equal sum of days
|
||||
const totalDist = days[0]!.distance + days[1]!.distance;
|
||||
expect(totalDist).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("splits into three days with two overnight stops", () => {
|
||||
const waypoints: Waypoint[] = [
|
||||
{ lat: 0, lon: 0, name: "A" },
|
||||
{ lat: 1, lon: 0, name: "B", isDayBreak: true },
|
||||
{ lat: 2, lon: 0, name: "C", isDayBreak: true },
|
||||
{ lat: 3, lon: 0, name: "D" },
|
||||
];
|
||||
const track = makeTrack([
|
||||
[0, 0, 0], [0.5, 0, 10], [1, 0, 20],
|
||||
[1.5, 0, 30], [2, 0, 40],
|
||||
[2.5, 0, 50], [3, 0, 60],
|
||||
]);
|
||||
const days = computeDays(waypoints, [track]);
|
||||
expect(days).toHaveLength(3);
|
||||
expect(days[0]!.startName).toBe("A");
|
||||
expect(days[0]!.endName).toBe("B");
|
||||
expect(days[1]!.startName).toBe("B");
|
||||
expect(days[1]!.endName).toBe("C");
|
||||
expect(days[2]!.startName).toBe("C");
|
||||
expect(days[2]!.endName).toBe("D");
|
||||
});
|
||||
|
||||
it("computes ascent and descent per day", () => {
|
||||
const waypoints: Waypoint[] = [
|
||||
{ lat: 0, lon: 0 },
|
||||
{ lat: 1, lon: 0, isDayBreak: true },
|
||||
{ lat: 2, lon: 0 },
|
||||
];
|
||||
const track = makeTrack([
|
||||
[0, 0, 100],
|
||||
[0.5, 0, 200], // +100 ascent
|
||||
[1, 0, 150], // -50 descent
|
||||
[1.5, 0, 50], // -100 descent
|
||||
[2, 0, 250], // +200 ascent
|
||||
]);
|
||||
const days = computeDays(waypoints, [track]);
|
||||
expect(days).toHaveLength(2);
|
||||
expect(days[0]!.ascent).toBe(100);
|
||||
expect(days[0]!.descent).toBe(50);
|
||||
expect(days[1]!.ascent).toBe(200);
|
||||
expect(days[1]!.descent).toBe(100);
|
||||
});
|
||||
|
||||
it("handles overnight on first waypoint gracefully", () => {
|
||||
const waypoints: Waypoint[] = [
|
||||
{ lat: 0, lon: 0, isDayBreak: true },
|
||||
{ lat: 1, lon: 0 },
|
||||
];
|
||||
const track = makeTrack([[0, 0, 0], [1, 0, 100]]);
|
||||
const days = computeDays(waypoints, [track]);
|
||||
// First waypoint is implicit start of Day 1, isDayBreak on it creates
|
||||
// a zero-length day followed by the real day
|
||||
expect(days.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("handles overnight on last waypoint gracefully", () => {
|
||||
const waypoints: Waypoint[] = [
|
||||
{ lat: 0, lon: 0 },
|
||||
{ lat: 1, lon: 0, isDayBreak: true },
|
||||
];
|
||||
const track = makeTrack([[0, 0, 0], [1, 0, 100]]);
|
||||
const days = computeDays(waypoints, [track]);
|
||||
// Last waypoint is implicit end, isDayBreak on it = single day
|
||||
expect(days.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("handles single waypoint", () => {
|
||||
const waypoints: Waypoint[] = [{ lat: 0, lon: 0 }];
|
||||
const track = makeTrack([[0, 0, 0]]);
|
||||
const days = computeDays(waypoints, [track]);
|
||||
// Single waypoint can't form a meaningful day with distance
|
||||
expect(days).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -48,4 +48,59 @@ describe("generateGpx", () => {
|
|||
expect(parsed.waypoints[0]!.name).toBe("Start");
|
||||
expect(parsed.tracks[0]).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("emits <type>overnight</type> 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("<type>overnight</type>");
|
||||
// Only Dessau should have the type element
|
||||
expect(gpx.match(/<type>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 <trk> elements
|
||||
expect(gpx.match(/<trk>/g)).toHaveLength(2);
|
||||
expect(gpx).toContain("<name>Day 1: A - B</name>");
|
||||
expect(gpx).toContain("<name>Day 2: B - C</name>");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue