Files
TREK/client/tests/unit/remoteEventHandler/trip.test.ts
T
jubnl 285b9bcde0 fix(trips): keep accommodations on their dates when the trip range shifts (#1288)
The v3.1.3 fix re-anchored dated bookings after a trip date change but
explicitly excluded hotels: day_accommodations has no absolute date columns,
so stays remained glued to positionally re-dated day rows and shifted with
the range. updateTrip now snapshots day dates before generateDays and a new
resyncAccommodationDays re-anchors each stay (and its linked hotel
reservation, restamping its stale reservation_time) to the days holding its
pre-change dates; out-of-range stays stay glued so whole-trip moves still
shift together. Unlinked dated hotels resync like any booking, and the
date-change block is wrapped in a transaction.

Changing the start date now also asks how plans should follow via a new
date_shift_mode field ('keep_bookings' default / 'shift_all', which reuses
the reorder/insert restamp path to glue everything), exposed in the trip
edit modal (all 22 locales), the shared contract, and the MCP update_trip
tool. Clients no longer show stale state: the initiator reloads
reservations + accommodations after saving, collaborators refetch on a
date-changing trip:updated, and reconnect hydration nudges the planner's
accommodations too.
2026-07-18 11:07:45 +02:00

77 lines
4.0 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useTripStore } from '../../../src/store/tripStore';
import { resetAllStores } from '../../helpers/store';
import { buildTrip, buildPlace } from '../../helpers/factories';
beforeEach(() => {
resetAllStores();
vi.restoreAllMocks();
});
describe('remoteEventHandler > trip', () => {
it('FE-WSEVT-TRIP-001: trip:updated replaces trip in state', () => {
const originalTrip = buildTrip({ id: 1, title: 'Paris Trip' });
useTripStore.setState({ trip: originalTrip });
const updatedTrip = buildTrip({ id: 1, title: 'Paris & Lyon Trip' });
useTripStore.getState().handleRemoteEvent({ type: 'trip:updated', trip: updatedTrip });
const { trip } = useTripStore.getState();
expect(trip?.title).toBe('Paris & Lyon Trip');
});
it('FE-WSEVT-TRIP-002: trip:updated does not affect other state fields', () => {
const existingPlace = buildPlace({ id: 55, name: 'Eiffel Tower' });
useTripStore.setState({
trip: buildTrip({ id: 1, title: 'Original' }),
places: [existingPlace],
});
const updatedTrip = buildTrip({ id: 1, title: 'Updated' });
useTripStore.getState().handleRemoteEvent({ type: 'trip:updated', trip: updatedTrip });
const { places } = useTripStore.getState();
expect(places).toHaveLength(1);
expect(places[0].id).toBe(55);
});
// A remote date-range change re-anchors bookings/accommodations server-side, so the
// handler must pull authoritative days + reservations and nudge the planner (#1288).
describe('date-range change refresh (#1288)', () => {
const stubRefresh = () => {
const refreshDays = vi.fn().mockResolvedValue(undefined);
const loadReservations = vi.fn().mockResolvedValue(undefined);
useTripStore.setState({ refreshDays, loadReservations });
const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
return { refreshDays, loadReservations, dispatchSpy };
};
const firedAccommodationsRefresh = (spy: ReturnType<typeof vi.spyOn>) =>
spy.mock.calls.some(([e]) => (e as Event).type === 'accommodations:refresh');
it('FE-WSEVT-TRIP-003: trip:updated with a changed start_date refetches days + reservations and fires accommodations:refresh', () => {
useTripStore.setState({ trip: buildTrip({ id: 1, start_date: '2025-06-01', end_date: '2025-06-05' }) });
const { refreshDays, loadReservations, dispatchSpy } = stubRefresh();
const updatedTrip = buildTrip({ id: 1, start_date: '2025-05-31', end_date: '2025-06-05' });
useTripStore.getState().handleRemoteEvent({ type: 'trip:updated', trip: updatedTrip });
expect(refreshDays).toHaveBeenCalledWith(1);
expect(loadReservations).toHaveBeenCalledWith(1);
expect(firedAccommodationsRefresh(dispatchSpy)).toBe(true);
});
it('FE-WSEVT-TRIP-004: a title-only trip:updated does not trigger any refetch', () => {
useTripStore.setState({ trip: buildTrip({ id: 1, title: 'Old', start_date: '2025-06-01', end_date: '2025-06-05' }) });
const { refreshDays, loadReservations, dispatchSpy } = stubRefresh();
const updatedTrip = buildTrip({ id: 1, title: 'New', start_date: '2025-06-01', end_date: '2025-06-05' });
useTripStore.getState().handleRemoteEvent({ type: 'trip:updated', trip: updatedTrip });
expect(refreshDays).not.toHaveBeenCalled();
expect(loadReservations).not.toHaveBeenCalled();
expect(firedAccommodationsRefresh(dispatchSpy)).toBe(false);
});
it('FE-WSEVT-TRIP-005: a trip:updated for a different trip id does not trigger a refetch', () => {
useTripStore.setState({ trip: buildTrip({ id: 1, start_date: '2025-06-01', end_date: '2025-06-05' }) });
const { refreshDays, loadReservations } = stubRefresh();
const updatedTrip = buildTrip({ id: 2, start_date: '2025-07-01', end_date: '2025-07-05' });
useTripStore.getState().handleRemoteEvent({ type: 'trip:updated', trip: updatedTrip });
expect(refreshDays).not.toHaveBeenCalled();
expect(loadReservations).not.toHaveBeenCalled();
});
});
});