Files
ridebus-board/test/departure-features.test.mjs
Zakhar Timoshenko e3074555a7 feat: add service window component and related functionality
- Introduced ServiceWindow component to display the last departure today and the first tomorrow.
- Implemented cancellation notice parsing to handle route cancellations effectively.
- Enhanced departure display logic to include cancellation alerts and status updates.
- Created information blocks for displaying service information, weather, and QR codes.
- Added tests for departure features, information handling, and service window functionality.
- Updated fixtures for testing various scenarios including cancellations and long wait times.
2026-10-09 23:55:47 +03:00

78 lines
6.9 KiB
JavaScript
Raw Permalink 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 { normalizeAlerts, normalizeDepartures } from '../server/board.mjs';
import { parseCancellationNotice } from '../src/lib/cancellations.mjs';
import { displayDepartures, firstAvailable, nextSameRoute, longWait, readability } from '../src/lib/departures.ts';
import { defaultSettings } from '../src/lib/storage.ts';
const now = Date.parse('2026-10-09T12:00:00+03:00');
const stops = [{ id: 1, name: 'Швейная фабрика' }, { id: 2, name: 'Мариненко' }];
const routes = [{ id: 6, number: '6' }, { id: 1, number: '1' }];
const departure = (id, routeId, time, extra = {}) => ({ id, routeId, routeNumber: String(routeId), destination: 'Автовокзал', scheduledAt: `2026-10-09T${time}:00+03:00`, scheduledTime: time, ...extra });
const raw = { id: 1, city_id: 1, route_id: 6, alert_type: 'cancellation', message: '09.10.2026: не выполняются рейсы:\nШвейная фабрика — 12:04', start_time: '2026-10-09T00:00:00+03:00', end_time: '2026-10-10T00:00:00+03:00' };
const alerts = normalizeAlerts([raw], routes, 1, now, stops);
test('reviewed cancellation format binds date, route, exact stop and clock; generic prose never cancels every trip', () => {
assert.deepEqual(alerts[0].cancellations, [{ routeNumber: '6', stopId: 1, serviceDate: '2026-10-09', time: '12:04' }]);
const result = displayDepartures([departure('a', 6, '12:04'), departure('b', 1, '12:08'), departure('c', 6, '12:12')], alerts, 1, now, true);
assert.deepEqual(result.map(item => item.status), ['cancelled', 'scheduled', 'notice']);
assert.equal(firstAvailable(result).id, 'b');
const generic = normalizeAlerts([{ ...raw, message: 'Один из рейсов отменён, уточните информацию.' }], routes, 1, now, stops);
assert.equal(displayDepartures([departure('a', 6, '12:04')], generic, 1, now, true)[0].status, 'notice');
});
test('multi-route notices keep each route and stop separate; ambiguous platform names and malformed notices do not match', () => {
const message = '09.10.2026: не выполняются рейсы:\nМаршрут №6:\nШвейная фабрика — 12:04, 13:05\n\nМаршрут №1:\nМариненко — 12:10';
const targets = parseCancellationNotice(message, ['6', '1'], stops);
assert.equal(targets.length, 3); assert.equal(targets[2].routeNumber, '1'); assert.equal(targets[2].stopId, 2);
assert.deepEqual(parseCancellationNotice(message, ['6'], stops), []);
assert.deepEqual(parseCancellationNotice(raw.message, ['6'], [...stops, { id: 3, name: 'Швейная фабрика' }]), []);
assert.deepEqual(parseCancellationNotice(raw.message.replace('12:04', '25:04'), ['6'], stops), []);
assert.deepEqual(parseCancellationNotice(raw.message.replace('09.10.2026', '31.02.2026'), ['6'], stops), []);
});
test('matching an origin cancellation reaches downstream stops and retains the previous service date after midnight', () => {
const trip = departure('a', 6, '12:14', { originStopId: 1, originTime: '12:04', serviceDate: '2026-10-09' });
assert.equal(displayDepartures([trip], alerts, 2, now, true)[0].status, 'cancelled');
const overnight = departure('b', 6, '00:14', { scheduledAt: '2026-10-10T00:14:00+03:00', originStopId: 1, originTime: '23:54', serviceDate: '2026-10-09' });
const overnightAlerts = normalizeAlerts([{ ...raw, message: raw.message.replace('12:04', '23:54') }], routes, 1, now, stops);
assert.equal(displayDepartures([overnight], overnightAlerts, 2, now, true)[0].status, 'cancelled');
assert.equal(displayDepartures([{ ...overnight, serviceDate: '2026-10-10' }], overnightAlerts, 2, now, true)[0].status, 'notice');
});
test('expired, not-yet-active or stale cancellation data cannot remove a departure; ambiguous simultaneous trips remain available', () => {
const trips = [departure('a', 6, '12:04')];
assert.equal(displayDepartures(trips, alerts, 1, now, false)[0].status, 'scheduled');
assert.equal(displayDepartures(trips, [{ ...alerts[0], endAt: new Date(now).toISOString() }], 1, now, true)[0].status, 'scheduled');
assert.equal(displayDepartures(trips, [{ ...alerts[0], startAt: new Date(now + 1000).toISOString() }], 1, now, true)[0].status, 'scheduled');
const ambiguous = displayDepartures([...trips, departure('other-direction', 66, '12:04', { routeNumber: '6', destination: 'Калинина' })], [{ ...alerts[0], routeNumbers: ['6'] }], 1, now, true);
assert.deepEqual(ambiguous.map(item => item.status), ['notice', 'notice']);
assert.equal(firstAvailable([{ ...trips[0], status: 'cancelled' }]), undefined);
});
test('next departure uses the same route and destination, excludes duplicates and cancellations, and crosses midnight', () => {
const first = { ...departure('a', 6, '23:54'), status: 'scheduled' };
const next = { ...departure('d', 6, '00:40', { scheduledAt: '2026-10-10T00:40:00+03:00' }), status: 'scheduled' };
const list = [first, { ...first, id: 'same-clock' }, { ...first, id: 'cancelled', scheduledAt: '2026-10-10T00:10:00+03:00', status: 'cancelled' },
{ ...first, id: 'opposite', scheduledAt: '2026-10-10T00:20:00+03:00', destination: 'Калинина' }, next];
assert.equal(nextSameRoute(list, first), next);
assert.equal(nextSameRoute([first], first), undefined);
assert.equal(nextSameRoute(list, undefined), undefined);
});
test('long wait starts at 90 minutes, can be disabled, and turns off as an arrival approaches', () => {
const trip = departure('a', 6, '13:30');
assert.equal(longWait(defaultSettings, trip, now), true);
assert.equal(longWait(defaultSettings, trip, now + 1), false);
assert.equal(longWait(defaultSettings, trip, now + 60000), false);
assert.equal(longWait({ ...defaultSettings, longWaitEnabled: false }, trip, now), false);
assert.equal(longWait(defaultSettings, undefined, now), false);
});
test('viewing distance increases text and minimum row height, while street animation is calmer', () => {
const near = readability(defaultSettings), far = readability({ ...defaultSettings, viewingDistance: 'far' });
const street = readability({ ...defaultSettings, displayProfile: 'street', viewingDistance: 'far' });
assert.equal(near.scale, 1); assert.equal(near.rowHeight, 62);
assert.ok(far.scale > near.scale && far.rowHeight > near.rowHeight);
assert.ok(street.scale > far.scale && street.rowHeight > far.rowHeight); assert.equal(street.calm, true);
});
test('short live response preserves origin identity and normalizes one-digit hours for exact matching', () => {
const result = normalizeDepartures({ routes: [{ id: 6, route_number: '6', name: 'А — Б', arrivals: [{ scheduledTime: '9:04', trip: { departureTime: '8:54', startStopId: 1 } }] }] }, Date.parse('2026-10-09T09:00:00+03:00'));
assert.equal(result.departures[0].scheduledTime, '09:04'); assert.equal(result.departures[0].originTime, '08:54');
assert.equal(result.departures[0].originStopId, 1); assert.equal(result.departures[0].serviceDate, '2026-10-09');
});