Files
TREK/server/tests/unit/nest/tour-plan-read.test.ts
T
Maurice 164a3633ac feat(tours): plan ski and winter tours, snowshoeing and winter hiking among them
Ski becomes "Ski & winter", a type the planner plans with six disciplines
(alpine, snowboard, cross-country, ski touring, snowshoeing, winter hiking),
nothing preselected; saving needs a discipline, and a technique for
cross-country. Every leg is drawn straight between the points through
planner/skiRouting.ts, the one place the piste router will plug in, with its
heights from the elevation source and no ski time worked out.

The hike's winter label goes entirely: its migration, column, field, marks,
GPX rule, MCP field and strings. A hike is summer only; a ski tour with the
snowshoe discipline exports as snowshoeing. The unreleased ski migration takes
the six disciplines and the collection migration loses the winter column.

The pistes are offered for a ski tour, shown until switched off, and with no
tour open, never for a hike. Export, copy, collections, stages (untimed),
actual tracks, the atlas, list and day marks (one glyph per discipline), 3D,
planning over a ski import and list_tours/get_tour_plan know the planned ski
tour.
2026-10-20 18:40:48 +02:00

146 lines
5.6 KiB
TypeScript

import { derivedFields, planLegs, planLine, planPoints } from '../../../src/nest/tours/tour-plan-read';
import type { WalkProgress } from '../../../src/nest/tours/tour-visits.service';
import type { TourDetailResponse, TourWaypoint } from '@trek/shared';
import { describe, expect, it } from 'vitest';
// TOUR-PLAN-READ-001 to TOUR-PLAN-READ-007
/** The detail of a tour of this type, only as far as the plan reads it. */
function detailOf(tourType: TourDetailResponse['tour']['tour_type']): TourDetailResponse {
return { tour: { tour_type: tourType }, waypoints: [] } as unknown as TourDetailResponse;
}
const point = (sequence: number, extra: Partial<TourWaypoint> = {}): TourWaypoint => ({
lat: 48 + sequence * 0.01,
lng: 11,
role: 'via',
sequence,
...extra,
});
/** A walk on a day where the station keyed `seen` was visited. */
const run: WalkProgress = {
place_id: 1,
day_id: 2,
assignment_id: 3,
stations: [
{
position: 1,
key: 'seen',
name: null,
lat: 48,
lng: 11,
status: 'visited',
importance: 'normal',
protected: false,
stay_minutes: null,
opening_hours: null,
linked_place_id: null,
},
],
open: { stations: 1, stay_minutes_known: 0, stays_unknown: 1 },
walking_minutes: null,
};
describe('tour plan read (MCP-T01)', () => {
it('TOUR-PLAN-READ-001: a station without a key is planned on the day read, never another station of it', () => {
const points = [point(0, { key: 'seen' }), point(1), point(2, { key: 'other' })];
expect(planPoints('walk', points, run).map((station) => 'visit_status' in station && station.visit_status)).toEqual(
['visited', 'planned', 'planned'],
);
});
it('TOUR-PLAN-READ-002: the points of any other type stay as stored, whatever day is read', () => {
const points = [point(0, { key: 'seen' }), point(1)];
expect(planPoints('bike', points, run)).toEqual(points);
});
it('TOUR-PLAN-READ-003: a portage is a fixed leg of its own kind, and a single point has no leg', () => {
const portage = point(0, {
fixed_leg: {
kind: 'portage',
geometry: [
[48, 11],
[48.01, 11],
],
},
});
expect(planLegs([portage, point(1)], { recorded: false })).toEqual([
expect.objectContaining({ kind: 'portage', name: null, length_km: 1.11 }),
]);
expect(planLegs([point(0)], { recorded: false })).toEqual([]);
});
it('TOUR-PLAN-READ-004: a line of fewer than two points is no line', () => {
expect(planLine([])).toBeNull();
expect(planLine([[48, 11]])).toBeNull();
});
it('TOUR-PLAN-READ-005: a paddle tour names its check bench and checklist as worked out, a walk its station counts', () => {
const none = { controlsFromTrack: false, line: null, run: null };
expect(derivedFields(detailOf('kayak'), none)).toEqual(
expect.arrayContaining(['tour.paddle_check', 'obstacle_check']),
);
expect(derivedFields(detailOf('kayak'), none)).not.toContain('tour.station_count');
const walk = derivedFields(detailOf('walk'), none);
expect(walk).toEqual(expect.arrayContaining(['tour.station_count', 'tour.station_stays_unknown']));
expect(walk).not.toContain('tour.paddle_check');
expect(walk).not.toContain('run.open');
});
it('TOUR-PLAN-READ-006: a leg of a recorded track is a track, never routed, and a fixed leg keeps its own kind', () => {
expect(planLegs([point(0), point(1)], { recorded: false })).toEqual([
expect.objectContaining({ kind: 'routed', length_km: null }),
]);
expect(planLegs([point(0), point(1)], { recorded: true })).toEqual([
expect.objectContaining({ kind: 'track', name: null, length_km: null }),
]);
const free = point(0, {
fixed_leg: {
kind: 'free',
geometry: [
[48, 11],
[48.01, 11],
],
},
});
expect(planLegs([free, point(1)], { recorded: true })).toEqual([expect.objectContaining({ kind: 'free' })]);
});
it('TOUR-PLAN-READ-007: an actual track names its metrics as worked out, a multi-day tour its stage count and stops', () => {
const none = { controlsFromTrack: false, line: null, run: null };
const plain = derivedFields(detailOf('hike'), none);
expect(plain).not.toContain('activities[].metrics');
expect(plain).not.toContain('tour.stage_count');
const tracked = { ...detailOf('hike'), activities: [{ id: 1 }] } as unknown as TourDetailResponse;
expect(derivedFields(tracked, none)).toContain('activities[].metrics');
expect(derivedFields({ ...detailOf('hike'), activities: [] }, none)).not.toContain('activities[].metrics');
const staged = {
...detailOf('hike'),
tour: { tour_type: 'hike', stage_count: 2 },
} as unknown as TourDetailResponse;
expect(derivedFields(staged, none)).toEqual(expect.arrayContaining(['tour.stage_count', 'tour.stage_stops']));
});
it('TOUR-PLAN-READ-008: every leg of a planned ski tour is drawn freely with its straight length, a fixed one kept (SKI-02)', () => {
const free = point(1, {
fixed_leg: {
kind: 'free',
geometry: [
[48.01, 11],
[48.02, 11],
],
},
});
expect(planLegs([point(0), free, point(2)], { recorded: false, drawn: true })).toEqual([
expect.objectContaining({ kind: 'free', name: null, length_km: 1.11 }),
expect.objectContaining({ kind: 'free', name: null, length_km: 1.11 }),
]);
// A ski track kept as recorded stays a track.
expect(planLegs([point(0), point(1)], { recorded: true, drawn: true })).toEqual([
expect.objectContaining({ kind: 'track', length_km: null }),
]);
});
});