mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
177 lines
8.1 KiB
TypeScript
177 lines
8.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { offlineDb } from '../../../src/db/offlineDb';
|
|
import { useAuthStore } from '../../../src/store/authStore';
|
|
import { useTripStore } from '../../../src/store/tripStore';
|
|
import {
|
|
buildAssignment,
|
|
buildBudgetItem,
|
|
buildDay,
|
|
buildPackingItem,
|
|
buildReservation,
|
|
buildTodoItem,
|
|
buildTrip,
|
|
} from '../../helpers/factories';
|
|
import { resetAllStores } from '../../helpers/store';
|
|
|
|
/**
|
|
* The trip-scoped events the broadcast parity work taught the store (plan 2.8):
|
|
* changes written elsewhere (a fellow traveller, the assistant, an MCP client)
|
|
* that used to be dropped and only showed after a reload.
|
|
*/
|
|
|
|
const ME = 7;
|
|
|
|
beforeEach(() => {
|
|
resetAllStores();
|
|
useAuthStore.setState({ user: { id: ME, username: 'me', email: 'me@example.test' } as never });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const send = (event: Record<string, unknown>) => useTripStore.getState().handleRemoteEvent(event as { type: string });
|
|
|
|
describe('remoteEventHandler > broadcast parity', () => {
|
|
it('FE-BCAST-001: assignment:participants sets who joins the visit and caches its day', async () => {
|
|
const put = vi.spyOn(offlineDb.days, 'put');
|
|
useTripStore.setState({
|
|
days: [buildDay({ id: 10 }), buildDay({ id: 20 })],
|
|
assignments: {
|
|
'10': [buildAssignment({ id: 100, day_id: 10 })],
|
|
'20': [buildAssignment({ id: 200, day_id: 20 }), buildAssignment({ id: 201, day_id: 20 })],
|
|
},
|
|
});
|
|
const participants = [{ user_id: 3, username: 'ana', avatar: null }];
|
|
send({ type: 'assignment:participants', assignmentId: 201, participants });
|
|
const { assignments } = useTripStore.getState();
|
|
expect(assignments['20'].find((a) => a.id === 201)?.participants).toEqual(participants);
|
|
expect(assignments['20'].find((a) => a.id === 200)?.participants).not.toEqual(participants);
|
|
expect(assignments['10'][0].participants).not.toEqual(participants);
|
|
await vi.waitFor(() => expect(put).toHaveBeenCalledTimes(1));
|
|
expect(put.mock.calls[0][0]).toMatchObject({ id: 20 });
|
|
});
|
|
|
|
it('FE-BCAST-002: assignment:participants for an assignment this tab does not hold changes nothing', () => {
|
|
const before = { '10': [buildAssignment({ id: 100, day_id: 10 })] };
|
|
useTripStore.setState({ days: [buildDay({ id: 10 })], assignments: before });
|
|
send({ type: 'assignment:participants', assignmentId: 999, participants: [] });
|
|
expect(useTripStore.getState().assignments).toBe(before);
|
|
});
|
|
|
|
it('FE-BCAST-003: packing:reordered numbers the named rows as the server does and caches them', async () => {
|
|
const bulkPut = vi.spyOn(offlineDb.packingItems, 'bulkPut');
|
|
useTripStore.setState({
|
|
packingItems: [
|
|
buildPackingItem({ id: 1, sort_order: 0 }),
|
|
buildPackingItem({ id: 2, sort_order: 1 }),
|
|
buildPackingItem({ id: 3, sort_order: 2 }),
|
|
],
|
|
});
|
|
// 404 is a row this tab does not hold (somebody's private item): it still takes
|
|
// index 1 on the server, so item 1 lands on 2. Item 2 is not named and keeps its order.
|
|
send({ type: 'packing:reordered', orderedIds: [3, 404, 1] });
|
|
const items = useTripStore.getState().packingItems;
|
|
expect(items.map((i) => [i.id, i.sort_order])).toEqual([
|
|
[3, 0],
|
|
[1, 2],
|
|
[2, 1],
|
|
]);
|
|
await vi.waitFor(() => expect(bulkPut).toHaveBeenCalledTimes(1));
|
|
expect(bulkPut.mock.calls[0][0]).toEqual(items);
|
|
});
|
|
|
|
it('FE-BCAST-004: todo:reordered applies the order the same way and caches it', async () => {
|
|
const bulkPut = vi.spyOn(offlineDb.todoItems, 'bulkPut');
|
|
useTripStore.setState({
|
|
todoItems: [buildTodoItem({ id: 1, sort_order: 0 }), buildTodoItem({ id: 2, sort_order: 1 })],
|
|
});
|
|
send({ type: 'todo:reordered', orderedIds: [2, 1] });
|
|
const items = useTripStore.getState().todoItems;
|
|
expect(items.map((i) => [i.id, i.sort_order])).toEqual([
|
|
[2, 0],
|
|
[1, 1],
|
|
]);
|
|
await vi.waitFor(() => expect(bulkPut).toHaveBeenCalledTimes(1));
|
|
expect(bulkPut.mock.calls[0][0]).toEqual(items);
|
|
});
|
|
|
|
it('FE-BCAST-005: a reorder without ids leaves the list alone', () => {
|
|
const packingItems = [buildPackingItem({ id: 1 })];
|
|
useTripStore.setState({ packingItems });
|
|
send({ type: 'packing:reordered' });
|
|
expect(useTripStore.getState().packingItems).toBe(packingItems);
|
|
});
|
|
|
|
it('FE-BCAST-006: trip:deleted of the open trip marks it gone, another trip does not', async () => {
|
|
const drop = vi.spyOn(offlineDb.trips, 'delete');
|
|
useTripStore.setState({ trip: buildTrip({ id: 5 }) });
|
|
send({ type: 'trip:deleted', tripId: 6, id: 6 });
|
|
expect(useTripStore.getState().tripGone).toBeNull();
|
|
send({ type: 'trip:deleted', tripId: 5, id: 5 });
|
|
expect(useTripStore.getState().tripGone).toEqual({ tripId: 5, reason: 'deleted' });
|
|
await vi.waitFor(() => expect(drop).toHaveBeenCalledWith(5));
|
|
});
|
|
|
|
it('FE-BCAST-007: member:removed marks the trip gone only when it is this user', () => {
|
|
useTripStore.setState({ trip: buildTrip({ id: 5 }) });
|
|
send({ type: 'member:removed', tripId: 5, userId: 99 });
|
|
expect(useTripStore.getState().tripGone).toBeNull();
|
|
send({ type: 'member:removed', tripId: 5, userId: ME });
|
|
expect(useTripStore.getState().tripGone).toEqual({ tripId: 5, reason: 'removed' });
|
|
});
|
|
|
|
it('FE-BCAST-025: member:updated renames the guest on every row that lists it and caches those rows', async () => {
|
|
const putBudget = vi.spyOn(offlineDb.budgetItems, 'bulkPut');
|
|
const putDay = vi.spyOn(offlineDb.days, 'put');
|
|
const guest = { user_id: 3, username: 'Ana' };
|
|
const other = { user_id: 4, username: 'Bo' };
|
|
const untouched = buildBudgetItem({ id: 61, members: [{ ...other, paid: 0 }] });
|
|
useTripStore.setState({
|
|
days: [buildDay({ id: 10 })],
|
|
budgetItems: [buildBudgetItem({ id: 60, members: [{ ...guest, paid: 0 }, { ...other, paid: 1 }] }), untouched],
|
|
reservations: [buildReservation({ id: 70, travelers: [guest] })],
|
|
packingItems: [buildPackingItem({ id: 80, recipients: [guest], contributors: [{ ...guest, status: 'packed' }] })],
|
|
assignments: { '10': [buildAssignment({ id: 100, day_id: 10, participants: [guest] })] },
|
|
});
|
|
send({ type: 'member:updated', tripId: 5, member: { id: 3, username: 'Anna' } });
|
|
const state = useTripStore.getState();
|
|
expect(state.budgetItems[0].members?.map((m) => m.username)).toEqual(['Anna', 'Bo']);
|
|
expect(state.budgetItems[1]).toBe(untouched);
|
|
expect(state.reservations[0].travelers?.[0].username).toBe('Anna');
|
|
expect(state.packingItems[0].recipients?.[0].username).toBe('Anna');
|
|
expect(state.packingItems[0].contributors?.[0]).toEqual({ user_id: 3, username: 'Anna', status: 'packed' });
|
|
expect(state.assignments['10'][0].participants?.[0].username).toBe('Anna');
|
|
await vi.waitFor(() => expect(putDay).toHaveBeenCalledTimes(1));
|
|
expect(putBudget.mock.calls[0][0]).toEqual([state.budgetItems[0]]);
|
|
});
|
|
|
|
it("FE-BCAST-026: only this user's removal drops the open trip from the offline cache", async () => {
|
|
const drop = vi.spyOn(offlineDb.trips, 'delete');
|
|
useTripStore.setState({ trip: buildTrip({ id: 5 }) });
|
|
send({ type: 'member:removed', tripId: 5, userId: 99 });
|
|
send({ type: 'member:removed', tripId: 5, userId: ME });
|
|
await vi.waitFor(() => expect(drop).toHaveBeenCalledWith(5));
|
|
expect(drop).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('FE-BCAST-008: loading another trip forgets a trip that went away', () => {
|
|
useTripStore.setState({ tripGone: { tripId: 5, reason: 'removed' } });
|
|
useTripStore.getState().resetTrip();
|
|
expect(useTripStore.getState().tripGone).toBeNull();
|
|
});
|
|
|
|
it('FE-BCAST-009: a burst of packing:template-applied reads the list once, after it settles', () => {
|
|
vi.useFakeTimers();
|
|
const reload = vi.fn(async () => undefined);
|
|
useTripStore.setState({ trip: buildTrip({ id: 5 }), reloadPackingItems: reload });
|
|
for (let i = 0; i < 5; i++) send({ type: 'packing:template-applied', tripId: 5, items: [] });
|
|
expect(reload).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(300);
|
|
expect(reload).toHaveBeenCalledTimes(1);
|
|
expect(reload).toHaveBeenCalledWith(5);
|
|
});
|
|
});
|