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.
This commit is contained in:
jubnl
2026-05-05 01:01:34 +02:00
parent a6a0521261
commit 3aa6b0952a
17 changed files with 51 additions and 13 deletions
@@ -10,8 +10,11 @@ import { usePermissionsStore } from '../../store/permissionsStore';
import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { buildUser, buildTrip, buildBudgetItem, buildSettings } from '../../../tests/helpers/factories'; import { buildUser, buildTrip, buildBudgetItem, buildSettings } from '../../../tests/helpers/factories';
import BudgetPanel from './BudgetPanel'; import BudgetPanel from './BudgetPanel';
import { offlineDb } from '../../db/offlineDb';
beforeEach(() => { beforeEach(async () => {
await new Promise<void>(resolve => setTimeout(resolve, 0));
await Promise.all(offlineDb.tables.map(t => t.clear()));
resetAllStores(); resetAllStores();
// Settlement and per-person APIs needed by BudgetPanel // Settlement and per-person APIs needed by BudgetPanel
server.use( server.use(
+4 -1
View File
@@ -9,6 +9,7 @@ import { buildUser, buildTrip, buildTripFile } from '../../tests/helpers/factori
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useTripStore } from '../store/tripStore'; import { useTripStore } from '../store/tripStore';
import FilesPage from './FilesPage'; import FilesPage from './FilesPage';
import { offlineDb } from '../db/offlineDb';
vi.mock('../components/Files/FileManager', () => ({ vi.mock('../components/Files/FileManager', () => ({
default: ({ files }: { files: unknown[]; onUpload: unknown; onDelete: unknown }) => default: ({ files }: { files: unknown[]; onUpload: unknown; onDelete: unknown }) =>
@@ -29,7 +30,9 @@ function renderFilesPage(tripId: number | string = 1) {
); );
} }
beforeEach(() => { beforeEach(async () => {
await new Promise<void>(resolve => setTimeout(resolve, 0));
await Promise.all(offlineDb.tables.map(t => t.clear()));
vi.clearAllMocks(); vi.clearAllMocks();
resetAllStores(); resetAllStores();
seedStore(useAuthStore, { isAuthenticated: true, user: buildUser() }); seedStore(useAuthStore, { isAuthenticated: true, user: buildUser() });
+1
View File
@@ -9,6 +9,7 @@ export const accommodationRepo = {
.where('trip_id').equals(Number(tripId)).toArray() .where('trip_id').equals(Number(tripId)).toArray()
const refresh = (async () => { const refresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await accommodationsApi.list(tripId) const result = await accommodationsApi.list(tripId)
upsertAccommodations(result.accommodations || []).catch(() => {}) upsertAccommodations(result.accommodations || []).catch(() => {})
+1
View File
@@ -11,6 +11,7 @@ export const budgetRepo = {
.toArray() .toArray()
const refresh = (async () => { const refresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await budgetApi.list(tripId) const result = await budgetApi.list(tripId)
upsertBudgetItems(result.items) upsertBudgetItems(result.items)
+1
View File
@@ -11,6 +11,7 @@ export const dayRepo = {
.sortBy('day_number' as keyof Day)) as Day[] .sortBy('day_number' as keyof Day)) as Day[]
const refresh = (async () => { const refresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await daysApi.list(tripId) const result = await daysApi.list(tripId)
upsertDays(result.days) upsertDays(result.days)
+1
View File
@@ -11,6 +11,7 @@ export const fileRepo = {
.toArray() .toArray()
const refresh = (async () => { const refresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await filesApi.list(tripId) const result = await filesApi.list(tripId)
upsertTripFiles(result.files) upsertTripFiles(result.files)
+1
View File
@@ -11,6 +11,7 @@ export const packingRepo = {
.toArray() .toArray()
const refresh = (async () => { const refresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await packingApi.list(tripId) const result = await packingApi.list(tripId)
upsertPackingItems(result.items) upsertPackingItems(result.items)
+1
View File
@@ -11,6 +11,7 @@ export const placeRepo = {
.toArray() .toArray()
const refresh = (async () => { const refresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await placesApi.list(tripId, params) const result = await placesApi.list(tripId, params)
upsertPlaces(result.places) upsertPlaces(result.places)
+1
View File
@@ -11,6 +11,7 @@ export const reservationRepo = {
.toArray() .toArray()
const refresh = (async () => { const refresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await reservationsApi.list(tripId) const result = await reservationsApi.list(tripId)
upsertReservations(result.reservations) upsertReservations(result.reservations)
+1
View File
@@ -11,6 +11,7 @@ export const todoRepo = {
.toArray() .toArray()
const refresh = (async () => { const refresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await todoApi.list(tripId) const result = await todoApi.list(tripId)
upsertTodoItems(result.items) upsertTodoItems(result.items)
+2
View File
@@ -11,6 +11,7 @@ export const tripRepo = {
const all = await offlineDb.trips.toArray() const all = await offlineDb.trips.toArray()
const refresh: TripsRefresh = (async () => { const refresh: TripsRefresh = (async () => {
if (!navigator.onLine) return null
try { try {
const [active, archived] = await Promise.all([ const [active, archived] = await Promise.all([
tripsApi.list(), tripsApi.list(),
@@ -41,6 +42,7 @@ export const tripRepo = {
const cached = await offlineDb.trips.get(Number(tripId)) const cached = await offlineDb.trips.get(Number(tripId))
const refresh: TripRefresh = (async () => { const refresh: TripRefresh = (async () => {
if (!navigator.onLine) return null
try { try {
const result = await tripsApi.get(tripId) const result = await tripsApi.get(tripId)
upsertTrip(result.trip) upsertTrip(result.trip)
+4 -1
View File
@@ -4,8 +4,11 @@ import { server } from '../../../tests/helpers/msw/server';
import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { buildBudgetItem } from '../../../tests/helpers/factories'; import { buildBudgetItem } from '../../../tests/helpers/factories';
import { useTripStore } from '../tripStore'; import { useTripStore } from '../tripStore';
import { offlineDb } from '../../db/offlineDb';
beforeEach(() => { beforeEach(async () => {
await new Promise<void>(resolve => setTimeout(resolve, 0));
await Promise.all(offlineDb.tables.map(t => t.clear()));
resetAllStores(); resetAllStores();
server.resetHandlers(); server.resetHandlers();
}); });
+3
View File
@@ -24,6 +24,9 @@ export const createBudgetSlice = (set: SetState, get: GetState): BudgetSlice =>
try { try {
const data = await budgetRepo.list(tripId) const data = await budgetRepo.list(tripId)
set({ budgetItems: data.items }) set({ budgetItems: data.items })
data.refresh.then(fresh => {
if (fresh) set({ budgetItems: fresh.items })
}).catch(() => {})
} catch (err: unknown) { } catch (err: unknown) {
console.error('Failed to load budget items:', err) console.error('Failed to load budget items:', err)
} }
+3
View File
@@ -20,6 +20,9 @@ export const createPlacesSlice = (set: SetState, get: GetState): PlacesSlice =>
try { try {
const data = await placeRepo.list(tripId) const data = await placeRepo.list(tripId)
set({ places: data.places }) set({ places: data.places })
data.refresh.then(fresh => {
if (fresh) set({ places: fresh.places })
}).catch(() => {})
} catch (err: unknown) { } catch (err: unknown) {
console.error('Failed to refresh places:', err) console.error('Failed to refresh places:', err)
} }
+9 -8
View File
@@ -89,6 +89,15 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
loadTrip: async (tripId: number | string) => { loadTrip: async (tripId: number | string) => {
set({ isLoading: true, error: null }) set({ isLoading: true, error: null })
try { 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 // All reads from IndexedDB — instant, no network wait
const [tripData, daysData, placesData, packingData, todoData, cachedTags, cachedCategories] = await Promise.all([ const [tripData, daysData, placesData, packingData, todoData, cachedTags, cachedCategories] = await Promise.all([
tripRepo.get(tripId), tripRepo.get(tripId),
@@ -100,14 +109,6 @@ export const useTripStore = create<TripStoreState>((set, get) => ({
offlineDb.categories.toArray(), 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 buildMaps = (days: Day[]) => {
const assignmentsMap: AssignmentsMap = {} const assignmentsMap: AssignmentsMap = {}
const dayNotesMap: DayNotesMap = {} const dayNotesMap: DayNotesMap = {}
+4 -1
View File
@@ -4,6 +4,7 @@ import { useTripStore } from '../../../src/store/tripStore';
import { resetAllStores, seedStore } from '../../helpers/store'; import { resetAllStores, seedStore } from '../../helpers/store';
import { buildPlace, buildAssignment } from '../../helpers/factories'; import { buildPlace, buildAssignment } from '../../helpers/factories';
import { server } from '../../helpers/msw/server'; import { server } from '../../helpers/msw/server';
import { offlineDb } from '../../../src/db/offlineDb';
vi.mock('../../../src/api/websocket', () => ({ vi.mock('../../../src/api/websocket', () => ({
connect: vi.fn(), connect: vi.fn(),
@@ -17,7 +18,9 @@ vi.mock('../../../src/api/websocket', () => ({
setPreReconnectHook: vi.fn(), setPreReconnectHook: vi.fn(),
})); }));
beforeEach(() => { beforeEach(async () => {
await new Promise<void>(resolve => setTimeout(resolve, 0));
await Promise.all(offlineDb.tables.map(t => t.clear()));
resetAllStores(); resetAllStores();
}); });
+10 -1
View File
@@ -4,6 +4,7 @@ import { useTripStore } from '../../src/store/tripStore';
import { resetAllStores } from '../helpers/store'; import { resetAllStores } from '../helpers/store';
import { buildTrip, buildDay, buildPlace, buildPackingItem, buildTodoItem, buildTag, buildCategory, buildAssignment, buildDayNote } from '../helpers/factories'; import { buildTrip, buildDay, buildPlace, buildPackingItem, buildTodoItem, buildTag, buildCategory, buildAssignment, buildDayNote } from '../helpers/factories';
import { server } from '../helpers/msw/server'; import { server } from '../helpers/msw/server';
import { offlineDb } from '../../src/db/offlineDb';
vi.mock('../../src/api/websocket', () => ({ vi.mock('../../src/api/websocket', () => ({
connect: vi.fn(), connect: vi.fn(),
@@ -17,7 +18,11 @@ vi.mock('../../src/api/websocket', () => ({
setPreReconnectHook: vi.fn(), 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<void>(resolve => setTimeout(resolve, 0));
await Promise.all(offlineDb.tables.map(t => t.clear()));
resetAllStores(); resetAllStores();
}); });
@@ -75,6 +80,10 @@ describe('tripStore', () => {
const tag = buildTag(); const tag = buildTag();
const category = buildCategory(); 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( server.use(
http.get('/api/trips/1', () => HttpResponse.json({ trip })), http.get('/api/trips/1', () => HttpResponse.json({ trip })),
http.get('/api/trips/1/days', () => HttpResponse.json({ days: [] })), http.get('/api/trips/1/days', () => HttpResponse.json({ days: [] })),