mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
fix(tours): check a city walk's stops off and save a stop from its dialog, with the walk under test (CW-01 to CW-05, CW-09)
Marking a stop of a city walk visited or skipped answered with a server error every time: the write that keeps a changed status stamped its time as a function call, which neither SQLite nor Postgres takes. It now uses the clock the database itself spells, so a stop checked off, skipped or taken back stays that way and the day plan of everyone on the trip follows. The Save button of a stop's dialog in the planner did nothing, because it sat outside the dialog's form. It saves the name, stay, importance and protection again. A walk saved with two points that carry one key, as a loop back to the start does, keeps that key on the first point and gives the other one of its own, so checking one off never marks both. A walk put on one day twice is read for an assistant from its first assignment. The server tests cover the visits from the repository to the routes: per day, through a moved and a removed assignment, a copy of the walk and a copied trip without visits, a removed stop taking its visits along, and the refusals for another trip, a stop the walk does not have, a member who may not edit days and an unknown key or body. The MCP progress of a walk, the station fields and counts of the tour list, and the contracts in shared are tested too; the existing tests expect the keys every saved point now carries, the station count of a list row and the new entity. The client tests cover the stop dialog and its pills, the walk themes and categories of the search along a tour, the stop menu of the waypoint list, the walk's timing, routing and type choice, and answer the trip planner's read of the visits in the tests instead of leaving it unanswered.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
TOUR_POI_CATEGORIES,
|
||||
TOUR_POI_CATEGORIES_BY_TYPE,
|
||||
TOUR_POI_MAX_CATEGORIES,
|
||||
tourPoiSearchRequestSchema,
|
||||
tourPoiSearchResponseSchema,
|
||||
WALK_THEME_KEYS,
|
||||
WALK_THEMES,
|
||||
} from './tour-poi-search.schema';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
@@ -78,3 +81,25 @@ describe('tour POI search contracts (PLN-02)', () => {
|
||||
expect(tourPoiSearchResponseSchema.safeParse(answer).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('city walk themes (CW-05)', () => {
|
||||
it('offers four themes, each a search a walk may ask for', () => {
|
||||
expect(WALK_THEME_KEYS).toEqual(['culture', 'parks', 'food', 'mixed']);
|
||||
for (const key of WALK_THEME_KEYS) {
|
||||
const categories = WALK_THEMES[key];
|
||||
expect(categories.length).toBeGreaterThan(0);
|
||||
expect(categories.length).toBeLessThanOrEqual(TOUR_POI_MAX_CATEGORIES);
|
||||
for (const category of categories) expect(TOUR_POI_CATEGORIES_BY_TYPE.walk).toContain(category);
|
||||
const search = { tour_type: 'walk', categories: [...categories], corridor_m: 250, line };
|
||||
expect(tourPoiSearchRequestSchema.safeParse(search).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a walk's own kinds to a walk's list, and a hike's to a hike's", () => {
|
||||
for (const category of ['museum', 'park', 'cafe', 'restaurant', 'market'] as const) {
|
||||
expect(TOUR_POI_CATEGORIES_BY_TYPE.walk).toContain(category);
|
||||
expect(TOUR_POI_CATEGORIES_BY_TYPE.hike).not.toContain(category);
|
||||
}
|
||||
expect(TOUR_POI_CATEGORIES_BY_TYPE.walk).not.toContain('peak');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
TOUR_VISIT_STATUSES,
|
||||
tourVisitListResponseSchema,
|
||||
tourVisitSchema,
|
||||
tourVisitUpdateRequestSchema,
|
||||
WALK_IMPORT_URL_MAX,
|
||||
walkImportPreviewRequestSchema,
|
||||
walkImportPreviewResponseSchema,
|
||||
} from './tour-walk.schema';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('city walk visits (CW-03)', () => {
|
||||
const visit = { assignment_id: 9, station_key: 'stop-cathedral', status: 'visited' };
|
||||
|
||||
it('reads a visit of a station on a day, visited or skipped', () => {
|
||||
expect(TOUR_VISIT_STATUSES).toEqual(['visited', 'skipped']);
|
||||
expect(tourVisitListResponseSchema.parse({ visits: [visit, { ...visit, status: 'skipped' }] }).visits).toHaveLength(
|
||||
2,
|
||||
);
|
||||
expect(tourVisitListResponseSchema.parse({ visits: [] })).toEqual({ visits: [] });
|
||||
});
|
||||
|
||||
it('refuses a visit that is planned, has no assignment or names no station key', () => {
|
||||
expect(tourVisitSchema.safeParse({ ...visit, status: 'planned' }).success).toBe(false);
|
||||
expect(tourVisitSchema.safeParse({ ...visit, assignment_id: 0 }).success).toBe(false);
|
||||
expect(tourVisitSchema.safeParse({ ...visit, station_key: 'a/b' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('takes a station back to planned, marks it visited or skipped, and takes nothing else', () => {
|
||||
for (const status of ['planned', 'visited', 'skipped']) {
|
||||
expect(tourVisitUpdateRequestSchema.parse({ status })).toEqual({ status });
|
||||
}
|
||||
expect(tourVisitUpdateRequestSchema.safeParse({ status: 'done' }).success).toBe(false);
|
||||
expect(tourVisitUpdateRequestSchema.safeParse({}).success).toBe(false);
|
||||
expect(tourVisitUpdateRequestSchema.safeParse({ status: 'visited', note: 'late' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('city walk import preview contract (CW-06, CW-07)', () => {
|
||||
it('takes one link, trimmed, up to its length, and nothing else', () => {
|
||||
expect(walkImportPreviewRequestSchema.parse({ url: ' https://maps.app.goo.gl/abc ' })).toEqual({
|
||||
url: 'https://maps.app.goo.gl/abc',
|
||||
});
|
||||
expect(walkImportPreviewRequestSchema.safeParse({ url: ' ' }).success).toBe(false);
|
||||
expect(walkImportPreviewRequestSchema.safeParse({ url: 'x'.repeat(WALK_IMPORT_URL_MAX) }).success).toBe(true);
|
||||
expect(walkImportPreviewRequestSchema.safeParse({ url: 'x'.repeat(WALK_IMPORT_URL_MAX + 1) }).success).toBe(false);
|
||||
expect(walkImportPreviewRequestSchema.safeParse({ url: 'https://maps.app.goo.gl/abc', mode: 'walk' }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('answers the stops the link names, a stop without coordinates left without them', () => {
|
||||
const answer = walkImportPreviewResponseSchema.parse({
|
||||
stations: [
|
||||
{ name: 'Cathedral', lat: 48.1, lng: 11.5 },
|
||||
{ name: null, lat: null, lng: null },
|
||||
],
|
||||
resolved_url: 'https://www.google.com/maps/dir/...',
|
||||
});
|
||||
expect(answer.stations[1]).toEqual({ name: null, lat: null, lng: null });
|
||||
expect(
|
||||
walkImportPreviewResponseSchema.safeParse({ stations: [{ name: 'X', lat: 91, lng: 0 }], resolved_url: '' })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,10 @@ import {
|
||||
hikingPreferencesOf,
|
||||
MAX_PADDLE_SPEED_KMH,
|
||||
MAX_PLANNED_TOUR_DURATION_MINUTES,
|
||||
MAX_STATION_STAY_MINUTES,
|
||||
recommendedPaddleSpeedKmh,
|
||||
plannedTourDurationMinutesSchema,
|
||||
stationKeySchema,
|
||||
tourCreateRequestSchema,
|
||||
tourListItemSchema,
|
||||
tourMaxHikingDifficultySchema,
|
||||
@@ -412,3 +414,100 @@ describe('bike tours (BIK-01 to BIK-04)', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('city walks (CW-01 to CW-03, CW-10)', () => {
|
||||
const walk = { ...baseRequest, tour_type: 'walk' as const };
|
||||
const station = {
|
||||
key: '0b7e7c5a-5d43-4c79-9b52-2f7f0d0e2c11',
|
||||
name: 'Cathedral',
|
||||
stay_minutes: 30,
|
||||
importance: 'must' as const,
|
||||
protected: true,
|
||||
opening_hours: 'Mo-Su 09:00-18:00',
|
||||
};
|
||||
const withFirst = <T extends { waypoints: typeof baseRequest.waypoints }>(request: T, extra: object) => ({
|
||||
...request,
|
||||
waypoints: [{ ...request.waypoints[0], ...extra }, request.waypoints[1]],
|
||||
});
|
||||
|
||||
it('saves a walk with its stations, and no hiking limit', () => {
|
||||
const parsed = tourCreateRequestSchema.parse(withFirst(walk, station));
|
||||
expect(parsed).toMatchObject({ tour_type: 'walk', max_hiking_difficulty: null });
|
||||
expect(parsed.waypoints[0]).toMatchObject(station);
|
||||
});
|
||||
|
||||
it("keeps a station's stay, importance and protection to a city walk", () => {
|
||||
for (const extra of [{ stay_minutes: 20 }, { importance: 'optional' }, { protected: true }]) {
|
||||
expect(() => tourCreateRequestSchema.parse(withFirst(baseRequest, extra))).toThrow(
|
||||
/stations belong to a city walk/,
|
||||
);
|
||||
}
|
||||
// A key, a name and opening hours are any point's, and protection switched off is no protection.
|
||||
const named = withFirst(baseRequest, { key: 'stop-a1', name: 'Hut', opening_hours: '24/7', protected: false });
|
||||
expect(tourCreateRequestSchema.safeParse(named).success).toBe(true);
|
||||
});
|
||||
|
||||
it('refuses a walk over a via ferrata, with a hiking limit or with hiking preferences', () => {
|
||||
const ferrata = {
|
||||
kind: 'ferrata' as const,
|
||||
ref: 'way/1',
|
||||
name: null,
|
||||
geometry: [
|
||||
[48, 11],
|
||||
[48.01, 11.02],
|
||||
],
|
||||
};
|
||||
expect(() => tourCreateRequestSchema.parse(withFirst(walk, { fixed_leg: ferrata }))).toThrow(
|
||||
/a city walk has no via ferrata/,
|
||||
);
|
||||
expect(() => tourCreateRequestSchema.parse({ ...walk, max_hiking_difficulty: 3 })).toThrow(
|
||||
/a city walk has no hiking difficulty/,
|
||||
);
|
||||
expect(() => tourCreateRequestSchema.parse({ ...walk, hiking_preferences: { avoid_steps: true } })).toThrow(
|
||||
/a city walk has no hiking preferences/,
|
||||
);
|
||||
});
|
||||
|
||||
it('takes a station key of 6 to 64 letters, digits, dashes and underscores', () => {
|
||||
expect(stationKeySchema.safeParse('0b7e7c5a-5d43-4c79-9b52-2f7f0d0e2c11').success).toBe(true);
|
||||
expect(stationKeySchema.safeParse('stop_1').success).toBe(true);
|
||||
expect(stationKeySchema.safeParse('x'.repeat(64)).success).toBe(true);
|
||||
for (const key of ['stop1', 'x'.repeat(65), 'stop/1', 'stop 1', 'stöp-1']) {
|
||||
expect(stationKeySchema.safeParse(key).success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('takes a stay from 0, a point passed through, to a whole day', () => {
|
||||
expect(MAX_STATION_STAY_MINUTES).toBe(1440);
|
||||
for (const stay_minutes of [0, 1440, null]) {
|
||||
expect(tourCreateRequestSchema.safeParse(withFirst(walk, { stay_minutes })).success).toBe(true);
|
||||
}
|
||||
for (const stay_minutes of [1441, -1, 12.5]) {
|
||||
expect(tourCreateRequestSchema.safeParse(withFirst(walk, { stay_minutes })).success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("lists a walk's station count, and reads an item cached before it existed", () => {
|
||||
const item = tourListItemSchema.parse({
|
||||
place_id: 1,
|
||||
name: 'Old town',
|
||||
tour_type: 'walk',
|
||||
distance: null,
|
||||
elevation_gain: null,
|
||||
elevation_loss: null,
|
||||
duration: null,
|
||||
difficulty: null,
|
||||
wanderer_ref: null,
|
||||
match_confidence: null,
|
||||
max_hiking_difficulty: null,
|
||||
planned: false,
|
||||
caution: false,
|
||||
has_waypoints: true,
|
||||
station_count: 3,
|
||||
});
|
||||
expect(item.station_count).toBe(3);
|
||||
const { station_count: _count, ...cached } = item;
|
||||
expect(tourListItemSchema.parse(cached).station_count).toBeUndefined();
|
||||
expect(tourListItemSchema.safeParse({ ...item, station_count: -1 }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user