test(reservations): server + client coverage for traveler assignment

This commit is contained in:
Maurice
2026-07-24 14:32:28 +02:00
committed by Maurice
parent e89dae5528
commit bc06125d6e
5 changed files with 284 additions and 0 deletions
@@ -0,0 +1,66 @@
// FE-COMP-TRAVELERS-001 to FE-COMP-TRAVELERS-006
import { render, screen } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import type { ReservationTraveler } from '@trek/shared';
import type { TripMember } from '../Budget/BudgetPanelMemberChips';
import TravelerChips from './TravelerChips';
const tripMembers: TripMember[] = [
{ id: 1, username: 'alice', avatar_url: null },
{ id: 2, username: 'bob', avatar_url: null },
];
const travelers: ReservationTraveler[] = [
{ user_id: 1, username: 'alice', avatar_url: '/uploads/avatars/alice.jpg' },
{ user_id: 2, username: 'bob', avatar_url: '/uploads/avatars/bob.jpg' },
];
describe('TravelerChips', () => {
it('FE-COMP-TRAVELERS-001: renders an avatar chip per traveler', () => {
render(<TravelerChips travelers={travelers} tripMembers={tripMembers} onSetTravelers={vi.fn()} />);
// Each assigned traveler carries an avatar_url, so one <img> renders per chip.
expect(document.querySelectorAll('img')).toHaveLength(2);
});
it('FE-COMP-TRAVELERS-002: picker button opens the dropdown listing trip members', async () => {
const user = userEvent.setup();
render(<TravelerChips travelers={[]} tripMembers={tripMembers} onSetTravelers={vi.fn()} />);
// Dropdown starts closed — members are not in the DOM yet.
expect(screen.queryByText('alice')).not.toBeInTheDocument();
// reservations.travelers.assign = "Assign travelers"
await user.click(screen.getByRole('button', { name: 'Assign travelers' }));
expect(screen.getByText('alice')).toBeInTheDocument();
expect(screen.getByText('bob')).toBeInTheDocument();
});
it('FE-COMP-TRAVELERS-003: toggling a member on calls onSetTravelers with the added id', async () => {
const user = userEvent.setup();
const onSetTravelers = vi.fn();
render(<TravelerChips travelers={[]} tripMembers={tripMembers} onSetTravelers={onSetTravelers} />);
await user.click(screen.getByRole('button', { name: 'Assign travelers' }));
await user.click(screen.getByText('alice'));
expect(onSetTravelers).toHaveBeenCalledWith([1]);
});
it('FE-COMP-TRAVELERS-004: toggling an assigned member off calls onSetTravelers without their id', async () => {
const user = userEvent.setup();
const onSetTravelers = vi.fn();
render(<TravelerChips travelers={[travelers[0]]} tripMembers={tripMembers} onSetTravelers={onSetTravelers} />);
await user.click(screen.getByRole('button', { name: 'Assign travelers' }));
await user.click(screen.getByText('alice'));
expect(onSetTravelers).toHaveBeenCalledWith([]);
});
it('FE-COMP-TRAVELERS-005: read-only with no travelers renders nothing', () => {
const { container } = render(
<TravelerChips travelers={[]} tripMembers={tripMembers} onSetTravelers={vi.fn()} readOnly />
);
expect(container).toBeEmptyDOMElement();
});
it('FE-COMP-TRAVELERS-006: read-only with travelers renders chips but hides the picker button', () => {
render(<TravelerChips travelers={travelers} tripMembers={tripMembers} onSetTravelers={vi.fn()} readOnly />);
expect(document.querySelectorAll('img')).toHaveLength(2);
expect(screen.queryByRole('button', { name: 'Assign travelers' })).not.toBeInTheDocument();
});
});
@@ -0,0 +1,56 @@
// FE-STORE-RESERVATIONS-001 to FE-STORE-RESERVATIONS-003
import { http, HttpResponse } from 'msw';
import { server } from '../../../tests/helpers/msw/server';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { buildReservation } from '../../../tests/helpers/factories';
import { useTripStore } from '../tripStore';
beforeEach(() => {
resetAllStores();
server.resetHandlers();
});
describe('reservationsSlice', () => {
it('FE-STORE-RESERVATIONS-001: setReservationTravelers patches travelers on matching reservation', async () => {
const reservation = buildReservation({ id: 5, trip_id: 1, travelers: [] });
seedStore(useTripStore, { reservations: [reservation] });
const travelers = [{ user_id: 1, username: 'alice', avatar_url: null, avatar: null, is_guest: 0 }];
server.use(
http.put('/api/trips/1/reservations/5/travelers', () =>
HttpResponse.json({ travelers })
)
);
await useTripStore.getState().setReservationTravelers(1, 5, [1]);
const stored = useTripStore.getState().reservations.find(r => r.id === 5);
expect(stored?.travelers).toHaveLength(1);
expect(stored?.travelers?.[0].user_id).toBe(1);
});
it('FE-STORE-RESERVATIONS-002: setReservationTravelers throws on API error', async () => {
const reservation = buildReservation({ id: 6, trip_id: 1 });
seedStore(useTripStore, { reservations: [reservation] });
server.use(
http.put('/api/trips/1/reservations/6/travelers', () =>
HttpResponse.json({ error: 'forbidden' }, { status: 403 })
)
);
await expect(useTripStore.getState().setReservationTravelers(1, 6, [1])).rejects.toThrow();
});
it('FE-STORE-RESERVATIONS-003: setReservationTravelers leaves other reservations untouched', async () => {
const a = buildReservation({ id: 7, trip_id: 1, travelers: [] });
const b = buildReservation({ id: 8, trip_id: 1, travelers: [] });
seedStore(useTripStore, { reservations: [a, b] });
const travelers = [{ user_id: 2, username: 'bob', avatar_url: null }];
server.use(
http.put('/api/trips/1/reservations/7/travelers', () =>
HttpResponse.json({ travelers })
)
);
await useTripStore.getState().setReservationTravelers(1, 7, [2]);
expect(useTripStore.getState().reservations.find(r => r.id === 8)?.travelers).toEqual([]);
});
});