Files
TREK/client/tests/unit/mobile/trip/transportsModel.test.ts
T
Maurice 4ee5e3fa96 fix(bookings): booking cards always show the calendar date of their linked day
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
2026-10-12 10:16:01 +02:00

205 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});
});