Files
TREK/client/src/store/paddleObstacleStore.test.ts
T
Maurice de86e6383a feat(tours): the obstacle checklist, the decision panel and the safety note in the planner
PAD-06 and PAD-09 on screen. A paddle tour open in the planner or shown as
recorded carries a checklist card: the obstacles of the data along its
route in route order with the paddler's own portages and locks among them,
each with its status, its paddling rule and the SUP advice at a lock, and
the earlier decisions the route or the data no longer carries behind a
fold. A row moves the camera onto the obstacle and the other parts of its
lock and opens its card on the map, where the decision panel sits: four
choices, the notices that stay after a choice (a ban, the SUP advice, a
portage still to plan), the date of the last check on site, a note and who
decided when. An object the map does not hold opens in a dialog instead.
Deciding waits while the route has unsaved changes, and a stale or dirty
list is dimmed, never hidden.

The tours list, the desktop day plan and the phone day plan show the bench
as one pill, the tour detail shows it with the data's date, and every paddle
tour reaches the safety note from its footer. Nothing reads as safe where
TREK knows nothing, and nothing blocks.
2026-10-17 01:16:41 +00:00

96 lines
3.1 KiB
TypeScript

// FE-STORE-PADDLE-001 to FE-STORE-PADDLE-004: the camera focus of the paddle obstacle store (PAD-06). A click in
// the checklist selects an object and frames it with the other parts of its lock; an area is framed as asked; each
// focus is numbered so the newest wins; publishing new objects keeps it, clearing the store drops it.
import type { PaddleFeature, PaddleGeometry } from '@trek/shared';
import { beforeEach, describe, expect, it } from 'vitest';
import {
clearPaddleObstacles,
focusPaddleArea,
focusPaddleObstacle,
publishPaddleObstacles,
usePaddleObstacleStore,
} from './paddleObstacleStore';
const point = (lng: number, lat: number): PaddleGeometry => ({ type: 'Point', coordinates: [lng, lat] });
function feature(ref: string, geometry: PaddleGeometry, groupKey: string | null = null): PaddleFeature {
return {
ref,
kind: 'lock',
group_key: groupKey,
name: null,
geometry,
tags: {},
tag_hash: ref,
region_id: 'oberpfalz',
};
}
const LOCK = [
feature('node/3', point(12.001, 49.2), 'lock:node/3'),
feature('node/4', point(12.003, 49.201), 'lock:node/3'),
];
const WEIR = feature('node/9', point(12.01, 49.21));
function publish(features: PaddleFeature[]): void {
publishPaddleObstacles({
status: 'ready',
features,
related: [],
routeLengthM: 1000,
coverage: 'covered',
regions: [],
truncated: false,
});
}
beforeEach(() => {
clearPaddleObstacles();
});
describe('paddleObstacleStore focus', () => {
it('FE-STORE-PADDLE-001: focusing a lock part selects it and frames every part with a margin', () => {
publish([...LOCK, WEIR]);
expect(focusPaddleObstacle('node/4')).toBe(true);
const { selectedRef, focus } = usePaddleObstacleStore.getState();
expect(selectedRef).toBe('node/4');
expect(focus?.seq).toBe(1);
const [[south, west], [north, east]] = focus?.points ?? [
[0, 0],
[0, 0],
];
expect(south).toBeCloseTo(49.2 - 0.0015, 6);
expect(west).toBeCloseTo(12.001 - 0.0015, 6);
expect(north).toBeCloseTo(49.201 + 0.0015, 6);
expect(east).toBeCloseTo(12.003 + 0.0015, 6);
});
it('FE-STORE-PADDLE-002: an object the store does not hold is not focused', () => {
publish([WEIR]);
expect(focusPaddleObstacle('node/404')).toBe(false);
expect(usePaddleObstacleStore.getState()).toMatchObject({ selectedRef: null, focus: null });
});
it('FE-STORE-PADDLE-003: an area is framed as asked, each focus numbered after the last, none for no points', () => {
focusPaddleArea([[49.2, 12.005]]);
focusPaddleArea([]);
focusPaddleArea([
[49.2, 12],
[49.3, 12.1],
]);
const { focus } = usePaddleObstacleStore.getState();
expect(focus?.seq).toBe(2);
expect(focus?.points[1]?.[0]).toBeCloseTo(49.3015, 6);
});
it('FE-STORE-PADDLE-004: publishing new objects keeps the focus, clearing the store drops it', () => {
publish([WEIR]);
focusPaddleObstacle('node/9');
publish([WEIR, ...LOCK]);
expect(usePaddleObstacleStore.getState().focus?.seq).toBe(1);
clearPaddleObstacles();
expect(usePaddleObstacleStore.getState().focus).toBeNull();
});
});