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: [] })),