mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
373 lines
12 KiB
TypeScript
373 lines
12 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type { Day, Trip } from '../types';
|
|
|
|
vi.mock('./platform', () => ({ isNativeIos: vi.fn(() => true), isNativeApp: vi.fn(() => true) }));
|
|
vi.mock('./capabilities', () => ({ hasNativeFeature: vi.fn(() => true) }));
|
|
vi.mock('./reportFailure', () => ({ reportNativeFailure: vi.fn() }));
|
|
vi.mock('./trekShell', () => ({
|
|
TrekShell: {
|
|
setWidgetSnapshot: vi.fn(async () => {}),
|
|
syncTripDay: vi.fn(async () => ({ running: true })),
|
|
endTripDay: vi.fn(async () => {}),
|
|
},
|
|
}));
|
|
|
|
import { TrekShell } from './trekShell';
|
|
import {
|
|
buildSnapshot,
|
|
pickTrip,
|
|
publishWidgets,
|
|
todayOf,
|
|
tripDayState,
|
|
widgetTripChoices,
|
|
withBookings,
|
|
} from './widgets';
|
|
|
|
const trip = (id: number, start: string | null, end: string | null, archived = 0) =>
|
|
({ id, title: `Trip ${id}`, start_date: start, end_date: end, is_archived: archived }) as unknown as Trip;
|
|
|
|
const day = (id: number, n: number, date: string, places: { name: string; time?: string }[]) =>
|
|
({
|
|
id,
|
|
trip_id: 1,
|
|
day_number: n,
|
|
date,
|
|
assignments: places.map((p, i) => ({
|
|
id: id * 10 + i,
|
|
order_index: i,
|
|
place: { name: p.name, place_time: p.time ?? null },
|
|
})),
|
|
}) as unknown as Day;
|
|
|
|
const labels = {
|
|
nextTrip: 'Next',
|
|
today: 'Today',
|
|
noTrip: 'None',
|
|
inDays: 'in {n} days',
|
|
tomorrow: 'Tomorrow',
|
|
nothingPlanned: '-',
|
|
};
|
|
|
|
describe('pickTrip', () => {
|
|
it('prefers the running trip, then the next upcoming one', () => {
|
|
const trips = [
|
|
trip(1, '2026-12-01', '2026-12-05'),
|
|
trip(2, '2026-11-20', '2026-11-25'),
|
|
trip(3, '2026-11-01', '2026-11-10'),
|
|
];
|
|
expect(pickTrip(trips, '2026-11-06')?.id).toBe(3);
|
|
expect(pickTrip(trips, '2026-11-12')?.id).toBe(2);
|
|
});
|
|
|
|
it('of several running trips takes the dashboard hero: the one that started first', () => {
|
|
const trips = [
|
|
trip(1, '2026-11-02', '2026-11-06'),
|
|
trip(2, '2026-10-20', '2026-11-18'),
|
|
trip(3, '2026-11-04', '2026-11-12'),
|
|
];
|
|
expect(pickTrip(trips, '2026-11-06')?.id).toBe(2);
|
|
});
|
|
|
|
it('skips undated, archived and finished trips', () => {
|
|
expect(
|
|
pickTrip(
|
|
[trip(1, null, null), trip(2, '2026-12-01', '2026-12-02', 1), trip(3, '2026-01-01', '2026-01-02')],
|
|
'2026-11-06'
|
|
)
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('todayOf and tripDayState', () => {
|
|
const days = [
|
|
day(2, 2, '2026-11-07', []),
|
|
day(1, 1, '2026-11-06', [{ name: 'Museum', time: '09:00' }, { name: 'Walk' }, { name: 'Dinner', time: '19:30' }]),
|
|
];
|
|
|
|
it('finds today in day order and keeps the plan order', () => {
|
|
const today = todayOf(
|
|
trip(1, '2026-11-06', '2026-11-07'),
|
|
days,
|
|
'2026-11-06',
|
|
(n, m) => `${n}/${m}`,
|
|
(t) => `T${t}`
|
|
);
|
|
expect(today?.dayLabel).toBe('1/2');
|
|
expect(today?.stops.map((s) => s.name)).toEqual(['Museum', 'Walk', 'Dinner']);
|
|
expect(today?.stops[0]).toMatchObject({ time: 'T09:00', minutes: 540 });
|
|
});
|
|
|
|
it('is null on a day outside the trip', () => {
|
|
expect(
|
|
todayOf(
|
|
trip(1, '2026-11-06', '2026-11-07'),
|
|
days,
|
|
'2026-11-09',
|
|
() => '',
|
|
(t) => t
|
|
)
|
|
).toBeNull();
|
|
});
|
|
|
|
it('names the next timed stop and counts the ones behind', () => {
|
|
const today = todayOf(
|
|
trip(1, '2026-11-06', '2026-11-07'),
|
|
days,
|
|
'2026-11-06',
|
|
() => 'D',
|
|
(t) => t
|
|
);
|
|
if (!today) throw new Error('the trip runs today');
|
|
expect(tripDayState(today, 8 * 60, 'done')).toMatchObject({ nextName: 'Museum', done: 0, total: 2 });
|
|
expect(tripDayState(today, 12 * 60, 'done')).toMatchObject({ nextName: 'Dinner', nextTime: '19:30', done: 1 });
|
|
expect(tripDayState(today, 22 * 60, 'done')).toMatchObject({ nextName: 'done', done: 2, nextTime: undefined });
|
|
});
|
|
|
|
it('gives the Android Live Update the next stop in minutes and one colour per timed stop', () => {
|
|
const state = tripDayState(
|
|
{
|
|
tripTitle: 'T',
|
|
path: '/trips/1',
|
|
dayLabel: 'D',
|
|
stops: [
|
|
{ name: 'Museum', time: '10:00', minutes: 600, color: '#D6273B' },
|
|
{ name: 'Walk', time: null, minutes: null },
|
|
{ name: 'Dinner', time: '19:30', minutes: 1170, color: null },
|
|
],
|
|
},
|
|
12 * 60,
|
|
'done'
|
|
);
|
|
expect(state).toMatchObject({
|
|
nextName: 'Dinner',
|
|
nextMinutes: 1170,
|
|
colors: ['#D6273B', null],
|
|
done: 1,
|
|
total: 2,
|
|
});
|
|
expect(
|
|
tripDayState({ tripTitle: 'T', path: '/', dayLabel: 'D', stops: [] }, 0, 'done').nextMinutes
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('publishWidgets', () => {
|
|
it('writes the snapshot without the internal minutes and ends the activity on a day without times', async () => {
|
|
const snapshot = buildSnapshot(
|
|
trip(1, '2026-11-06', '2026-11-07'),
|
|
{ tripTitle: 'Trip 1', path: '/trips/1', dayLabel: 'D', stops: [{ name: 'Walk', time: null, minutes: null }] },
|
|
labels
|
|
);
|
|
expect(snapshot.today?.stops[0]).toEqual({ name: 'Walk', time: null });
|
|
await publishWidgets(snapshot, {
|
|
tripTitle: 'Trip 1',
|
|
path: '/trips/1',
|
|
dayLabel: 'D',
|
|
nextName: 'x',
|
|
nextTime: undefined,
|
|
done: 0,
|
|
total: 0,
|
|
});
|
|
expect(JSON.parse(vi.mocked(TrekShell.setWidgetSnapshot).mock.calls[0][0].json).trip.path).toBe('/trips/1');
|
|
expect(TrekShell.endTripDay).toHaveBeenCalled();
|
|
expect(TrekShell.syncTripDay).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('the richer snapshot', () => {
|
|
it('puts the timed bookings of the day between the stops and gives the day its place in the trip', () => {
|
|
const days = [
|
|
day(1, 1, '2026-11-06', [{ name: 'Museum', time: '09:00' }, { name: 'Walk' }, { name: 'Dinner', time: '19:30' }]),
|
|
day(2, 2, '2026-11-07', []),
|
|
];
|
|
const plain = todayOf(
|
|
trip(1, '2026-11-06', '2026-11-07'),
|
|
days,
|
|
'2026-11-06',
|
|
() => 'D',
|
|
(t) => t
|
|
);
|
|
if (!plain) throw new Error('the trip runs today');
|
|
const today = withBookings(
|
|
plain,
|
|
[
|
|
{ id: 1, trip_id: 1, title: 'Train', type: 'train', status: 'confirmed', reservation_time: '2026-11-06T12:15' },
|
|
{
|
|
id: 2,
|
|
trip_id: 1,
|
|
title: 'Tomorrow',
|
|
type: 'flight',
|
|
status: 'confirmed',
|
|
reservation_time: '2026-11-07T08:00',
|
|
},
|
|
{ id: 3, trip_id: 1, title: 'Gone', type: 'event', status: 'cancelled', reservation_time: '2026-11-06T10:00' },
|
|
] as never,
|
|
'2026-11-06',
|
|
(t) => t
|
|
);
|
|
expect(today.stops.map((s) => s.name)).toEqual(['Museum', 'Walk', 'Train', 'Dinner']);
|
|
expect(today.stops[2]).toMatchObject({ time: '12:15', minutes: 735, symbol: 'tram.fill' });
|
|
expect(today).toMatchObject({ index: 1, total: 2 });
|
|
expect(withBookings(plain, [], '2026-11-06', (t) => t)).toBe(plain);
|
|
});
|
|
|
|
it('makes a stop of every leg of a flight with a stopover, and keeps a flight in the air ahead until it lands', () => {
|
|
const plain = todayOf(
|
|
trip(1, '2026-11-09', '2026-11-10'),
|
|
[day(5, 1, '2026-11-09', []), day(6, 2, '2026-11-10', [])],
|
|
'2026-11-09',
|
|
() => 'D',
|
|
(t) => t
|
|
);
|
|
if (!plain) throw new Error('the trip runs today');
|
|
const legs = [
|
|
{ from: 'SNA', to: 'ATL', dep_day_id: 5, dep_time: '14:00', arr_day_id: 5, arr_time: '21:11' },
|
|
{ from: 'ATL', to: 'ORF', dep_day_id: 5, dep_time: '22:40', arr_day_id: 6, arr_time: '00:31' },
|
|
];
|
|
const today = withBookings(
|
|
plain,
|
|
[
|
|
{ id: 1, trip_id: 1, title: 'Car', type: 'car', status: 'confirmed', reservation_time: '2026-11-09T13:00' },
|
|
{
|
|
id: 2,
|
|
trip_id: 1,
|
|
title: 'SNA → ATL → ORF',
|
|
type: 'flight',
|
|
status: 'confirmed',
|
|
reservation_time: '2026-11-09T14:00',
|
|
metadata: JSON.stringify({ legs }),
|
|
},
|
|
] as never,
|
|
'2026-11-09',
|
|
(t) => t
|
|
);
|
|
expect(today.stops.map((s) => s.name)).toEqual(['Car', 'SNA → ATL', 'ATL → ORF']);
|
|
// In the air on the first leg, between the legs, and on the second leg past midnight.
|
|
expect(tripDayState(today, 16 * 60 + 22, 'done')).toMatchObject({ nextName: 'SNA → ATL', done: 1, total: 3 });
|
|
expect(tripDayState(today, 21 * 60 + 30, 'done')).toMatchObject({ nextName: 'ATL → ORF', done: 2 });
|
|
expect(tripDayState(today, 23 * 60 + 30, 'done')).toMatchObject({ nextName: 'ATL → ORF', done: 2 });
|
|
});
|
|
|
|
it('lands a leg without an arrival day on the day it leaves', () => {
|
|
const plain = todayOf(
|
|
trip(1, '2026-11-09', '2026-11-09'),
|
|
[day(5, 1, '2026-11-09', [])],
|
|
'2026-11-09',
|
|
() => 'D',
|
|
(t) => t
|
|
);
|
|
if (!plain) throw new Error('the trip runs today');
|
|
const legs = [
|
|
{ from: 'SNA', to: 'ATL', dep_day_id: 5, dep_time: '08:00', arr_time: '10:00' },
|
|
{ from: 'ATL', to: 'ORF', dep_day_id: 5, dep_time: '12:00', arr_time: '14:00' },
|
|
];
|
|
const today = withBookings(
|
|
plain,
|
|
[
|
|
{ id: 2, trip_id: 1, title: 'Hop', type: 'flight', status: 'confirmed', metadata: JSON.stringify({ legs }) },
|
|
] as never,
|
|
'2026-11-09',
|
|
(t) => t
|
|
);
|
|
expect(tripDayState(today, 15 * 60, 'done')).toMatchObject({ nextName: 'done', done: 2 });
|
|
});
|
|
|
|
it('counts a booking with an end time as under way until it ends', () => {
|
|
const plain = todayOf(
|
|
trip(1, '2026-11-09', '2026-11-09'),
|
|
[day(5, 1, '2026-11-09', [])],
|
|
'2026-11-09',
|
|
() => 'D',
|
|
(t) => t
|
|
);
|
|
if (!plain) throw new Error('the trip runs today');
|
|
const today = withBookings(
|
|
plain,
|
|
[
|
|
{
|
|
id: 1,
|
|
trip_id: 1,
|
|
title: 'Tour',
|
|
type: 'tour',
|
|
status: 'confirmed',
|
|
reservation_time: '2026-11-09T10:00',
|
|
reservation_end_time: '2026-11-09T13:00',
|
|
},
|
|
] as never,
|
|
'2026-11-09',
|
|
(t) => t
|
|
);
|
|
expect(tripDayState(today, 11 * 60, 'done')).toMatchObject({ nextName: 'Tour', done: 0 });
|
|
expect(tripDayState(today, 13 * 60 + 1, 'done')).toMatchObject({ nextName: 'done', done: 1 });
|
|
});
|
|
|
|
it('carries the extras and leaves out what is empty', () => {
|
|
const snapshot = buildSnapshot(
|
|
{ ...trip(1, '2026-11-06', '2026-11-07'), day_count: 2 } as Trip,
|
|
{
|
|
tripTitle: 'Trip 1',
|
|
path: '/trips/1',
|
|
dayLabel: 'D',
|
|
index: 1,
|
|
total: 2,
|
|
stops: [{ name: 'Tea', time: '10:00', minutes: 600, color: '#2FA37A', symbol: 'leaf.fill' }],
|
|
},
|
|
labels,
|
|
{
|
|
place: 'Kyoto, Japan',
|
|
cover: null,
|
|
placeCount: 12,
|
|
memberCount: 0,
|
|
weather: { temp: 18, main: 'Clear', label: 'Sunny' },
|
|
bookings: [],
|
|
todos: { open: 1, total: 2, items: [], path: '/trips/1?tab=listen' },
|
|
packing: null,
|
|
}
|
|
);
|
|
expect(snapshot.trip).toEqual({
|
|
title: 'Trip 1',
|
|
start: '2026-11-06',
|
|
end: '2026-11-07',
|
|
path: '/trips/1',
|
|
place: 'Kyoto, Japan',
|
|
dayCount: 2,
|
|
placeCount: 12,
|
|
});
|
|
expect(snapshot.today).toEqual({
|
|
dayLabel: 'D',
|
|
index: 1,
|
|
total: 2,
|
|
stops: [{ name: 'Tea', time: '10:00', minutes: 600, color: '#2FA37A', symbol: 'leaf.fill' }],
|
|
});
|
|
expect(snapshot.weather).toEqual({ temp: 18, main: 'Clear', label: 'Sunny' });
|
|
expect(snapshot.bookings).toEqual([]);
|
|
expect(snapshot.todos?.open).toBe(1);
|
|
expect(snapshot).not.toHaveProperty('packing');
|
|
});
|
|
});
|
|
|
|
describe('widgets set to a trip', () => {
|
|
it('offers the trips running or ahead, in the dashboard order, at most six', () => {
|
|
const trips = [
|
|
trip(1, '2026-01-01', '2026-01-05'),
|
|
trip(2, '2026-12-01', '2026-12-05'),
|
|
trip(3, '2026-11-04', '2026-11-12'),
|
|
trip(4, '2026-11-20', '2026-11-22', 1),
|
|
trip(5, null, null),
|
|
...[6, 7, 8, 9, 10, 11].map((id) => trip(id, `2027-0${id - 5}-01`, `2027-0${id - 5}-03`)),
|
|
];
|
|
expect(widgetTripChoices(trips, '2026-11-06').map((t) => t.id)).toEqual([3, 2, 6, 7, 8, 9]);
|
|
});
|
|
|
|
it('writes each trip under its own key and the list to pick from', async () => {
|
|
vi.mocked(TrekShell.setWidgetSnapshot).mockClear();
|
|
const snapshot = buildSnapshot(trip(2, '2026-12-01', '2026-12-05'), null, labels);
|
|
await publishWidgets(snapshot, null, [
|
|
{ id: 2, title: 'Trip 2', start: '2026-12-01', end: '2026-12-05', snapshot },
|
|
]);
|
|
const calls = vi.mocked(TrekShell.setWidgetSnapshot).mock.calls.map(([options]) => options);
|
|
expect(calls.map((c) => c.key)).toEqual([undefined, 'trip.2', 'trips']);
|
|
expect(JSON.parse(calls[2].json)).toEqual([{ id: 2, title: 'Trip 2', start: '2026-12-01', end: '2026-12-05' }]);
|
|
});
|
|
});
|