Files
TREK/client/tests/unit/remoteEventHandler/places.test.ts
T
Maurice 47a4276e84 fix(client): make the offline core survive a logout and stop clobbering itself
logout() unregistered every sync trigger, and the only place that registers
them is App's mount effect - which never runs again, because logout navigates
through the router rather than reloading. Log out, log back in, go offline,
edit something: it queues and nothing ever flushes it. No online listener, no
visibilitychange, no 30s heartbeat, no WS reconnect hook. Register on auth
success instead; registerSyncTriggers is already idempotent.

The background sync loops only checked the auth gate on entry. Logout swaps
the Dexie handle to the shared anonymous database, so a trip loop or a blob
download still in flight afterwards wrote the logged-out account's itinerary
and documents into it - the database that survives logout and greets the next
person on a shared device. Check the gate per iteration.

And the remote place:updated applier replaced the whole embedded place, while
the local reducer right next to it preserves the assignment's own times. Same
event, two answers, and the remote one wipes a scheduled 09:00-10:30 off the
itinerary when someone else renames the place. One merge helper now, used by
both, so they cannot drift again. The test that should have caught it seeded
null times and asserted only the name.

Also: moving a day note deleted before it created, and restored the note only
locally when the create failed; t() interpolation was corrupted by a '$' in a
parameter value; a 429 upload was treated as permanently failed; and the
geolocation watch was started inside a state updater.
2026-08-23 11:59:12 +02:00

127 lines
5.1 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { useTripStore } from '../../../src/store/tripStore';
import { resetAllStores } from '../../helpers/store';
import { buildPlace, buildAssignment } from '../../helpers/factories';
beforeEach(() => {
resetAllStores();
});
describe('remoteEventHandler > places', () => {
const seedData = () => {
const place = buildPlace({ id: 1, name: 'Original' });
const assignment = buildAssignment({ id: 100, place, day_id: 10 });
useTripStore.setState({
places: [place],
assignments: { '10': [assignment] },
});
};
it('FE-WSEVT-PLACE-001: place:created prepends new place to places array', () => {
seedData();
const newPlace = buildPlace({ id: 99, name: 'New Place' });
useTripStore.getState().handleRemoteEvent({ type: 'place:created', place: newPlace });
const { places } = useTripStore.getState();
expect(places[0].id).toBe(99);
expect(places).toHaveLength(2);
});
it('FE-WSEVT-PLACE-002: place:created is idempotent — no duplicate if same ID', () => {
seedData();
const duplicate = buildPlace({ id: 1, name: 'Duplicate' });
useTripStore.getState().handleRemoteEvent({ type: 'place:created', place: duplicate });
const { places } = useTripStore.getState();
expect(places).toHaveLength(1);
expect(places[0].name).toBe('Original');
});
it('FE-WSEVT-PLACE-003: place:updated updates place in places array', () => {
seedData();
const updated = buildPlace({ id: 1, name: 'Updated Name' });
useTripStore.getState().handleRemoteEvent({ type: 'place:updated', place: updated });
const { places } = useTripStore.getState();
expect(places[0].name).toBe('Updated Name');
});
it('FE-WSEVT-PLACE-004: place:updated cascades into assignments nested place', () => {
seedData();
const updated = buildPlace({ id: 1, name: 'Cascaded Update' });
useTripStore.getState().handleRemoteEvent({ type: 'place:updated', place: updated });
const { assignments } = useTripStore.getState();
expect(assignments['10'][0].place?.name).toBe('Cascaded Update');
});
it('FE-WSEVT-PLACE-004b: place:updated keeps the assignment its own times', () => {
// The day view renders COALESCE(assignment_time, place_time), so the copy
// embedded in an assignment carries the per-stop schedule. A broadcast about
// the pool row must not overwrite it (the local reducer merges the same way).
const place = buildPlace({ id: 1, name: 'Original', place_time: '08:00', end_time: '09:00' });
const assignment = buildAssignment({
id: 100,
day_id: 10,
place: { ...place, place_time: '14:00', end_time: '15:30' },
});
useTripStore.setState({ places: [place], assignments: { '10': [assignment] } });
useTripStore.getState().handleRemoteEvent({
type: 'place:updated',
place: buildPlace({ id: 1, name: 'Renamed', place_time: '08:00', end_time: '09:00' }),
});
const embedded = useTripStore.getState().assignments['10'][0].place!;
expect(embedded.name).toBe('Renamed');
expect(embedded.place_time).toBe('14:00');
expect(embedded.end_time).toBe('15:30');
});
it('FE-WSEVT-PLACE-005: place:deleted removes place from places array', () => {
seedData();
useTripStore.getState().handleRemoteEvent({ type: 'place:deleted', placeId: 1 });
const { places } = useTripStore.getState();
expect(places).toHaveLength(0);
});
it('FE-WSEVT-PLACE-006: place:deleted cascades — assignments referencing that place are removed', () => {
seedData();
useTripStore.getState().handleRemoteEvent({ type: 'place:deleted', placeId: 1 });
const { assignments } = useTripStore.getState();
expect(assignments['10']).toHaveLength(0);
});
it('FE-WSEVT-PLACE-007: place:updated leaves other places and their assignments untouched', () => {
const one = buildPlace({ id: 1, name: 'One' });
const two = buildPlace({ id: 2, name: 'Two' });
useTripStore.setState({
places: [one, two],
assignments: {
'10': [
buildAssignment({ id: 100, place: one, day_id: 10 }),
buildAssignment({ id: 200, place: two, day_id: 10 }),
],
},
});
useTripStore.getState().handleRemoteEvent({ type: 'place:updated', place: buildPlace({ id: 2, name: 'Two renamed' }) });
const { places, assignments } = useTripStore.getState();
expect(places.map(p => p.name)).toEqual(['One', 'Two renamed']);
expect(assignments['10'].map(a => a.place?.name)).toEqual(['One', 'Two renamed']);
});
it('FE-WSEVT-PLACE-008: place:deleted keeps assignments of the other places', () => {
const one = buildPlace({ id: 1 });
const two = buildPlace({ id: 2 });
useTripStore.setState({
places: [one, two],
assignments: {
'10': [
buildAssignment({ id: 100, place: one, day_id: 10 }),
buildAssignment({ id: 200, place: two, day_id: 10 }),
],
},
});
useTripStore.getState().handleRemoteEvent({ type: 'place:deleted', placeId: 1 });
const { places, assignments } = useTripStore.getState();
expect(places.map(p => p.id)).toEqual([2]);
expect(assignments['10'].map(a => a.id)).toEqual([200]);
});
});