mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
bdd8c08dd4
The day plan wrote straight to the API, so offline a place dropped on a day snapped back with "Error assigning place", a renamed day or a new day note failed, and a moved stop, a leg's travel mode or who joins a visit were lost. Visits (put on a day, taken off, reordered, moved, travellers, leg mode), the day's name, notes text and travel mode, and the day notes (add, edit, delete, move) now go through assignmentRepo, dayRepo and dayNoteRepo and the mutation queue: the cached day changes at once, a visit or note made offline gets a temporary id that the edits after it wait for, and an online write held behind an older queued one joins the queue, since none of these carry a version token. A new order of a day that names a visit made offline is sent with its real id, or without it when its create failed. Desktop, the phone timeline and note sheet all write through the store, so both are covered. The replay reads each answer for what it names: the day answer keeps the cached notes and visits, the participants answer lands on its visit, and a visit the server refuses with 409 (a Tour already on that day) fails instead of waiting as a conflict no retry can solve. The trip's name, description, reminder and the archive flag wait in the queue too, from the planner, the phone and the dashboard. What only the server can do says so instead of failing: new dates, a day count, a currency or a cover, a new trip, a copy or a delete, members, guests, share and invite links, and a new order of the days, which moves the dates of the bookings. Refs #2601
192 lines
7.2 KiB
TypeScript
192 lines
7.2 KiB
TypeScript
/**
|
|
* mutationQueue replay of offline plan writes (#2601): a visit put on a day
|
|
* moves to its real id and the new order of the day follows it, a note does the
|
|
* same on its day, a day answer keeps the notes the cache holds, a trip answer
|
|
* keeps the counts of the list, and a visit the server refuses with 409 fails
|
|
* instead of waiting as a conflict.
|
|
*/
|
|
import 'fake-indexeddb/auto';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { clearAll, offlineDb } from '../../../src/db/offlineDb';
|
|
import { assignmentRepo } from '../../../src/repo/assignmentRepo';
|
|
import { dayNoteRepo } from '../../../src/repo/dayNoteRepo';
|
|
import { dayRepo } from '../../../src/repo/dayRepo';
|
|
import { tripRepo } from '../../../src/repo/tripRepo';
|
|
import { setAuthed } from '../../../src/sync/authGate';
|
|
import { mutationQueue } from '../../../src/sync/mutationQueue';
|
|
import { buildAssignment, buildDay, buildDayNote, buildPlace, buildTrip } from '../../helpers/factories';
|
|
import { server } from '../../helpers/msw/server';
|
|
|
|
function setOnline(v: boolean): void {
|
|
Object.defineProperty(navigator, 'onLine', { value: v, writable: true, configurable: true });
|
|
}
|
|
|
|
const place = buildPlace({ id: 300, trip_id: 4 });
|
|
|
|
beforeEach(async () => {
|
|
await clearAll();
|
|
mutationQueue._resetFlushing();
|
|
setAuthed(true);
|
|
setOnline(false);
|
|
await offlineDb.days.put(
|
|
buildDay({
|
|
id: 70,
|
|
trip_id: 4,
|
|
title: 'Old',
|
|
assignments: [
|
|
buildAssignment({ id: 500, day_id: 70, order_index: 0, place: buildPlace({ id: 301, trip_id: 4 }) }),
|
|
],
|
|
notes_items: [buildDayNote({ id: 900, day_id: 70 })],
|
|
})
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
setAuthed(false);
|
|
setOnline(true);
|
|
});
|
|
|
|
describe('mutationQueue.flush, plan', () => {
|
|
it('FE-SYNC-DAYS-001: a replayed visit takes its real id and the queued order names it', async () => {
|
|
const visit = await assignmentRepo.create(4, 70, place);
|
|
await assignmentRepo.reorder(4, 70, [visit.id, 500]);
|
|
await assignmentRepo.setLegMode(4, visit.id, 'driving');
|
|
const orders: unknown[] = [];
|
|
const legs: string[] = [];
|
|
server.use(
|
|
http.post('/api/trips/4/days/70/assignments', () =>
|
|
HttpResponse.json({ assignment: buildAssignment({ id: 520, day_id: 70, order_index: 1, place }) })
|
|
),
|
|
http.put('/api/trips/4/days/70/assignments/reorder', async ({ request }) => {
|
|
orders.push(await request.json());
|
|
return HttpResponse.json({ success: true });
|
|
}),
|
|
http.put('/api/trips/4/assignments/:id/transport', ({ params }) => {
|
|
legs.push(String(params.id));
|
|
return HttpResponse.json({
|
|
assignment: buildAssignment({ id: 520, day_id: 70, order_index: 0, leg_transport_mode: 'driving', place }),
|
|
});
|
|
})
|
|
);
|
|
setOnline(true);
|
|
|
|
await mutationQueue.flush();
|
|
|
|
expect(orders).toEqual([{ orderedIds: [520, 500] }]);
|
|
expect(legs).toEqual(['520']);
|
|
expect(await offlineDb.mutationQueue.count()).toBe(0);
|
|
const ids = (await offlineDb.days.get(70))?.assignments?.map((a) => a.id);
|
|
expect(ids).toContain(520);
|
|
expect(ids).not.toContain(visit.id);
|
|
});
|
|
|
|
it('FE-SYNC-DAYS-002: an order that names a visit whose create failed is sent without it', async () => {
|
|
const visit = await assignmentRepo.create(4, 70, place);
|
|
await assignmentRepo.reorder(4, 70, [visit.id, 500]);
|
|
const orders: unknown[] = [];
|
|
server.use(
|
|
http.post('/api/trips/4/days/70/assignments', () =>
|
|
HttpResponse.json({ error: 'Place not found' }, { status: 404 })
|
|
),
|
|
http.put('/api/trips/4/days/70/assignments/reorder', async ({ request }) => {
|
|
orders.push(await request.json());
|
|
return HttpResponse.json({ success: true });
|
|
})
|
|
);
|
|
setOnline(true);
|
|
|
|
await mutationQueue.flush();
|
|
|
|
expect(orders).toEqual([{ orderedIds: [500] }]);
|
|
expect((await offlineDb.days.get(70))?.assignments?.map((a) => a.id)).toEqual([500]);
|
|
});
|
|
|
|
it('FE-SYNC-DAYS-003: a visit refused with 409 fails, it does not wait as a conflict', async () => {
|
|
await assignmentRepo.create(4, 70, place);
|
|
server.use(
|
|
http.post('/api/trips/4/days/70/assignments', () =>
|
|
HttpResponse.json({ error: 'Tour is already assigned to this day' }, { status: 409 })
|
|
)
|
|
);
|
|
setOnline(true);
|
|
|
|
await mutationQueue.flush();
|
|
|
|
const [row] = await offlineDb.mutationQueue.toArray();
|
|
expect(row.status).toBe('failed');
|
|
expect((await offlineDb.days.get(70))?.assignments?.map((a) => a.id)).toEqual([500]);
|
|
});
|
|
|
|
it('FE-SYNC-DAYS-004: the participants answer lands on the visit it names', async () => {
|
|
await assignmentRepo.setParticipants(4, 500, [5]);
|
|
server.use(
|
|
http.put('/api/trips/4/assignments/500/participants', () =>
|
|
HttpResponse.json({ participants: [{ user_id: 5, username: 'tom', avatar: null }] })
|
|
)
|
|
);
|
|
setOnline(true);
|
|
|
|
await mutationQueue.flush();
|
|
|
|
expect((await offlineDb.days.get(70))?.assignments?.[0].participants).toEqual([
|
|
{ user_id: 5, username: 'tom', avatar: null },
|
|
]);
|
|
});
|
|
|
|
it('FE-SYNC-DAYS-005: a replayed note takes its real id and the queued edit follows it', async () => {
|
|
const { note } = await dayNoteRepo.create(4, 70, { text: 'Lunch' });
|
|
await dayNoteRepo.update(4, 70, note, { text: 'Brunch' });
|
|
const puts: string[] = [];
|
|
server.use(
|
|
http.post('/api/trips/4/days/70/notes', () =>
|
|
HttpResponse.json({ note: buildDayNote({ id: 901, day_id: 70, text: 'Lunch' }) })
|
|
),
|
|
http.put('/api/trips/4/days/70/notes/:id', ({ params }) => {
|
|
puts.push(String(params.id));
|
|
return HttpResponse.json({ note: buildDayNote({ id: 901, day_id: 70, text: 'Brunch' }) });
|
|
})
|
|
);
|
|
setOnline(true);
|
|
|
|
await mutationQueue.flush();
|
|
|
|
expect(puts).toEqual(['901']);
|
|
expect((await offlineDb.days.get(70))?.notes_items?.map((n) => [n.id, n.text])).toEqual([
|
|
[900, 'Test note'],
|
|
[901, 'Brunch'],
|
|
]);
|
|
});
|
|
|
|
it('FE-SYNC-DAYS-006: the day answer keeps the notes and visits the cache holds', async () => {
|
|
await dayRepo.update(4, 70, { title: 'Arrival' });
|
|
server.use(
|
|
http.put('/api/trips/4/days/70', () =>
|
|
HttpResponse.json({ day: { id: 70, trip_id: 4, title: 'Arrival', notes: null, assignments: [] } })
|
|
)
|
|
);
|
|
setOnline(true);
|
|
|
|
await mutationQueue.flush();
|
|
|
|
const day = await offlineDb.days.get(70);
|
|
expect(day?.title).toBe('Arrival');
|
|
expect(day?.notes_items?.map((n) => n.id)).toEqual([900]);
|
|
expect(day?.assignments?.map((a) => a.id)).toEqual([500]);
|
|
});
|
|
|
|
it('FE-SYNC-DAYS-007: a trip renamed offline syncs and keeps the counts of the cached row', async () => {
|
|
await offlineDb.trips.put({ ...buildTrip({ id: 4, title: 'Rome' }), place_count: 7 } as never);
|
|
await tripRepo.update(4, { title: 'Roma', start_date: buildTrip({ id: 4 }).start_date });
|
|
expect((await offlineDb.trips.get(4))?.title).toBe('Roma');
|
|
server.use(http.put('/api/trips/4', () => HttpResponse.json({ trip: { ...buildTrip({ id: 4 }), title: 'Roma' } })));
|
|
setOnline(true);
|
|
|
|
await mutationQueue.flush();
|
|
|
|
expect(await offlineDb.mutationQueue.count()).toBe(0);
|
|
expect(await offlineDb.trips.get(4)).toMatchObject({ title: 'Roma', place_count: 7 });
|
|
});
|
|
});
|