trails/packages/gpx/src/moving-time.test.ts
Ullrich Schäfer 0325d01bca
activity-stats: shared StatRow + canonical metrics across surfaces
Implements the activity-stats change (specs/activity-stats):

- gpx: `movingTime(tracks)` — moving seconds from trackpoint timestamps,
  excluding stationary spans + long gaps; null when no timestamps.
- stats.ts: pure formatters (distance/elevation/duration/speed/pace),
  sport-aware `deriveRate` (pace for foot sports, speed otherwise, speed
  default), and `activityStatItems` encoding the canonical order
  (distance · time · [moving] · pace/speed · ascent · descent; compact subset).
- StatRow: one shared presentational component (size sm/lg), adopted on the
  activity detail (full set; loader derives moving time), route detail
  (distance/ascent/descent), feed card + profile list (compact).
- i18n: journal.stats.* in en + de.

Tests: movingTime (stationary/gap exclusion, moving ≤ elapsed), deriveRate,
formatters, activityStatItems ordering + compact, StatRow render (jsdom).
typecheck + lint + unit (gpx 63, journal 311) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:48:36 +02:00

56 lines
2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { movingTime } from "./moving-time.ts";
import type { TrackPoint } from "./types.ts";
// ~0.0001° latitude ≈ 11.1 m. At 11 m per interval, speed depends on dt.
function pt(lat: number, lon: number, time?: string): TrackPoint {
return { lat, lon, time };
}
describe("movingTime", () => {
it("returns null when no trackpoints carry timestamps", () => {
const tracks = [[pt(0, 0), pt(0, 0.0001), pt(0, 0.0002)]];
expect(movingTime(tracks)).toBeNull();
});
it("sums intervals where the athlete is moving", () => {
// Three points 10s apart, each ~11 m → ~1.1 m/s (moving). 2 intervals.
const tracks = [[
pt(0, 0, "2026-06-01T08:00:00Z"),
pt(0, 0.0001, "2026-06-01T08:00:10Z"),
pt(0, 0.0002, "2026-06-01T08:00:20Z"),
]];
expect(movingTime(tracks)).toBe(20);
});
it("excludes stationary spans (below the speed threshold)", () => {
// Middle interval: no movement over 10s → stopped, excluded.
const tracks = [[
pt(0, 0, "2026-06-01T08:00:00Z"),
pt(0, 0.0001, "2026-06-01T08:00:10Z"), // moving (~11 m / 10s)
pt(0, 0.0001, "2026-06-01T08:00:20Z"), // stationary (0 m)
pt(0, 0.0002, "2026-06-01T08:00:30Z"), // moving again
]];
expect(movingTime(tracks)).toBe(20); // 2 moving intervals of 10s
});
it("excludes long gaps (pauses / signal loss)", () => {
const tracks = [[
pt(0, 0, "2026-06-01T08:00:00Z"),
pt(0, 0.0001, "2026-06-01T08:00:10Z"), // 10s moving
pt(0, 0.0002, "2026-06-01T09:00:00Z"), // ~1h gap → excluded
]];
expect(movingTime(tracks)).toBe(10);
});
it("never exceeds elapsed time", () => {
const tracks = [[
pt(0, 0, "2026-06-01T08:00:00Z"),
pt(0, 0.0001, "2026-06-01T08:00:10Z"),
pt(0, 0.0001, "2026-06-01T08:00:40Z"), // 30s stationary
]];
const elapsed = 40;
const moving = movingTime(tracks)!;
expect(moving).toBeLessThanOrEqual(elapsed);
});
});