mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
72a82b3c44
214 items came out of writing the tests. Real defects are corrected, dead code is removed rather than covered, and the tests that pinned the old behaviour move with them. Also drops two agent coverage scratch files that slipped into the coverage PR and adds the ignore rule that keeps them out.
203 lines
8.0 KiB
TypeScript
203 lines
8.0 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { useTripStore } from '../../../src/store/tripStore';
|
|
import { resetAllStores } from '../../helpers/store';
|
|
import { buildBudgetItem } from '../../helpers/factories';
|
|
import type { BudgetItemMember } from '../../../src/types';
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
});
|
|
|
|
describe('remoteEventHandler > budget', () => {
|
|
const member1: BudgetItemMember = { user_id: 5, paid: 0, username: 'eve' };
|
|
const member2: BudgetItemMember = { user_id: 6, paid: 1, username: 'frank' };
|
|
|
|
const seedData = () => {
|
|
useTripStore.setState({
|
|
budgetItems: [
|
|
buildBudgetItem({ id: 1, persons: 1, members: [{ ...member1 }] }),
|
|
buildBudgetItem({ id: 2, persons: 2, members: [{ ...member2 }] }),
|
|
],
|
|
});
|
|
};
|
|
|
|
it('FE-WSEVT-BUDGET-001: budget:created adds item to budgetItems', () => {
|
|
seedData();
|
|
const newItem = buildBudgetItem({ id: 99, name: 'Hotel' });
|
|
useTripStore.getState().handleRemoteEvent({ type: 'budget:created', item: newItem });
|
|
const { budgetItems } = useTripStore.getState();
|
|
expect(budgetItems).toHaveLength(3);
|
|
expect(budgetItems.find(i => i.id === 99)).toBeDefined();
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-002: budget:created is idempotent — no duplicate if same ID', () => {
|
|
seedData();
|
|
const duplicate = buildBudgetItem({ id: 1, name: 'Duplicate' });
|
|
useTripStore.getState().handleRemoteEvent({ type: 'budget:created', item: duplicate });
|
|
const { budgetItems } = useTripStore.getState();
|
|
expect(budgetItems).toHaveLength(2);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-003: budget:updated replaces item in array', () => {
|
|
seedData();
|
|
const updated = buildBudgetItem({ id: 1, name: 'Updated Hotel', total_price: 500 });
|
|
useTripStore.getState().handleRemoteEvent({ type: 'budget:updated', item: updated });
|
|
const { budgetItems } = useTripStore.getState();
|
|
const item = budgetItems.find(i => i.id === 1);
|
|
expect(item?.name).toBe('Updated Hotel');
|
|
expect(item?.total_price).toBe(500);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-004: budget:deleted removes item by ID', () => {
|
|
seedData();
|
|
useTripStore.getState().handleRemoteEvent({ type: 'budget:deleted', itemId: 1 });
|
|
const { budgetItems } = useTripStore.getState();
|
|
expect(budgetItems).toHaveLength(1);
|
|
expect(budgetItems.find(i => i.id === 1)).toBeUndefined();
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-005: budget:members-updated replaces entire members array and persons count', () => {
|
|
seedData();
|
|
const newMembers: BudgetItemMember[] = [{ user_id: 7, paid: 1, username: 'grace' }, { user_id: 8, paid: 0, username: 'heidi' }];
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'budget:members-updated',
|
|
itemId: 1,
|
|
members: newMembers,
|
|
persons: 3,
|
|
});
|
|
const { budgetItems } = useTripStore.getState();
|
|
const item = budgetItems.find(i => i.id === 1);
|
|
expect(item?.members).toEqual(newMembers);
|
|
expect(item?.persons).toBe(3);
|
|
// Other item should be unchanged
|
|
const item2 = budgetItems.find(i => i.id === 2);
|
|
expect(item2?.members).toEqual([{ ...member2 }]);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-006: budget:member-paid-updated toggles specific member paid status', () => {
|
|
seedData();
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'budget:member-paid-updated',
|
|
itemId: 1,
|
|
userId: 5,
|
|
paid: true,
|
|
});
|
|
const { budgetItems } = useTripStore.getState();
|
|
const item = budgetItems.find(i => i.id === 1);
|
|
const m = item?.members?.find(m => m.user_id === 5);
|
|
expect(m?.paid).toBe(true);
|
|
// Other item members unchanged (member2 keeps its seeded paid value)
|
|
const item2 = budgetItems.find(i => i.id === 2);
|
|
expect(item2?.members?.[0].paid).toBe(1);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-007: budget:members-updated for an unknown item leaves every item untouched', () => {
|
|
seedData();
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'budget:members-updated',
|
|
itemId: 4242,
|
|
members: [{ user_id: 9, paid: 0, username: 'ivan' }],
|
|
persons: 9,
|
|
});
|
|
const { budgetItems } = useTripStore.getState();
|
|
expect(budgetItems.map(i => i.persons)).toEqual([1, 2]);
|
|
expect(budgetItems[0].members).toEqual([{ ...member1 }]);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-008: budget:member-paid-updated only touches the addressed member', () => {
|
|
useTripStore.setState({
|
|
budgetItems: [
|
|
buildBudgetItem({ id: 1, members: [{ ...member1 }, { ...member2 }] }),
|
|
],
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'budget:member-paid-updated',
|
|
itemId: 1,
|
|
userId: 5,
|
|
paid: 1,
|
|
});
|
|
const members = useTripStore.getState().budgetItems[0].members ?? [];
|
|
expect(members.find(m => m.user_id === 5)?.paid).toBe(1);
|
|
expect(members.find(m => m.user_id === 6)?.paid).toBe(1); // seeded value, untouched
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-009: budget:member-paid-updated on an item without members does not throw', () => {
|
|
useTripStore.setState({
|
|
budgetItems: [buildBudgetItem({ id: 1, members: undefined })],
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'budget:member-paid-updated',
|
|
itemId: 1,
|
|
userId: 5,
|
|
paid: 1,
|
|
});
|
|
expect(useTripStore.getState().budgetItems[0].members).toEqual([]);
|
|
});
|
|
|
|
describe('budget:reordered', () => {
|
|
it('FE-WSEVT-BUDGET-010: orderedIds rewrites sort_order and keeps unlisted items at the end', () => {
|
|
useTripStore.setState({
|
|
budgetItems: [
|
|
buildBudgetItem({ id: 1, sort_order: 0 }),
|
|
buildBudgetItem({ id: 2, sort_order: 1 }),
|
|
buildBudgetItem({ id: 3, sort_order: 2 }),
|
|
],
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({ type: 'budget:reordered', orderedIds: [3, 1] });
|
|
const { budgetItems } = useTripStore.getState();
|
|
expect(budgetItems.map(i => i.id)).toEqual([3, 1, 2]);
|
|
// The whole array is reindexed, so re-sorting by sort_order reproduces the
|
|
// order that was just applied.
|
|
expect(budgetItems.map(i => i.sort_order)).toEqual([0, 1, 2]);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-011: an orderedIds entry for an unknown item is dropped', () => {
|
|
useTripStore.setState({
|
|
budgetItems: [buildBudgetItem({ id: 1 }), buildBudgetItem({ id: 2 })],
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({ type: 'budget:reordered', orderedIds: [2, 999, 1] });
|
|
expect(useTripStore.getState().budgetItems.map(i => i.id)).toEqual([2, 1]);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-012: orderedCategories groups items and appends unlisted categories', () => {
|
|
useTripStore.setState({
|
|
budgetItems: [
|
|
buildBudgetItem({ id: 1, category: 'Food' }),
|
|
buildBudgetItem({ id: 2, category: 'Transport' }),
|
|
buildBudgetItem({ id: 3, category: 'Food' }),
|
|
buildBudgetItem({ id: 4, category: 'Lodging' }),
|
|
],
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'budget:reordered',
|
|
orderedCategories: ['Transport', 'Food', 'Souvenirs'],
|
|
});
|
|
// Transport, then both Food rows in their original order, then the
|
|
// category the server did not mention.
|
|
expect(useTripStore.getState().budgetItems.map(i => i.id)).toEqual([2, 1, 3, 4]);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-013: an item without a category is grouped under "Other"', () => {
|
|
useTripStore.setState({
|
|
budgetItems: [
|
|
buildBudgetItem({ id: 1, category: null }),
|
|
buildBudgetItem({ id: 2, category: 'Food' }),
|
|
],
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({
|
|
type: 'budget:reordered',
|
|
orderedCategories: ['Other', 'Food'],
|
|
});
|
|
expect(useTripStore.getState().budgetItems.map(i => i.id)).toEqual([1, 2]);
|
|
});
|
|
|
|
it('FE-WSEVT-BUDGET-014: a payload with neither key is a no-op', () => {
|
|
useTripStore.setState({
|
|
budgetItems: [buildBudgetItem({ id: 1 }), buildBudgetItem({ id: 2 })],
|
|
});
|
|
useTripStore.getState().handleRemoteEvent({ type: 'budget:reordered' });
|
|
expect(useTripStore.getState().budgetItems.map(i => i.id)).toEqual([1, 2]);
|
|
});
|
|
});
|
|
});
|