Files
ridebus-board/test/journey.test.mjs
Zakhar Timoshenko 5c69f6691d feat: implement night mode functionality with UI updates and journey tracking
- 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.
2026-10-11 05:04:00 +03:00

81 lines
5.5 KiB
JavaScript
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 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);
});