mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
feat(tours): a hike can be marked as a winter hike or a snowshoe hike
HIK-01. The details of the hiking planner gain an optional Winter activity switch, off by default, so the normal hiking flow asks for nothing more. Switched on, the hike is a winter hike or a snowshoe hike; the choice stays dimmed and out of reach while the switch is off. The label is saved with the tour, comes back when it is opened, survives a draft reload, a copy and a take-over, and switching it off takes only the label away: the route, its grade and the days stay as they were. The label is a label only. Routing, the walking time and the 3D terrain view work as for any hike, and the info dot and the head pill's tooltip say that nothing was checked for snow, avalanches or whether a path can be walked in winter. The saved label shows as a frosted snowflake on the corner of the tour's tile in the tour list and the day plan, and as a pill beside the type in the tour's detail. The server keeps it in tours.winter_activity (CHECK list, NULL for every tour saved before and for every other type), refuses it on a bike tour, a city walk and a paddle tour, and keeps the stored label when an older client saves a hike without the field. A snowshoe hike exports to GPX as snowshoeing. list_tours names the field, with the same words on what it does not say.
This commit is contained in:
@@ -66,6 +66,7 @@ const UNNUMBERED = [
|
||||
'Migration20200101045200_a_tour_can_be_ridden',
|
||||
'Migration20200101045300_a_tour_can_be_a_city_walk',
|
||||
'Migration20200101045400_a_tour_waypoint_remembers_the_kind_of_place_it_was_found_as',
|
||||
'Migration20200101045500_a_hike_can_be_marked_as_a_winter_activity',
|
||||
];
|
||||
// The last step the positional runner ever had; planLegacyBaseline refuses a
|
||||
// schema_version past it.
|
||||
|
||||
@@ -105,6 +105,7 @@ describe('ToursRepository reads', () => {
|
||||
revision: 1,
|
||||
hiking_preferences: null,
|
||||
bike_profile: null,
|
||||
winter_activity: null,
|
||||
},
|
||||
{
|
||||
place_id: older.id,
|
||||
@@ -137,6 +138,7 @@ describe('ToursRepository reads', () => {
|
||||
revision: 1,
|
||||
hiking_preferences: null,
|
||||
bike_profile: null,
|
||||
winter_activity: null,
|
||||
},
|
||||
]);
|
||||
expect((await repo.listForTrip(trip.id)).map((r) => r.place_id)).not.toContain(plain.id);
|
||||
@@ -250,6 +252,7 @@ describe('ToursRepository writes', () => {
|
||||
revision: 1,
|
||||
hiking_preferences: null,
|
||||
bike_profile: null,
|
||||
winter_activity: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,6 +290,22 @@ describe('ToursRepository writes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('TOURREPO-016: a hike keeps its winter label through an edit that leaves it out, and loses it only when told (HIK-01)', async () => {
|
||||
const { trip } = fixture();
|
||||
const place = createPlace(testDb, trip.id, { name: 'Snowy ridge' });
|
||||
await repo.insertTour({ place_id: place.id, ...METRICS, winter_activity: 'snowshoe' });
|
||||
expect((await repo.findInTrip(trip.id, place.id))?.winter_activity).toBe('snowshoe');
|
||||
|
||||
expect(await repo.updateInTrip(trip.id, place.id, HIKE_UPDATE)).toBe(true);
|
||||
expect((await repo.findInTrip(trip.id, place.id))?.winter_activity).toBe('snowshoe');
|
||||
|
||||
expect(await repo.updateInTrip(trip.id, place.id, { ...HIKE_UPDATE, winter_activity: 'winter_hike' })).toBe(true);
|
||||
expect((await repo.listForTrip(trip.id))[0]?.winter_activity).toBe('winter_hike');
|
||||
|
||||
expect(await repo.updateInTrip(trip.id, place.id, { ...HIKE_UPDATE, winter_activity: null })).toBe(true);
|
||||
expect((await repo.findInTrip(trip.id, place.id))?.winter_activity).toBeNull();
|
||||
});
|
||||
|
||||
it('TOURREPO-008: listRowsForTrip and insertCopy carry every column, created_at included, onto the copied place', async () => {
|
||||
const { trip, other } = fixture();
|
||||
const source = createPlace(testDb, trip.id);
|
||||
@@ -332,6 +351,7 @@ describe('ToursRepository writes', () => {
|
||||
revision: 1,
|
||||
hiking_preferences: null,
|
||||
bike_profile: null,
|
||||
winter_activity: null,
|
||||
taken_over_from_trip_title: null,
|
||||
taken_over_from_tour_name: null,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ToursWinterActivity } from '../../../src/db/entities/Tours.entity';
|
||||
import { WINTER_ACTIVITIES } from '@trek/shared';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* HIK-01: the winter label column holds a CHECK list (migration 045500) that
|
||||
* the entity carries as its enum, and the contract holds its own list. A label
|
||||
* one side knows and the other refuses would be a hike the planner can save
|
||||
* and the database cannot store.
|
||||
*/
|
||||
describe('winter activity column matches the contract', () => {
|
||||
it('knows the same winter activities as the tours contract', () => {
|
||||
expect(Object.values(ToursWinterActivity).sort()).toEqual([...WINTER_ACTIVITIES].sort());
|
||||
});
|
||||
});
|
||||
@@ -2,9 +2,9 @@ import { gpxActivityOf } from '../../../src/nest/tours/tour-gpx-export.helpers';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// TOUR-GPX-ACTIVITY-001 to TOUR-GPX-ACTIVITY-003
|
||||
// TOUR-GPX-ACTIVITY-001 to TOUR-GPX-ACTIVITY-004
|
||||
|
||||
describe('gpxActivityOf (PR1-06A, BIK-01)', () => {
|
||||
describe('gpxActivityOf (PR1-06A, BIK-01, HIK-01)', () => {
|
||||
it('TOUR-GPX-ACTIVITY-001: a bike tour names its profile, and the plainest riding without a known one', () => {
|
||||
expect(gpxActivityOf({ tour_type: 'bike', bike_profile: 'mtb' })).toBe('mountain_biking');
|
||||
expect(gpxActivityOf({ tour_type: 'bike', bike_profile: null })).toBe('cycling');
|
||||
@@ -21,4 +21,11 @@ describe('gpxActivityOf (PR1-06A, BIK-01)', () => {
|
||||
expect(gpxActivityOf({ tour_type: 'hike' })).toBe('hiking');
|
||||
expect(gpxActivityOf({ tour_type: 'walk' })).toBe('walk');
|
||||
});
|
||||
|
||||
it('TOUR-GPX-ACTIVITY-004: a hike on snowshoes is snowshoeing, a winter hike stays hiking (HIK-01)', () => {
|
||||
expect(gpxActivityOf({ tour_type: 'hike', winter_activity: 'snowshoe' })).toBe('snowshoeing');
|
||||
expect(gpxActivityOf({ tour_type: 'hike', winter_activity: 'winter_hike' })).toBe('hiking');
|
||||
expect(gpxActivityOf({ tour_type: 'hike', winter_activity: null })).toBe('hiking');
|
||||
expect(gpxActivityOf({ tour_type: 'walk', winter_activity: 'snowshoe' })).toBe('walk');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Places } from '../../../src/db/entities/Places.entity';
|
||||
import { TourStationVisits } from '../../../src/db/entities/TourStationVisits.entity';
|
||||
import { TourWaypoints } from '../../../src/db/entities/TourWaypoints.entity';
|
||||
import { Tours } from '../../../src/db/entities/Tours.entity';
|
||||
import type { McpContext } from '../../../src/nest-mcp';
|
||||
import { getEntry, type ClassRef } from '../../../src/nest-mcp/metadata';
|
||||
import { PlacesMcp } from '../../../src/nest/places/places.mcp';
|
||||
@@ -355,4 +356,19 @@ describe('ToursMcp', () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('TOURS-MCP-015: list_tours names the winter label of a hike, and says it is only a label (HIK-01)', async () => {
|
||||
const { user, trip, ridge, gorge } = fixture();
|
||||
const orm = await createTestOrm(db);
|
||||
await updateRows(orm, Tours, { place: ridge.id }, { winter_activity: 'snowshoe' });
|
||||
await orm.close();
|
||||
const { tours } = read(await mcp.listTours({ tripId: trip.id }, ctx(user.id)));
|
||||
const byId = new Map(tours.map((tour: { place_id: number }) => [tour.place_id, tour]));
|
||||
expect(byId.get(ridge.id)).toMatchObject({ tour_type: 'hike', winter_activity: 'snowshoe' });
|
||||
expect(byId.get(gorge.id)).toMatchObject({ winter_activity: null });
|
||||
const entry = getEntry(ToursMcp as unknown as ClassRef, 'listTours');
|
||||
const description = entry?.kind === 'tool' ? (entry.options.description ?? '') : '';
|
||||
expect(description).toMatch(/winter_activity: winter_hike or snowshoe/);
|
||||
expect(description).toContain('never a statement that a way can be walked in winter');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user