- Added NightScreen component for displaying night mode information and alerts. - Introduced NextStops component to show upcoming stops during a journey. - Implemented logic for determining night mode activation based on schedule and user settings. - Created journey utility functions to manage onward journey data and stop information. - Updated tests to cover new night mode features and journey logic. - Added fixture for testing night mode scenarios with specific timestamps. - Removed QR code from night screen for a cleaner UI experience. - Enhanced admin settings to manage night mode configurations.
81 lines
5.5 KiB
JavaScript
81 lines
5.5 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { loadTimetable } from '../server/timetable.mjs';
|
||
import { isTimetable, timetableDepartures } from '../src/lib/timetable.mjs';
|
||
import { onwardJourney } from '../src/lib/journey.ts';
|
||
import { displayDepartures, firstAvailable } from '../src/lib/departures.ts';
|
||
import { readTimetable, writeTimetable } from '../src/lib/storage.ts';
|
||
|
||
const now = Date.parse('2026-10-08T11:00:00+03:00');
|
||
const route = { id: 37, route_number: '5', name: 'А — З', type: 'bus' };
|
||
const stops = ['А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ж', 'З'].map((name, i) => ({ stop_id: i + 1, name, sequence: i + 1, time_offset_minutes: i * 3 }));
|
||
async function bundle(trips = [{ departureTime: '12:00' }], rawStops = stops) {
|
||
return loadTimetable({ cityId: 1, cityName: 'Полоцк', stop: { id: 2, name: 'Б', routes: [route] }, now,
|
||
upstream: async path => path.includes('holidays') ? [] : path.endsWith('/stops') ? rawStops : [{ route_id: 37, day_type: 'weekday', trips }] });
|
||
}
|
||
const first = data => timetableDepartures(data, '2026-10-08')[0];
|
||
const journey = (data, departure = first(data), clock = now, cityId = 1, stopId = 2) => onwardJourney(data, departure, clock, cityId, stopId);
|
||
|
||
test('full timetable caches ordered route stops once and shows four downstream stops with time since boarding', async () => {
|
||
const data = await bundle(undefined, [...stops].reverse());
|
||
assert.equal(isTimetable(data, 1, 2), true);
|
||
assert.deepEqual(data.routes[0].stops.map(stop => stop.id), [1,2,3,4,5,6,7,8]);
|
||
assert.equal(first(data).endStopId, 8);
|
||
assert.deepEqual(journey(data), { stops: [{id:3,name:'В',minutes:3},{id:4,name:'Г',minutes:6},{id:5,name:'Д',minutes:9},{id:6,name:'Е',minutes:12}], more:2 });
|
||
});
|
||
test('partial journeys end at their actual terminal, and a terminal stop does not imply a return service', async () => {
|
||
const partial = await bundle([{departureTime:'12:00',startStopId:1,endStopId:3}]);
|
||
assert.deepEqual(journey(partial), {stops:[{id:3,name:'В',minutes:3}],more:0});
|
||
const terminal = await bundle([{departureTime:'12:00',startStopId:1,endStopId:2}]);
|
||
assert.deepEqual(journey(terminal), {stops:[],more:0});
|
||
});
|
||
test('reverse and shortened reverse trips show decreasing stop sequence with positive duration', async () => {
|
||
const data = await bundle([{departureTime:'12:00',startStopId:8,endStopId:1}]);
|
||
assert.equal(first(data).destination, 'А');
|
||
assert.deepEqual(journey(data), {stops:[{id:1,name:'А',minutes:3}],more:0});
|
||
});
|
||
test('after-midnight trips retain service-day and terminal identity for the diagram', async () => {
|
||
const data = await bundle([{departureTime:'23:59'}]);
|
||
const departure = timetableDepartures(data, '2026-10-09')[0];
|
||
assert.equal(departure.scheduledTime, '00:02');assert.equal(departure.serviceDate,'2026-10-08');
|
||
assert.equal(journey(data,departure,Date.parse('2026-10-09T00:00:00+03:00')).stops[0].name,'В');
|
||
});
|
||
test('missing and nonmonotonic offsets preserve names without inventing zero or negative travel times', async () => {
|
||
for (const changed of [null, -1, 'not-a-number', 1]) {
|
||
const data = await bundle(undefined, stops.map(stop => stop.stop_id === 4 ? {...stop,time_offset_minutes:changed} : stop));
|
||
assert.equal(isTimetable(data,1,2),true);
|
||
assert.deepEqual(journey(data).stops.map(stop => stop.minutes), [null,null,null,null]);
|
||
}
|
||
});
|
||
test('legacy caches remain valid; unknown endpoints, ambiguous stop identity, stale and foreign bundles hide the diagram', async () => {
|
||
const data = await bundle();
|
||
const legacy = structuredClone(data);delete legacy.routes[0].stops;delete legacy.routes[0].schedules[0].trips[0].endStopId;
|
||
assert.equal(isTimetable(legacy,1,2),true);assert.equal(journey(legacy),null);
|
||
assert.equal(journey(data,{...first(data),endStopId:999}),null);
|
||
assert.equal(journey(data,{...first(data),originStopId:6,endStopId:8}),null);
|
||
assert.equal(journey(data,first(data),now,2,2),null);
|
||
assert.equal(journey(data,first(data),now,1,3),null);
|
||
assert.equal(journey(data,first(data),now+7*86400000),null);
|
||
const ambiguous = await bundle(undefined, [...stops, {...stops[0],sequence:9}]);
|
||
assert.deepEqual(ambiguous.routes[0].stops, []);assert.equal(journey(ambiguous),null);
|
||
assert.equal(isTimetable({...data,routes:[{...data.routes[0],stops:[...data.routes[0].stops,data.routes[0].stops[0]]}]},1,2),false);
|
||
});
|
||
test('the diagram follows the next available trip after cancellation, including its different terminal', async () => {
|
||
const data = await bundle([{departureTime:'12:00',endStopId:8},{departureTime:'12:10',endStopId:3}]);
|
||
const departures = timetableDepartures(data,'2026-10-08');
|
||
const alerts = [{id:'cancel',type:'cancellation',routeIds:[37],routeNumbers:['5'],cancellations:[{routeNumber:'5',stopId:1,serviceDate:'2026-10-08',time:'12:00'}]}];
|
||
const displayed = displayDepartures(departures, alerts, 2, now, true);
|
||
const departure = firstAvailable(displayed);
|
||
assert.equal(departure.scheduledTime,'12:13');
|
||
assert.deepEqual(journey(data,departure),{stops:[{id:3,name:'В',minutes:3}],more:0});
|
||
});
|
||
test('offline round-trip preserves route topology and per-trip terminal metadata', async () => {
|
||
const values = new Map();
|
||
globalThis.localStorage = {getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)};
|
||
globalThis.indexedDB = {open:()=>{throw new Error('Unavailable');}};
|
||
const data = await bundle();await writeTimetable(data);
|
||
const saved = await readTimetable(1,2);
|
||
assert.deepEqual(journey(saved),journey(data));
|
||
assert.equal(await readTimetable(2,2),null);assert.equal(await readTimetable(1,3),null);
|
||
});
|