mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
4ee5e3fa96
Phone booking and transport cards printed only the title of the linked day, so a long title cut off in the field left no date at all. They now print the date beside the title like the desktop card, as a span that never shrinks while the title is truncated, and a day range takes the full width with the time under it. The desktop card gets the same non-shrinking date. A day without a date of its own (a trip planned without dates) now borrows the booking's start or end date, on the desktop cards, the list, the detail dialog, the shared page and the phone alike. Refs #2590
205 lines
10 KiB
TypeScript
205 lines
10 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',
|
||
dayDate: 'Jul 1 – Jul 3',
|
||
dayRange: true,
|
||
timeValue: '09:05 – 18:30',
|
||
});
|
||
expect(cardWhen(r, day1, day1, t, 'en-US', '24h')).toMatchObject({ dayValue: 'dayplan.dayN:1', dayDate: 'Wed, Jul 1', dayRange: false });
|
||
});
|
||
|
||
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',
|
||
dayDate: null,
|
||
dayRange: false,
|
||
timeValue: '—',
|
||
});
|
||
expect(cardWhen(res({ reservation_time: null }), undefined, undefined, t, 'en-US', '24h')).toEqual({
|
||
dayValue: '—',
|
||
dayDate: null,
|
||
dayRange: false,
|
||
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');
|
||
});
|
||
|
||
it('FE-MOB-TRPM-018: a day without a date shows the booking date beside its title (#2590)', () => {
|
||
const departure = buildDay({ id: 200, day_number: 1, title: 'Germany Departure Flight', date: null });
|
||
const arrival = buildDay({ id: 201, day_number: 2, title: 'Korea Arrival', date: null });
|
||
const r = res({ reservation_time: '2026-10-21T15:35', reservation_end_time: '2026-10-22T09:55' });
|
||
expect(cardWhen(r, departure, arrival, t, 'en-US', '24h')).toEqual({
|
||
dayValue: 'Germany Departure Flight – Korea Arrival',
|
||
dayDate: 'Oct 21 – Oct 22',
|
||
dayRange: true,
|
||
timeValue: '15:35 – 09:55',
|
||
});
|
||
expect(cardWhen(res({ reservation_time: null }), departure, undefined, t, 'en-US', '24h').dayDate).toBeNull();
|
||
});
|
||
});
|