Files
TREK/client/tests/unit/sync/mutationQueue.files.test.ts
T
Maurice 92c48fba22 fix(files): file notes, links, stars and the trash changed offline wait in the queue and sync once the connection is back
Every file write went straight to the API, so offline a star snapped back
without a word and moving a file to the trash failed with "Failed to delete
file". Notes, the place or booking of a file, a further link, the star, the
trash move, restore, delete for good and emptying the trash now go through
fileRepo and the mutation queue like the other trip data: the local copy
changes at once, and an online write to a file that still has an older one
queued joins the queue behind it, since file writes carry no version token.
The desktop file manager, its assign dialog and the phone's files tab,
menu, link and trash sheets all write through it.

The single file answers carry no ids of further links, so a replayed edit,
star or restore keeps the ones the local copy holds, and the link answer
({ success, links }) writes no row.

What only works online says so instead of failing: an upload shows that it
needs a connection, a place saved with newly picked files is stopped with a
hint, and removing a further link (its row id is known only to the server)
asks to try again once online.

Refs #2601
2026-10-12 19:32:16 +02:00

101 lines
3.8 KiB
TypeScript

/**
* mutationQueue replay of offline file writes (#2601): the file answers carry no
* ids of further links, so a replayed edit or star keeps the ones the local copy
* holds, the link answer ({ success, links }) writes no row, and a queued trash
* move takes the file off the local list for good.
*/
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 { fileRepo } from '../../../src/repo/fileRepo';
import { setAuthed } from '../../../src/sync/authGate';
import { mutationQueue } from '../../../src/sync/mutationQueue';
import { buildTripFile } from '../../helpers/factories';
import { server } from '../../helpers/msw/server';
function setOnline(v: boolean): void {
Object.defineProperty(navigator, 'onLine', { value: v, writable: true, configurable: true });
}
beforeEach(async () => {
await clearAll();
mutationQueue._resetFlushing();
setAuthed(true);
setOnline(false);
});
afterEach(() => {
vi.restoreAllMocks();
setAuthed(false);
setOnline(true);
});
describe('mutationQueue.flush — files', () => {
it('FE-SYNC-FILES-001: a replayed note, star and link keep the further links and send in order', async () => {
await offlineDb.tripFiles.put(
buildTripFile({
id: 12,
trip_id: 4,
description: null,
starred: 0,
linked_place_ids: [7],
linked_reservation_ids: [],
})
);
await fileRepo.update(4, 12, { description: 'Gate B' });
await fileRepo.toggleStar(4, 12);
await fileRepo.addLink(4, 12, { reservation_id: 9 });
const sent: string[] = [];
// The single-file routes answer the stored row without the link ids.
const row = (fields: object) => ({ file: { ...buildTripFile({ id: 12, trip_id: 4 }), ...fields } });
server.use(
http.put('/api/trips/4/files/12', () => {
sent.push('PUT');
return HttpResponse.json(row({ description: 'Gate B', starred: 0 }));
}),
http.patch('/api/trips/4/files/12/star', () => {
sent.push('PATCH');
return HttpResponse.json(row({ description: 'Gate B', starred: 1 }));
}),
http.post('/api/trips/4/files/12/link', () => {
sent.push('POST');
return HttpResponse.json({ success: true, links: [{ id: 3, file_id: 12, reservation_id: 9 }] });
})
);
setOnline(true);
await mutationQueue.flush();
expect(sent).toEqual(['PUT', 'PATCH', 'POST']);
expect(await offlineDb.mutationQueue.count()).toBe(0);
expect(await offlineDb.tripFiles.get(12)).toMatchObject({
description: 'Gate B',
starred: 1,
linked_place_ids: [7],
linked_reservation_ids: [9],
});
// The link answer named no file, so no other row appeared.
expect(await offlineDb.tripFiles.count()).toBe(1);
});
it('FE-SYNC-FILES-002: a replayed trash move and a restore leave the local list as the server has it', async () => {
await offlineDb.tripFiles.put(buildTripFile({ id: 12, trip_id: 4 }));
await fileRepo.delete(4, 12);
await fileRepo.restore(4, 20, buildTripFile({ id: 20, trip_id: 4, deleted_at: '2026-10-01 10:00:00' }));
server.use(
http.delete('/api/trips/4/files/12', () => HttpResponse.json({ success: true })),
http.post('/api/trips/4/files/20/restore', () =>
HttpResponse.json({ file: buildTripFile({ id: 20, trip_id: 4, deleted_at: null, original_name: 'back.pdf' }) })
)
);
setOnline(true);
await mutationQueue.flush();
expect(await offlineDb.mutationQueue.count()).toBe(0);
expect(await offlineDb.tripFiles.get(12)).toBeUndefined();
expect(await offlineDb.tripFiles.get(20)).toMatchObject({ original_name: 'back.pdf', deleted_at: null });
});
});