mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
27f12c2245
A new Flight widget on iOS (small to large and the lock screen) and Android shows the trip's next flight or the one in the air: both airports, the arc with the aircraft, a countdown the system keeps running, and the delay. It follows the feed's phase, so a late departure is not flown along the arc. Around a flight the travel day's Live Activity becomes a boarding pass with a running bar and countdown, in the Dynamic Island too.
144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
// FE-NATIVE-WIDGETFLIGHT-001 to FE-NATIVE-WIDGETFLIGHT-008
|
|
import type { FlightLiveLeg } from '@trek/shared';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { pickWidgetLeg, widgetFlight } from './widgetFlight';
|
|
import { liveFlightOf } from './widgets';
|
|
|
|
const DEP = Date.parse('2026-06-11T11:50:00Z');
|
|
const ARR = Date.parse('2026-06-11T20:35:00Z');
|
|
const H = 3_600_000;
|
|
const CLOCK = { locale: 'en-GB', timeFormat: '24h' };
|
|
|
|
function leg(over: Partial<FlightLiveLeg> = {}): FlightLiveLeg {
|
|
return {
|
|
reservation_id: 6,
|
|
leg_index: 0,
|
|
callsign: 'DLH400',
|
|
flight_number: 'LH 400',
|
|
service_date: '2026-06-11',
|
|
origin: { code: 'FRA', name: 'Frankfurt', lat: 50.03, lng: 8.57, timezone: 'Europe/Berlin' },
|
|
destination: { code: 'JFK', name: 'New York JFK', lat: 40.64, lng: -73.78, timezone: 'America/New_York' },
|
|
phase: 'scheduled',
|
|
scheduled_departure: new Date(DEP).toISOString(),
|
|
scheduled_arrival: new Date(ARR).toISOString(),
|
|
departed_at: null,
|
|
landed_at: null,
|
|
estimated_arrival: null,
|
|
departure_delay_min: null,
|
|
arrival_delay_min: null,
|
|
progress: null,
|
|
distance_total_km: null,
|
|
distance_remaining_km: null,
|
|
position: null,
|
|
aircraft: null,
|
|
last_seen_at: null,
|
|
fetched_at: null,
|
|
stale: false,
|
|
source: null,
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe('pickWidgetLeg', () => {
|
|
it('FE-NATIVE-WIDGETFLIGHT-001: prefers the flight in the air', () => {
|
|
const ahead = leg({ reservation_id: 7 });
|
|
const flying = leg({ reservation_id: 8, phase: 'airborne' });
|
|
expect(pickWidgetLeg([ahead, flying], DEP)?.reservation_id).toBe(8);
|
|
});
|
|
|
|
it('FE-NATIVE-WIDGETFLIGHT-002: takes the earliest within a week and drops one more than two hours landed', () => {
|
|
const later = leg({
|
|
reservation_id: 9,
|
|
scheduled_departure: new Date(DEP + 48 * H).toISOString(),
|
|
scheduled_arrival: new Date(ARR + 48 * H).toISOString(),
|
|
});
|
|
expect(pickWidgetLeg([later, leg()], DEP - H)?.reservation_id).toBe(6);
|
|
expect(pickWidgetLeg([leg()], ARR + 3 * H)).toBeNull();
|
|
expect(pickWidgetLeg([leg()], DEP - 8 * 24 * H)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('widgetFlight', () => {
|
|
it('FE-NATIVE-WIDGETFLIGHT-003: sends the schedule in seconds and the clock at each airport', () => {
|
|
const flight = widgetFlight([leg()], DEP - H, '/trips/1', CLOCK);
|
|
expect(flight).toMatchObject({
|
|
number: 'LH 400',
|
|
from: 'FRA',
|
|
to: 'JFK',
|
|
dep: DEP / 1000,
|
|
arr: ARR / 1000,
|
|
depTime: '13:50',
|
|
arrTime: '16:35',
|
|
path: '/trips/1?booking=6',
|
|
});
|
|
expect(flight?.arrExpected).toBeUndefined();
|
|
expect(flight?.landed).toBeUndefined();
|
|
});
|
|
|
|
it('FE-NATIVE-WIDGETFLIGHT-004: adds what the feed observed while the flight is in the air', () => {
|
|
const flight = widgetFlight(
|
|
[
|
|
leg({
|
|
phase: 'airborne',
|
|
departed_at: new Date(DEP + 20 * 60_000).toISOString(),
|
|
estimated_arrival: new Date(ARR + 15 * 60_000).toISOString(),
|
|
arrival_delay_min: 15,
|
|
aircraft: { registration: 'D-AIXA', type: 'A359' },
|
|
last_seen_at: new Date(DEP + 2 * H).toISOString(),
|
|
}),
|
|
],
|
|
DEP + 2 * H,
|
|
'/trips/1',
|
|
CLOCK
|
|
);
|
|
expect(flight).toMatchObject({
|
|
depActual: (DEP + 20 * 60_000) / 1000,
|
|
arrExpected: (ARR + 15 * 60_000) / 1000,
|
|
arrTimeExpected: '16:50',
|
|
delay: 15,
|
|
aircraft: 'A359 D-AIXA',
|
|
});
|
|
});
|
|
|
|
it('FE-NATIVE-WIDGETFLIGHT-005: says landed once touched down, and nothing without a flight', () => {
|
|
expect(
|
|
widgetFlight([leg({ phase: 'landed', landed_at: new Date(ARR).toISOString() })], ARR + H, '/trips/1', CLOCK)
|
|
?.landed
|
|
).toBe(true);
|
|
expect(widgetFlight([], DEP, '/trips/1', CLOCK)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('liveFlightOf', () => {
|
|
it('FE-NATIVE-WIDGETFLIGHT-006: holds the Live Activity from three hours before take-off to an hour after touchdown', () => {
|
|
const flight = widgetFlight([leg()], DEP - H, '/trips/1', CLOCK);
|
|
const at = (ms: number) => liveFlightOf(flight, ms / 1000);
|
|
expect(at(DEP - 4 * H)).toBeUndefined();
|
|
expect(at(DEP - 2 * H)).toMatchObject({
|
|
number: 'LH 400',
|
|
from: 'FRA',
|
|
to: 'JFK',
|
|
depTime: '13:50',
|
|
arrTime: '16:35',
|
|
});
|
|
expect(at(ARR + 2 * H)).toBeUndefined();
|
|
expect(liveFlightOf({ ...(flight as NonNullable<typeof flight>), landed: true }, (DEP + H) / 1000)).toBeUndefined();
|
|
expect(liveFlightOf(undefined, DEP / 1000)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('widget phase', () => {
|
|
it('FE-NATIVE-WIDGETFLIGHT-007: a late departure goes along as the expected one, and the flight in the air says so', () => {
|
|
const late = widgetFlight([leg({ departure_delay_min: 40 })], DEP, '/trips/1', CLOCK);
|
|
expect(late?.depExpected).toBe(DEP / 1000 + 40 * 60);
|
|
expect(late?.airborne).toBeUndefined();
|
|
expect(widgetFlight([leg({ phase: 'airborne' })], DEP + H, '/trips/1', CLOCK)?.airborne).toBe(true);
|
|
expect(liveFlightOf(late, DEP / 1000)?.dep).toBe(DEP / 1000 + 40 * 60);
|
|
});
|
|
|
|
it('FE-NATIVE-WIDGETFLIGHT-008: a flight the feed lost in the air stops being picked two hours after arrival', () => {
|
|
expect(pickWidgetLeg([leg({ phase: 'airborne' })], ARR + 3 * H)).toBeNull();
|
|
});
|
|
});
|