Files
TREK/client/tests/unit/sync/mutationQueue.tours.test.ts
Maurice e0efa0314a feat(tours): tours read and write through the offline core
A tourRepo replaces the direct toursApi calls: reads fall back to a new
Dexie tours table, and a tour saved or edited offline is written there and
queued like a place, under one temporary id for the place and its facet.
The queue moves both to the real id on replay and drops both when the
server refuses the create. Trip sync caches each trip's tours while the
addon is on, so a tour no longer reads as a plain place offline.

The tour metrics move to shared so the offline copy shows the server's
numbers, and useTourPlaceIds keeps one Set per change as intended.
2026-10-07 21:30:45 +02:00

102 lines
3.9 KiB
TypeScript

/**
* mutationQueue replay of offline tour writes: a tour is a place plus its
* facet, so a create moves both from the temporary id to the real one, points
* queued edits at it, and a refused create takes both back out.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import 'fake-indexeddb/auto'
import { http, HttpResponse } from 'msw'
import type { TourCreateRequest, TourListItem } from '@trek/shared'
import { server } from '../../helpers/msw/server'
import { setAuthed } from '../../../src/sync/authGate'
import { mutationQueue } from '../../../src/sync/mutationQueue'
import { offlineDb, clearAll } from '../../../src/db/offlineDb'
import { tourRepo } from '../../../src/repo/tourRepo'
import { placeRepo } from '../../../src/repo/placeRepo'
function setOnline(v: boolean): void {
Object.defineProperty(navigator, 'onLine', { value: v, writable: true, configurable: true })
}
const body: TourCreateRequest = {
name: 'Lake loop',
tour_type: 'hike',
route_geometry: [[47, 11, 1000], [47.02, 11, 1050]],
waypoints: [
{ lat: 47, lng: 11, role: 'start', sequence: 0 },
{ lat: 47.02, lng: 11, role: 'end', sequence: 1 },
],
max_hiking_difficulty: 2,
duration_seconds: null,
}
function savedTour(placeId: number, name = 'Lake loop'): TourListItem {
return {
place_id: placeId, name, tour_type: 'hike', distance: 2.2, elevation_gain: 50, elevation_loss: 0, duration: null,
difficulty: null, wanderer_ref: null, match_confidence: 1, max_hiking_difficulty: 2, planned: false, caution: false,
has_waypoints: true,
}
}
beforeEach(async () => {
await clearAll()
mutationQueue._resetFlushing()
setAuthed(true)
setOnline(false)
})
afterEach(() => {
vi.restoreAllMocks()
setAuthed(false)
setOnline(true)
})
describe('mutationQueue.flush — tours', () => {
it('FE-SYNC-TOUR-001: a replayed create moves place and facet to the real id and aims the queued edit at it', async () => {
const { tour } = await tourRepo.create(3, body)
const tempId = tour.place_id
await tourRepo.update(3, tempId, { ...body, name: 'Renamed' })
const puts: string[] = []
server.use(
http.post('/api/trips/3/tours', () => HttpResponse.json({ tour: savedTour(501), waypoints: body.waypoints })),
http.put('/api/trips/3/tours/:id', ({ params }) => {
puts.push(String(params.id))
return HttpResponse.json({ tour: savedTour(501, 'Renamed'), waypoints: body.waypoints })
}),
)
setOnline(true)
await mutationQueue.flush()
expect(puts).toEqual(['501'])
expect(await offlineDb.mutationQueue.count()).toBe(0)
expect(await offlineDb.tours.get(tempId)).toBeUndefined()
expect(await offlineDb.places.get(tempId)).toBeUndefined()
expect(await offlineDb.tours.get(501)).toMatchObject({ trip_id: 3, name: 'Renamed', waypoints: body.waypoints })
// The server built its place from the same route; it stays on the map until the next list.
expect(await offlineDb.places.get(501)).toMatchObject({ trip_id: 3, name: 'Renamed' })
})
it('FE-SYNC-TOUR-002: a create the server refuses takes its place and facet back out', async () => {
const { tour } = await tourRepo.create(3, body)
server.use(http.post('/api/trips/3/tours', () => HttpResponse.json({ error: 'Tour type is not available' }, { status: 400 })))
setOnline(true)
await mutationQueue.flush()
expect(await offlineDb.tours.get(tour.place_id)).toBeUndefined()
expect(await offlineDb.places.get(tour.place_id)).toBeUndefined()
expect((await offlineDb.mutationQueue.toArray())[0]).toMatchObject({ status: 'failed' })
})
it('FE-SYNC-TOUR-003: deleting a tour offline drops its facet with the place', async () => {
await offlineDb.tours.put({ ...savedTour(40), trip_id: 3 })
await placeRepo.delete(3, 40)
await offlineDb.tours.put({ ...savedTour(41), trip_id: 3 })
await placeRepo.deleteMany(3, [41])
expect(await offlineDb.tours.count()).toBe(0)
})
})