Files
TREK/client/src/components/Map/plannerWaypointDrag.test.ts
T
elmocito 57ea219a43 feat(tours): complete planner follow-up scope
This commit completes the reviewed Tours follow-up scope after the Tours MVP was merged into dev.

Included feature blocks:

- RC-01: identify Tours in PLAN day rows and show compact Tour facts;
- RC-06: search for a city or address in TOUR-PLANNER without hidden writes;
- RC-07: reorder Tour waypoints in the left sidebar;
- RC-08: drag Tour waypoints on Leaflet, MapLibre and Mapbox with stable viewport, history and routing behavior;
- RS-01: persist Tour descriptions and HTTPS information links with local provider classification;
- RS-02: persist breaks/additional time and calculated or manually adjusted planned Tour duration without changing scheduling semantics.
The implementation preserves:

- ordinary Place and Legacy Track behavior;
- Tours-off fallback behavior;
- existing GPX/import behavior;
- Tour assignment and deletion behavior;
- calculated routing duration as a separate value;
- existing Tour provenance fields;
- keyboard/list alternatives for waypoint editing.
Not included:

- PR1-09 trip-wide Tour summary;
- Wanderer integration;
- GPX import changes;
- new Tour types;
- Tour-to-Day drag-and-drop;
- further roadmap or Post-Release features.
Validation:

- focused RC-01/RC-06/RC-07/RC-08/RS-01/RS-02 tests passed;
- affected shared/server/client typechecks passed;
- migration and generated DB type checks passed;
- scoped ESLint passed;
- staged diff checks passed;
- owner manual tests passed for all six feature blocks.
2026-10-14 08:39:31 +02:00

153 lines
5.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { wirePlannerWaypointDrag } from './plannerWaypointDrag';
function pointer(type: string, x: number, y: number, pointerId = 1, pointerType: 'mouse' | 'touch' = 'mouse'): Event {
const event = new Event(type, { bubbles: true, cancelable: true });
Object.defineProperties(event, {
button: { value: 0 },
clientX: { value: x },
clientY: { value: y },
pointerId: { value: pointerId },
pointerType: { value: pointerType },
});
return event;
}
describe('Tour waypoint marker drag lifecycle', () => {
it('previews during a drag and commits once on pointer up, swallowing only the generated click', () => {
const element = document.createElement('button');
const initial = { lat: 48, lng: 11 };
const preview = vi.fn();
const onCommit = vi.fn(() => true);
const cleanup = wirePlannerWaypointDrag({
element,
initial,
coordinateAt: (clientX, clientY) => ({ lat: clientY / 100, lng: clientX / 100 }),
setPosition: preview,
onCommit,
});
element.dispatchEvent(pointer('pointerdown', 10, 10));
element.dispatchEvent(pointer('pointermove', 11, 11));
expect(preview).not.toHaveBeenCalled();
expect(onCommit).not.toHaveBeenCalled();
element.dispatchEvent(pointer('pointermove', 20, 30));
expect(preview).toHaveBeenLastCalledWith({ lat: 0.3, lng: 0.2 });
expect(onCommit).not.toHaveBeenCalled();
element.dispatchEvent(pointer('pointerup', 30, 40));
expect(preview).toHaveBeenLastCalledWith({ lat: 0.4, lng: 0.3 });
expect(onCommit).toHaveBeenCalledOnce();
expect(onCommit).toHaveBeenCalledWith({ lat: 0.4, lng: 0.3 });
const generatedClick = new MouseEvent('click', { bubbles: true, cancelable: true });
element.dispatchEvent(generatedClick);
expect(generatedClick.defaultPrevented).toBe(true);
const nextClick = new MouseEvent('click', { bubbles: true, cancelable: true });
element.dispatchEvent(nextClick);
expect(nextClick.defaultPrevented).toBe(false);
cleanup();
});
it('restores the starting coordinate on pointer cancellation and does not commit', () => {
const element = document.createElement('button');
const initial = { lat: 48, lng: 11 };
const preview = vi.fn();
const onCommit = vi.fn();
const cleanup = wirePlannerWaypointDrag({
element,
initial,
coordinateAt: (clientX, clientY) => ({ lat: clientY, lng: clientX }),
setPosition: preview,
onCommit,
});
element.dispatchEvent(pointer('pointerdown', 10, 10));
element.dispatchEvent(pointer('pointermove', 20, 30));
element.dispatchEvent(pointer('pointercancel', 20, 30));
expect(preview).toHaveBeenLastCalledWith(initial);
expect(onCommit).not.toHaveBeenCalled();
cleanup();
});
it('restores the preview when the canonical commit is rejected', () => {
const element = document.createElement('button');
const initial = { lat: 48, lng: 11 };
const preview = vi.fn();
const cleanup = wirePlannerWaypointDrag({
element,
initial,
coordinateAt: (clientX, clientY) => ({ lat: clientY, lng: clientX }),
setPosition: preview,
onCommit: () => false,
});
element.dispatchEvent(pointer('pointerdown', 10, 10));
element.dispatchEvent(pointer('pointermove', 20, 30));
element.dispatchEvent(pointer('pointerup', 20, 30));
expect(preview).toHaveBeenLastCalledWith(initial);
cleanup();
});
it('keeps pointer, mouse, and touch starts from bubbling into map pan handlers', () => {
const mapSurface = document.createElement('div');
const element = document.createElement('button');
mapSurface.append(element);
const mapPan = vi.fn();
mapSurface.addEventListener('pointerdown', mapPan);
mapSurface.addEventListener('mousedown', mapPan);
mapSurface.addEventListener('touchstart', mapPan);
const cleanup = wirePlannerWaypointDrag({
element,
initial: { lat: 48, lng: 11 },
coordinateAt: () => ({ lat: 48, lng: 11 }),
setPosition: vi.fn(),
onCommit: vi.fn(),
});
element.dispatchEvent(pointer('pointerdown', 10, 10));
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0 }));
element.dispatchEvent(new Event('touchstart', { bubbles: true, cancelable: true }));
expect(mapPan).not.toHaveBeenCalled();
cleanup();
});
it('commits a touch-pointer drag once at release', () => {
const element = document.createElement('button');
const onCommit = vi.fn(() => true);
const cleanup = wirePlannerWaypointDrag({
element,
initial: { lat: 48, lng: 11 },
coordinateAt: (clientX, clientY) => ({ lat: clientY / 10, lng: clientX / 10 }),
setPosition: vi.fn(),
onCommit,
});
element.dispatchEvent(pointer('pointerdown', 5, 5, 9, 'touch'));
element.dispatchEvent(pointer('pointermove', 25, 35, 9, 'touch'));
expect(onCommit).not.toHaveBeenCalled();
element.dispatchEvent(pointer('pointerup', 25, 35, 9, 'touch'));
expect(onCommit).toHaveBeenCalledOnce();
expect(onCommit).toHaveBeenCalledWith({ lat: 3.5, lng: 2.5 });
cleanup();
});
it('restores existing touch and selection styles when editing drag wiring is removed', () => {
const element = document.createElement('button');
element.style.touchAction = 'pan-x';
element.style.userSelect = 'text';
const cleanup = wirePlannerWaypointDrag({
element,
initial: { lat: 48, lng: 11 },
coordinateAt: () => ({ lat: 48, lng: 11 }),
setPosition: vi.fn(),
onCommit: vi.fn(),
});
expect(element.style.touchAction).toBe('none');
expect(element.style.userSelect).toBe('none');
cleanup();
expect(element.style.touchAction).toBe('pan-x');
expect(element.style.userSelect).toBe('text');
});
});