// FE-STORE-BUDGET-001 to FE-STORE-BUDGET-011 import { http, HttpResponse } from 'msw'; import { server } from '../../../tests/helpers/msw/server'; import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildBudgetItem } from '../../../tests/helpers/factories'; import { useTripStore } from '../tripStore'; beforeEach(() => { resetAllStores(); server.resetHandlers(); }); describe('budgetSlice', () => { it('FE-STORE-BUDGET-001: loadBudgetItems populates store', async () => { const item = buildBudgetItem({ trip_id: 1 }); server.use( http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [item] }) ) ); await useTripStore.getState().loadBudgetItems(1); expect(useTripStore.getState().budgetItems).toHaveLength(1); expect(useTripStore.getState().budgetItems[0].id).toBe(item.id); }); it('FE-STORE-BUDGET-002: loadBudgetItems swallows errors silently', async () => { server.use( http.get('/api/trips/1/budget', () => HttpResponse.json({ error: 'server error' }, { status: 500 }) ) ); // Should NOT throw await expect(useTripStore.getState().loadBudgetItems(1)).resolves.toBeUndefined(); expect(useTripStore.getState().budgetItems).toEqual([]); }); it('FE-STORE-BUDGET-003: addBudgetItem appends to store and returns item', async () => { const newItem = buildBudgetItem({ name: 'Hotel', trip_id: 1 }); server.use( http.post('/api/trips/1/budget', () => HttpResponse.json({ item: newItem }) ) ); const result = await useTripStore.getState().addBudgetItem(1, { name: 'Hotel' }); expect(result.id).toBe(newItem.id); expect(useTripStore.getState().budgetItems).toContainEqual(newItem); }); it('FE-STORE-BUDGET-004: addBudgetItem throws on API error', async () => { server.use( http.post('/api/trips/1/budget', () => HttpResponse.json({ error: 'Validation failed' }, { status: 422 }) ) ); await expect(useTripStore.getState().addBudgetItem(1, {})).rejects.toThrow(); }); it('FE-STORE-BUDGET-005: updateBudgetItem replaces item in store', async () => { const existing = buildBudgetItem({ id: 10, trip_id: 1, name: 'Old' }); seedStore(useTripStore, { budgetItems: [existing] }); const updated = { ...existing, name: 'New' }; server.use( http.put('/api/trips/1/budget/10', () => HttpResponse.json({ item: updated }) ) ); await useTripStore.getState().updateBudgetItem(1, 10, { name: 'New' }); const items = useTripStore.getState().budgetItems; expect(items).toHaveLength(1); expect(items[0].name).toBe('New'); }); it('FE-STORE-BUDGET-006: updateBudgetItem calls loadReservations when reservation_id + total_price provided', async () => { const existing = buildBudgetItem({ id: 20, trip_id: 1 }); seedStore(useTripStore, { budgetItems: [existing] }); const loadReservations = vi.fn().mockResolvedValue(undefined); seedStore(useTripStore, { loadReservations }); const itemWithReservation = { ...existing, reservation_id: 99 }; server.use( http.put('/api/trips/1/budget/20', () => HttpResponse.json({ item: itemWithReservation }) ) ); await useTripStore.getState().updateBudgetItem(1, 20, { total_price: 50 }); expect(loadReservations).toHaveBeenCalledWith(1); }); it('FE-STORE-BUDGET-007: deleteBudgetItem optimistically removes and rolls back on error', async () => { const item = buildBudgetItem({ id: 5, trip_id: 1 }); seedStore(useTripStore, { budgetItems: [item] }); server.use( http.delete('/api/trips/1/budget/5', () => HttpResponse.json({ error: 'forbidden' }, { status: 403 }) ) ); // The item is removed immediately (optimistic), then restored on error const deletePromise = useTripStore.getState().deleteBudgetItem(1, 5); await expect(deletePromise).rejects.toThrow(); // After rollback, item is back expect(useTripStore.getState().budgetItems).toContainEqual(item); }); it('FE-STORE-BUDGET-008: setBudgetItemMembers updates members on matching item', async () => { const item = buildBudgetItem({ id: 7, trip_id: 1, members: [] }); seedStore(useTripStore, { budgetItems: [item] }); const members = [{ user_id: 1, paid: false }, { user_id: 2, paid: false }]; const updatedItem = { ...item, persons: 2, members }; server.use( http.put('/api/trips/1/budget/7/members', () => HttpResponse.json({ members, item: updatedItem }) ) ); await useTripStore.getState().setBudgetItemMembers(1, 7, [1, 2]); const stored = useTripStore.getState().budgetItems.find(i => i.id === 7); expect(stored?.members).toHaveLength(2); expect(stored?.persons).toBe(2); }); it('FE-STORE-BUDGET-009: toggleBudgetMemberPaid updates paid flag on matching member', async () => { const item = buildBudgetItem({ id: 8, trip_id: 1, members: [{ user_id: 3, paid: 0, username: 'carol' }], }); seedStore(useTripStore, { budgetItems: [item] }); server.use( http.put('/api/trips/1/budget/8/members/3/paid', () => HttpResponse.json({ success: true, paid: true }) ) ); await useTripStore.getState().toggleBudgetMemberPaid(1, 8, 3, true); const stored = useTripStore.getState().budgetItems.find(i => i.id === 8); expect(stored?.members?.[0]?.paid).toBe(true); }); it('FE-STORE-BUDGET-010: reorderBudgetItems reorders optimistically and reloads on error', async () => { const a = buildBudgetItem({ id: 1, trip_id: 1 }); const b = buildBudgetItem({ id: 2, trip_id: 1 }); seedStore(useTripStore, { budgetItems: [a, b] }); // Reorder succeeds server.use( http.put('/api/trips/1/budget/reorder/items', () => HttpResponse.json({ success: true }) ) ); await useTripStore.getState().reorderBudgetItems(1, [2, 1]); const items = useTripStore.getState().budgetItems; expect(items[0].id).toBe(2); expect(items[1].id).toBe(1); }); it('FE-STORE-BUDGET-011: reorderBudgetItems reloads list on API error', async () => { const a = buildBudgetItem({ id: 1, trip_id: 1 }); const b = buildBudgetItem({ id: 2, trip_id: 1 }); seedStore(useTripStore, { budgetItems: [a, b] }); const freshItem = buildBudgetItem({ id: 99, trip_id: 1 }); server.use( http.put('/api/trips/1/budget/reorder/items', () => HttpResponse.json({ error: 'error' }, { status: 500 }) ), http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [freshItem] }) ) ); await useTripStore.getState().reorderBudgetItems(1, [2, 1]); // After failure, fresh list from server expect(useTripStore.getState().budgetItems[0].id).toBe(freshItem.id); }); });