mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
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.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user