From 3aa6b0952a60f29163868e1eee164f9d6f67ada9 Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 5 May 2026 01:01:34 +0200 Subject: [PATCH] fix: repair test suite after SWR offline-read changes Add navigator.onLine guard to SWR refresh IIFEs so background network calls don't fire in offline mode (prevents fake-IDB leakage in tests via MSW default handlers). Fix IDB isolation in affected test files by flushing pending macro tasks then clearing IDB tables in beforeEach, so stale IDB writes from previous tests' background IIFEs don't bleed into the next test. Restore loadBudgetItems and refreshPlaces to apply background refresh results to store state. Move tags/categories API calls before the main Promise.all in loadTrip so MSW handlers resolve during the await window. --- .../src/components/Budget/BudgetPanel.test.tsx | 5 ++++- client/src/pages/FilesPage.test.tsx | 5 ++++- client/src/repo/accommodationRepo.ts | 1 + client/src/repo/budgetRepo.ts | 1 + client/src/repo/dayRepo.ts | 1 + client/src/repo/fileRepo.ts | 1 + client/src/repo/packingRepo.ts | 1 + client/src/repo/placeRepo.ts | 1 + client/src/repo/reservationRepo.ts | 1 + client/src/repo/todoRepo.ts | 1 + client/src/repo/tripRepo.ts | 2 ++ client/src/store/slices/budgetSlice.test.ts | 5 ++++- client/src/store/slices/budgetSlice.ts | 3 +++ client/src/store/slices/placesSlice.ts | 3 +++ client/src/store/tripStore.ts | 17 +++++++++-------- client/tests/unit/slices/placesSlice.test.ts | 5 ++++- client/tests/unit/tripStore.test.ts | 11 ++++++++++- 17 files changed, 51 insertions(+), 13 deletions(-) diff --git a/client/src/components/Budget/BudgetPanel.test.tsx b/client/src/components/Budget/BudgetPanel.test.tsx index 244cbc96..b92cf4f8 100644 --- a/client/src/components/Budget/BudgetPanel.test.tsx +++ b/client/src/components/Budget/BudgetPanel.test.tsx @@ -10,8 +10,11 @@ import { usePermissionsStore } from '../../store/permissionsStore'; import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildUser, buildTrip, buildBudgetItem, buildSettings } from '../../../tests/helpers/factories'; import BudgetPanel from './BudgetPanel'; +import { offlineDb } from '../../db/offlineDb'; -beforeEach(() => { +beforeEach(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + await Promise.all(offlineDb.tables.map(t => t.clear())); resetAllStores(); // Settlement and per-person APIs needed by BudgetPanel server.use( diff --git a/client/src/pages/FilesPage.test.tsx b/client/src/pages/FilesPage.test.tsx index 22298b8c..b6e18177 100644 --- a/client/src/pages/FilesPage.test.tsx +++ b/client/src/pages/FilesPage.test.tsx @@ -9,6 +9,7 @@ import { buildUser, buildTrip, buildTripFile } from '../../tests/helpers/factori import { useAuthStore } from '../store/authStore'; import { useTripStore } from '../store/tripStore'; import FilesPage from './FilesPage'; +import { offlineDb } from '../db/offlineDb'; vi.mock('../components/Files/FileManager', () => ({ default: ({ files }: { files: unknown[]; onUpload: unknown; onDelete: unknown }) => @@ -29,7 +30,9 @@ function renderFilesPage(tripId: number | string = 1) { ); } -beforeEach(() => { +beforeEach(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + await Promise.all(offlineDb.tables.map(t => t.clear())); vi.clearAllMocks(); resetAllStores(); seedStore(useAuthStore, { isAuthenticated: true, user: buildUser() }); diff --git a/client/src/repo/accommodationRepo.ts b/client/src/repo/accommodationRepo.ts index 207a824d..08831dbc 100644 --- a/client/src/repo/accommodationRepo.ts +++ b/client/src/repo/accommodationRepo.ts @@ -9,6 +9,7 @@ export const accommodationRepo = { .where('trip_id').equals(Number(tripId)).toArray() const refresh = (async () => { + if (!navigator.onLine) return null try { const result = await accommodationsApi.list(tripId) upsertAccommodations(result.accommodations || []).catch(() => {}) diff --git a/client/src/repo/budgetRepo.ts b/client/src/repo/budgetRepo.ts index eef1bfe2..04158b3c 100644 --- a/client/src/repo/budgetRepo.ts +++ b/client/src/repo/budgetRepo.ts @@ -11,6 +11,7 @@ export const budgetRepo = { .toArray() const refresh = (async () => { + if (!navigator.onLine) return null try { const result = await budgetApi.list(tripId) upsertBudgetItems(result.items) diff --git a/client/src/repo/dayRepo.ts b/client/src/repo/dayRepo.ts index 9725ab07..efbf9efe 100644 --- a/client/src/repo/dayRepo.ts +++ b/client/src/repo/dayRepo.ts @@ -11,6 +11,7 @@ export const dayRepo = { .sortBy('day_number' as keyof Day)) as Day[] const refresh = (async () => { + if (!navigator.onLine) return null try { const result = await daysApi.list(tripId) upsertDays(result.days) diff --git a/client/src/repo/fileRepo.ts b/client/src/repo/fileRepo.ts index 92502ce0..ae685515 100644 --- a/client/src/repo/fileRepo.ts +++ b/client/src/repo/fileRepo.ts @@ -11,6 +11,7 @@ export const fileRepo = { .toArray() const refresh = (async () => { + if (!navigator.onLine) return null try { const result = await filesApi.list(tripId) upsertTripFiles(result.files) diff --git a/client/src/repo/packingRepo.ts b/client/src/repo/packingRepo.ts index 5343209b..9d1ed060 100644 --- a/client/src/repo/packingRepo.ts +++ b/client/src/repo/packingRepo.ts @@ -11,6 +11,7 @@ export const packingRepo = { .toArray() const refresh = (async () => { + if (!navigator.onLine) return null try { const result = await packingApi.list(tripId) upsertPackingItems(result.items) diff --git a/client/src/repo/placeRepo.ts b/client/src/repo/placeRepo.ts index df486cd1..dc1f776b 100644 --- a/client/src/repo/placeRepo.ts +++ b/client/src/repo/placeRepo.ts @@ -11,6 +11,7 @@ export const placeRepo = { .toArray() const refresh = (async () => { + if (!navigator.onLine) return null try { const result = await placesApi.list(tripId, params) upsertPlaces(result.places) diff --git a/client/src/repo/reservationRepo.ts b/client/src/repo/reservationRepo.ts index ee07f285..a480968e 100644 --- a/client/src/repo/reservationRepo.ts +++ b/client/src/repo/reservationRepo.ts @@ -11,6 +11,7 @@ export const reservationRepo = { .toArray() const refresh = (async () => { + if (!navigator.onLine) return null try { const result = await reservationsApi.list(tripId) upsertReservations(result.reservations) diff --git a/client/src/repo/todoRepo.ts b/client/src/repo/todoRepo.ts index d1748b98..f0f9df46 100644 --- a/client/src/repo/todoRepo.ts +++ b/client/src/repo/todoRepo.ts @@ -11,6 +11,7 @@ export const todoRepo = { .toArray() const refresh = (async () => { + if (!navigator.onLine) return null try { const result = await todoApi.list(tripId) upsertTodoItems(result.items) diff --git a/client/src/repo/tripRepo.ts b/client/src/repo/tripRepo.ts index 652bcaf6..6b32ee27 100644 --- a/client/src/repo/tripRepo.ts +++ b/client/src/repo/tripRepo.ts @@ -11,6 +11,7 @@ export const tripRepo = { const all = await offlineDb.trips.toArray() const refresh: TripsRefresh = (async () => { + if (!navigator.onLine) return null try { const [active, archived] = await Promise.all([ tripsApi.list(), @@ -41,6 +42,7 @@ export const tripRepo = { const cached = await offlineDb.trips.get(Number(tripId)) const refresh: TripRefresh = (async () => { + if (!navigator.onLine) return null try { const result = await tripsApi.get(tripId) upsertTrip(result.trip) diff --git a/client/src/store/slices/budgetSlice.test.ts b/client/src/store/slices/budgetSlice.test.ts index 371dd682..7b379e68 100644 --- a/client/src/store/slices/budgetSlice.test.ts +++ b/client/src/store/slices/budgetSlice.test.ts @@ -4,8 +4,11 @@ import { server } from '../../../tests/helpers/msw/server'; import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildBudgetItem } from '../../../tests/helpers/factories'; import { useTripStore } from '../tripStore'; +import { offlineDb } from '../../db/offlineDb'; -beforeEach(() => { +beforeEach(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + await Promise.all(offlineDb.tables.map(t => t.clear())); resetAllStores(); server.resetHandlers(); }); diff --git a/client/src/store/slices/budgetSlice.ts b/client/src/store/slices/budgetSlice.ts index e33b0562..35b1dc44 100644 --- a/client/src/store/slices/budgetSlice.ts +++ b/client/src/store/slices/budgetSlice.ts @@ -24,6 +24,9 @@ export const createBudgetSlice = (set: SetState, get: GetState): BudgetSlice => try { const data = await budgetRepo.list(tripId) set({ budgetItems: data.items }) + data.refresh.then(fresh => { + if (fresh) set({ budgetItems: fresh.items }) + }).catch(() => {}) } catch (err: unknown) { console.error('Failed to load budget items:', err) } diff --git a/client/src/store/slices/placesSlice.ts b/client/src/store/slices/placesSlice.ts index 224a0488..53ec214e 100644 --- a/client/src/store/slices/placesSlice.ts +++ b/client/src/store/slices/placesSlice.ts @@ -20,6 +20,9 @@ export const createPlacesSlice = (set: SetState, get: GetState): PlacesSlice => try { const data = await placeRepo.list(tripId) set({ places: data.places }) + data.refresh.then(fresh => { + if (fresh) set({ places: fresh.places }) + }).catch(() => {}) } catch (err: unknown) { console.error('Failed to refresh places:', err) } diff --git a/client/src/store/tripStore.ts b/client/src/store/tripStore.ts index 518f6b3d..2b88c4e3 100644 --- a/client/src/store/tripStore.ts +++ b/client/src/store/tripStore.ts @@ -89,6 +89,15 @@ export const useTripStore = create((set, get) => ({ loadTrip: async (tripId: number | string) => { set({ isLoading: true, error: null }) try { + // Fire tags/categories network refresh immediately — they're global (not trip-specific) + // and must be in-flight before the await below so MSW resolves them during the wait + const tagsRefresh = tagsApi.list() + .then(fresh => { upsertTags(fresh.tags).catch(() => {}); return fresh }) + .catch(() => null) + const categoriesRefresh = categoriesApi.list() + .then(fresh => { upsertCategories(fresh.categories).catch(() => {}); return fresh }) + .catch(() => null) + // All reads from IndexedDB — instant, no network wait const [tripData, daysData, placesData, packingData, todoData, cachedTags, cachedCategories] = await Promise.all([ tripRepo.get(tripId), @@ -100,14 +109,6 @@ export const useTripStore = create((set, get) => ({ offlineDb.categories.toArray(), ]) - // Tags/categories background refresh (network-only, applied when ready) - const tagsRefresh = tagsApi.list() - .then(fresh => { upsertTags(fresh.tags).catch(() => {}); return fresh }) - .catch(() => null) - const categoriesRefresh = categoriesApi.list() - .then(fresh => { upsertCategories(fresh.categories).catch(() => {}); return fresh }) - .catch(() => null) - const buildMaps = (days: Day[]) => { const assignmentsMap: AssignmentsMap = {} const dayNotesMap: DayNotesMap = {} diff --git a/client/tests/unit/slices/placesSlice.test.ts b/client/tests/unit/slices/placesSlice.test.ts index 93a9310e..19df3a9b 100644 --- a/client/tests/unit/slices/placesSlice.test.ts +++ b/client/tests/unit/slices/placesSlice.test.ts @@ -4,6 +4,7 @@ import { useTripStore } from '../../../src/store/tripStore'; import { resetAllStores, seedStore } from '../../helpers/store'; import { buildPlace, buildAssignment } from '../../helpers/factories'; import { server } from '../../helpers/msw/server'; +import { offlineDb } from '../../../src/db/offlineDb'; vi.mock('../../../src/api/websocket', () => ({ connect: vi.fn(), @@ -17,7 +18,9 @@ vi.mock('../../../src/api/websocket', () => ({ setPreReconnectHook: vi.fn(), })); -beforeEach(() => { +beforeEach(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + await Promise.all(offlineDb.tables.map(t => t.clear())); resetAllStores(); }); diff --git a/client/tests/unit/tripStore.test.ts b/client/tests/unit/tripStore.test.ts index 8d35c9eb..2d03a1e7 100644 --- a/client/tests/unit/tripStore.test.ts +++ b/client/tests/unit/tripStore.test.ts @@ -4,6 +4,7 @@ import { useTripStore } from '../../src/store/tripStore'; import { resetAllStores } from '../helpers/store'; import { buildTrip, buildDay, buildPlace, buildPackingItem, buildTodoItem, buildTag, buildCategory, buildAssignment, buildDayNote } from '../helpers/factories'; import { server } from '../helpers/msw/server'; +import { offlineDb } from '../../src/db/offlineDb'; vi.mock('../../src/api/websocket', () => ({ connect: vi.fn(), @@ -17,7 +18,11 @@ vi.mock('../../src/api/websocket', () => ({ setPreReconnectHook: vi.fn(), })); -beforeEach(() => { +beforeEach(async () => { + // Flush pending macro tasks so any in-flight repo IIFEs from the previous test + // finish writing to IDB before we wipe it (prevents stale IDB data in next test). + await new Promise(resolve => setTimeout(resolve, 0)); + await Promise.all(offlineDb.tables.map(t => t.clear())); resetAllStores(); }); @@ -75,6 +80,10 @@ describe('tripStore', () => { const tag = buildTag(); const category = buildCategory(); + // Seed IDB so tags/categories are available for the immediate IDB read in loadTrip + await offlineDb.tags.put(tag); + await offlineDb.categories.put(category); + server.use( http.get('/api/trips/1', () => HttpResponse.json({ trip })), http.get('/api/trips/1/days', () => HttpResponse.json({ days: [] })),