mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
1b51181930
The bookings model moves from the phone tabs into components/Planner/bookings, so both shells read the same sections, traveller predicate and type colours. The phone bookings and transports tabs had the same traveller filter and folded sections twice; that state now lives once in useReservationListFilter, which only those two tabs use. The desktop panel keeps its own filters in useBookingsView because it remembers them for the session. The status switch and the delete on a booking card stay per shell. The desktop hook renders its ConfirmDialog and awaits the delete, the phone asks in an MConfirmSheet and lets the delete run on its own; sharing them would change when the question closes.
186 lines
9.2 KiB
TypeScript
186 lines
9.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import { cardWhen, parseTransportMeta } from '../../../../src/mobile/screens/trip/tabs/transportsModel';
|
||
import { TRANSPORT_TYPE_COLOR, groupTransports } from '../../../../src/components/Planner/bookings/bookingsModel';
|
||
import { orderedEndpoints } from '../../../../src/utils/flightLegs';
|
||
import { buildDay, buildReservation } from '../../../helpers/factories';
|
||
import type { Day, Reservation } from '../../../../src/types';
|
||
|
||
// FE-MOB-TRPM-001 to FE-MOB-TRPM-017
|
||
|
||
function res(overrides: Partial<Reservation> = {}): Reservation {
|
||
return buildReservation({ type: 'flight', status: 'confirmed', ...overrides });
|
||
}
|
||
|
||
describe('transportsModel — metadata', () => {
|
||
it('FE-MOB-TRPM-001: exposes a chip colour per transport type', () => {
|
||
expect(TRANSPORT_TYPE_COLOR.flight).toBe('#3b82f6');
|
||
expect(TRANSPORT_TYPE_COLOR.transit).toBe('#7c3aed');
|
||
expect(TRANSPORT_TYPE_COLOR.transport_other).toBe(TRANSPORT_TYPE_COLOR.car);
|
||
expect(TRANSPORT_TYPE_COLOR.nope).toBeUndefined();
|
||
});
|
||
|
||
it('FE-MOB-TRPM-002: parses a JSON metadata string', () => {
|
||
const meta = parseTransportMeta(
|
||
res({ metadata: '{"airline":"LH","flight_number":"LH123","seat":"14A"}' }),
|
||
);
|
||
expect(meta).toEqual({ airline: 'LH', flight_number: 'LH123', seat: '14A' });
|
||
});
|
||
|
||
it('FE-MOB-TRPM-003: passes an already-parsed metadata object through', () => {
|
||
const parsed = { transit: { legs: [{ mode: 'subway', line: 'U2' }] } };
|
||
expect(parseTransportMeta({ ...res(), metadata: parsed } as unknown as Reservation)).toEqual(parsed);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-004: returns an empty object for missing or broken metadata', () => {
|
||
expect(parseTransportMeta(res({ metadata: null }))).toEqual({});
|
||
expect(parseTransportMeta(res({ metadata: '' }))).toEqual({});
|
||
expect(parseTransportMeta(res({ metadata: '{not json' }))).toEqual({});
|
||
});
|
||
|
||
it('FE-MOB-TRPM-005: orderedEndpoints sorts waypoints by sequence without mutating', () => {
|
||
const endpoints = [
|
||
{ role: 'to' as const, sequence: 2, name: 'Rome', code: null, lat: 0, lng: 0, timezone: null, local_time: null, local_date: null },
|
||
{ role: 'from' as const, sequence: 0, name: 'Berlin', code: null, lat: 0, lng: 0, timezone: null, local_time: null, local_date: null },
|
||
{ role: 'stop' as const, sequence: 1, name: 'Munich', code: null, lat: 0, lng: 0, timezone: null, local_time: null, local_date: null },
|
||
];
|
||
const r = res({ endpoints });
|
||
|
||
expect(orderedEndpoints(r).map(e => e.name)).toEqual(['Berlin', 'Munich', 'Rome']);
|
||
expect(endpoints.map(e => e.name)).toEqual(['Rome', 'Berlin', 'Munich']);
|
||
expect(orderedEndpoints(res({ endpoints: undefined }))).toEqual([]);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-006: orderedEndpoints treats a missing sequence as zero', () => {
|
||
const rome = { role: 'to' as const, sequence: 1, name: 'Rome', code: null, lat: 0, lng: 0, timezone: null, local_time: null, local_date: null };
|
||
const berlin = { role: 'from' as const, name: 'Berlin', code: null, lat: 0, lng: 0, timezone: null, local_time: null, local_date: null };
|
||
const pair = (list: unknown[]) =>
|
||
orderedEndpoints(res({ endpoints: list as unknown as Reservation['endpoints'] })).map(e => e.name);
|
||
|
||
expect(pair([rome, berlin])).toEqual(['Berlin', 'Rome']);
|
||
expect(pair([berlin, rome])).toEqual(['Berlin', 'Rome']);
|
||
});
|
||
});
|
||
|
||
describe('transportsModel — groupTransports', () => {
|
||
const days: Day[] = [
|
||
buildDay({ id: 100, date: '2026-07-01' }),
|
||
buildDay({ id: 101, date: '2026-07-02' }),
|
||
buildDay({ id: 102, date: '2026-07-03' }),
|
||
];
|
||
|
||
it('FE-MOB-TRPM-007: sorts by reservation date and time', () => {
|
||
const late = res({ id: 1, reservation_time: '2026-07-02T18:00' });
|
||
const early = res({ id: 2, reservation_time: '2026-07-02T06:30' });
|
||
const first = res({ id: 3, reservation_time: '2026-07-01T23:00' });
|
||
|
||
expect(groupTransports([late, early, first], days).confirmed.map(r => r.id)).toEqual([3, 2, 1]);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-008: falls back to the linked day date, treating a date-only value as midnight', () => {
|
||
const viaDay = res({ id: 1, reservation_time: null, day_id: 100 });
|
||
const dateOnly = res({ id: 2, reservation_time: '2026-07-01' });
|
||
const withTime = res({ id: 3, reservation_time: '2026-07-01T08:00' });
|
||
|
||
// both undated-but-day-linked entries key on 00:00, so created_at breaks the tie
|
||
const sorted = groupTransports(
|
||
[
|
||
withTime,
|
||
{ ...dateOnly, created_at: '2025-01-02T00:00:00.000Z' },
|
||
{ ...viaDay, created_at: '2025-01-01T00:00:00.000Z' },
|
||
],
|
||
days,
|
||
).confirmed;
|
||
expect(sorted.map(r => r.id)).toEqual([1, 2, 3]);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-009: a hotel keys on its accommodation start day, not its day_id', () => {
|
||
const hotel = res({
|
||
id: 1,
|
||
type: 'hotel',
|
||
reservation_time: null,
|
||
day_id: 102,
|
||
accommodation_start_day_id: 100,
|
||
});
|
||
const flight = res({ id: 2, reservation_time: '2026-07-02T09:00' });
|
||
|
||
expect(groupTransports([flight, hotel], days).confirmed.map(r => r.id)).toEqual([1, 2]);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-010: a hotel without an accommodation start day still uses day_id', () => {
|
||
const hotel = res({ id: 1, type: 'hotel', reservation_time: null, day_id: 102 });
|
||
const flight = res({ id: 2, reservation_time: '2026-07-02T09:00' });
|
||
|
||
expect(groupTransports([flight, hotel], days).confirmed.map(r => r.id)).toEqual([2, 1]);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-011: undated entries and entries on unknown days sink to the bottom', () => {
|
||
const dated = res({ id: 1, reservation_time: '2026-07-02T09:00' });
|
||
const undated = res({ id: 2, reservation_time: null, day_id: null, created_at: '2025-01-01T00:00:00.000Z' });
|
||
const unknownDay = res({ id: 3, reservation_time: null, day_id: 999, created_at: '2025-01-02T00:00:00.000Z' });
|
||
|
||
expect(groupTransports([undated, unknownDay, dated], days).confirmed.map(r => r.id)).toEqual([1, 2, 3]);
|
||
// same result whichever side of the comparison the undated entry lands on
|
||
expect(groupTransports([undated, dated], days).confirmed.map(r => r.id)).toEqual([1, 2]);
|
||
expect(groupTransports([dated, undated], days).confirmed.map(r => r.id)).toEqual([1, 2]);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-012: splits confirmed, pending and automated transit', () => {
|
||
const confirmed = res({ id: 1, reservation_time: '2026-07-01T08:00' });
|
||
const pending = res({ id: 2, status: 'pending', reservation_time: '2026-07-01T09:00' });
|
||
const cancelled = res({ id: 3, status: 'cancelled', reservation_time: '2026-07-01T07:00' });
|
||
const transitConfirmed = res({ id: 4, type: 'transit', reservation_time: '2026-07-01T10:00' });
|
||
const transitPending = res({ id: 5, type: 'transit', status: 'pending', reservation_time: '2026-07-01T06:00' });
|
||
|
||
const groups = groupTransports([confirmed, pending, cancelled, transitConfirmed, transitPending], days);
|
||
|
||
expect(groups.confirmed.map(r => r.id)).toEqual([1]);
|
||
// anything not confirmed lands in pending, still chronological
|
||
expect(groups.pending.map(r => r.id)).toEqual([3, 2]);
|
||
// transit is peeled off regardless of status
|
||
expect(groups.transit.map(r => r.id)).toEqual([5, 4]);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-013: an empty list yields three empty groups', () => {
|
||
expect(groupTransports([], days)).toEqual({ confirmed: [], pending: [], transit: [] });
|
||
expect(groupTransports([res({ id: 1, reservation_time: '2026-07-01T08:00' })], []).confirmed).toHaveLength(1);
|
||
});
|
||
|
||
it('FE-MOB-TRPM-014: entries without a created_at still sort deterministically', () => {
|
||
const a = { ...res({ id: 1, reservation_time: '2026-07-02T09:00' }), created_at: undefined };
|
||
const b = { ...res({ id: 2, reservation_time: '2026-07-02T09:00' }), created_at: undefined };
|
||
|
||
expect(groupTransports([a, b], days).confirmed.map(r => r.id)).toEqual([1, 2]);
|
||
});
|
||
});
|
||
|
||
describe('transportsModel — cardWhen', () => {
|
||
const t = (key: string, params?: Record<string, string | number | null>) => `${key}:${params?.n}`;
|
||
const day1 = buildDay({ id: 100, day_number: 1, title: null, date: '2026-07-01' });
|
||
const day3 = buildDay({ id: 102, day_number: 3, title: 'Coast', date: '2026-07-03' });
|
||
|
||
it('FE-MOB-TRPM-015: names the day or the day range and the time or time range', () => {
|
||
const r = res({ reservation_time: '2026-07-01T09:05', reservation_end_time: '2026-07-03T18:30' });
|
||
expect(cardWhen(r, day1, day3, t, 'en-US', '24h')).toEqual({
|
||
dayValue: 'dayplan.dayN:1 – Coast',
|
||
timeValue: '09:05 – 18:30',
|
||
});
|
||
expect(cardWhen(r, day1, day1, t, 'en-US', '24h').dayValue).toBe('dayplan.dayN:1');
|
||
});
|
||
|
||
it('FE-MOB-TRPM-016: without a day the booking date stands in, then a dash', () => {
|
||
expect(cardWhen(res({ reservation_time: '2026-07-04' }), undefined, undefined, t, 'en-US', '24h')).toEqual({
|
||
dayValue: 'Jul 4',
|
||
timeValue: '—',
|
||
});
|
||
expect(cardWhen(res({ reservation_time: null }), undefined, undefined, t, 'en-US', '24h')).toEqual({
|
||
dayValue: '—',
|
||
timeValue: '—',
|
||
});
|
||
});
|
||
|
||
it('FE-MOB-TRPM-017: a start time alone shows on its own, in the chosen clock', () => {
|
||
const r = res({ reservation_time: '2026-07-01T14:00', reservation_end_time: null });
|
||
expect(cardWhen(r, day1, undefined, t, 'en-US', '12h').timeValue).toBe('2:00 PM');
|
||
});
|
||
});
|