Files
ridebus-board/test/fixtures/board-features.tsx
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

62 lines
7.2 KiB
TypeScript
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 { useState } from 'react';
import { createRoot } from 'react-dom/client';
import '@fontsource-variable/manrope';
import '../../src/styles.css';
import { DeparturesScreen } from '../../src/App';
import { defaultSettings } from '../../src/lib/storage';
import { displayDepartures, readability } from '../../src/lib/departures';
import { normalizeAlerts } from '../../server/board.mjs';
import { colorScheme } from '../../src/lib/presentation.mjs';
import type { BoardData, Departure, Settings } from '../../src/types';
import type { CSSProperties } from 'react';
const stop = { id: 1, name: 'Швейная фабрика', direction: '' };
function Fixture() {
const [scenario, setScenario] = useState('cancellation');
const [settings, setSettings] = useState<Settings>({ ...defaultSettings, rowCount: 6 });
const [paused, setPaused] = useState(true);
const [approaching, setApproaching] = useState(false);
const tomorrow = scenario === 'tomorrow';
const distant = tomorrow || scenario === 'today';
const baseNow = Date.parse(tomorrow ? '2026-10-09T23:20:00+03:00' : '2026-10-09T12:00:00+03:00');
const routeNumbers = ['6', '1', '6', '2тк', '1', '17тк', '21тк', '6'];
const minutes = distant ? [125, 140, 165, 185, 195, 210, 220, 240] : [4, 8, 12, 17, 23, 31, 38, 44];
const departures: Departure[] = minutes.map((minutes, index) => {
const at = tomorrow ? Date.parse('2026-10-10T05:40:00+03:00') + (minutes - 125) * 60000 : baseNow + minutes * 60000;
const number = routeNumbers[index];
const destination = number === '1' ? 'Богатырская' : number === '6' ? 'Автовокзал' : number === '2тк' ? 'Больница (Новка) (через Аэропорт)' : 'Тросницкая';
return { id: String(index), routeId: index === 3 ? 20 : Number.parseInt(number), routeNumber: number,
routeName: `Швейная фабрика — ${destination}`, destination, type: number.includes('тк') ? 'minibus' : 'bus', scheduledAt: new Date(at).toISOString(),
scheduledTime: new Intl.DateTimeFormat('ru', { timeZone: 'Europe/Minsk', hour: '2-digit', minute: '2-digit' }).format(at), note: '', source: 'schedule' };
});
const now = approaching ? Date.parse(departures[0].scheduledAt) - 10 * 60000 : baseNow;
const routes = [...new Map(departures.map(item => [item.routeId, { id: item.routeId, number: item.routeNumber, name: item.routeName, type: item.type }])).values()];
const all = scenario === 'all-cancelled';
const message = all ? `09.10.2026: не выполняются рейсы:\n${routes.map(route => `Маршрут №${route.number}:\nШвейная фабрика — ${departures.filter(item => item.routeId === route.id).map(item => item.scheduledTime).join(', ')}`).join('\n')}`
: '09.10.2026: не выполняются рейсы:\nШвейная фабрика — 12:04';
const alerts = distant ? [] : normalizeAlerts([{ id: 1, city_id: 1, route_numbers: all ? routes.map(item => item.number) : ['6'], alert_type: 'cancellation', title: 'Отмена отдельных рейсов', message, start_time: '2026-10-09T00:00:00+03:00', end_time: '2026-10-10T00:00:00+03:00' }], routes, 1, now, [stop]);
const data: BoardData = { cityId: 1, cityName: 'Полоцк', stop, departures, routes, alerts, alertsAvailable: true, partial: false, updatedAt: new Date(now).toISOString(), serviceDate: '2026-10-09', mode: 'demo' };
const reading = readability(settings);
const update = <K extends keyof Settings>(key: K, value: Settings[K]) => setSettings(previous => ({ ...previous, [key]: value }));
return <div className="board" data-theme={settings.theme} data-profile={settings.displayProfile} data-distance={settings.viewingDistance} data-row-count={settings.rowCount} data-motion={reading.calm ? 'calm' : 'full'} style={{ '--reading-scale': reading.scale, '--accent': colorScheme(settings.theme === 'light' ? settings.lightColorScheme : settings.colorScheme, settings.theme).color } as CSSProperties}>
<header className="board-header"><h1>{stop.name}</h1><div className="header-right"><div className="clock">{new Intl.DateTimeFormat('ru', { timeZone: 'Europe/Minsk', hour: '2-digit', minute: '2-digit' }).format(now)}</div></div></header>
<main className="scene-container"><div className="scene"><DeparturesScreen data={data} departures={displayDepartures(departures, alerts, 1, now, true)} now={now} alerts={alerts} loading={false} rowCount={settings.rowCount} fresh error="" paused={paused} settings={settings} /></div></main>
<footer className="board-footer" style={{ position: 'relative', minHeight: 48, paddingBlock: 12 }}>
<details><summary style={{ cursor: 'pointer', color: 'var(--muted)', fontSize: 14 }}>Тестовые данные · настройки проверки</summary>
<div style={{ position: 'absolute', bottom: 48, left: 0, right: 0, padding: 20, border: '1px solid var(--outline)', borderRadius: 16, background: 'var(--input)', zIndex: 10, display: 'flex', flexWrap: 'wrap', gap: 16 }}>
<label>Сценарий<select value={scenario} onChange={event => { setScenario(event.target.value); setApproaching(false); }}><option value="cancellation">Отмена</option><option value="all-cancelled">Все отменены</option><option value="today">Долгое ожидание сегодня</option><option value="tomorrow">Долгое ожидание завтра</option></select></label>
<label>Профиль<select value={settings.displayProfile} onChange={event => update('displayProfile', event.target.value as Settings['displayProfile'])}><option value="tv">Телевизор</option><option value="street">Уличное табло</option></select></label>
<label>Расстояние<select value={settings.viewingDistance} onChange={event => update('viewingDistance', event.target.value as Settings['viewingDistance'])}><option value="near">Близко</option><option value="medium">Средне</option><option value="far">Далеко</option></select></label>
<label>Тема<select value={settings.theme} onChange={event => update('theme', event.target.value as Settings['theme'])}><option value="dark">Тёмная</option><option value="light">Светлая</option></select></label>
<label>Строки<select value={settings.rowCount} onChange={event => update('rowCount', Number(event.target.value))}>{[2,4,6].map(value => <option key={value} value={value}>{value}</option>)}</select></label>
<label>Следующий рейс<input type="checkbox" checked={settings.showNextSameRoute} onChange={event => update('showNextSameRoute', event.target.checked)} /></label>
<label>Длительное ожидание<input type="checkbox" checked={settings.longWaitEnabled} onChange={event => update('longWaitEnabled', event.target.checked)} /></label>
<label>До рейса 10 минут<input type="checkbox" checked={approaching} onChange={event => setApproaching(event.target.checked)} /></label>
<label>Смена страниц<input type="checkbox" checked={!paused} onChange={event => setPaused(!event.target.checked)} /></label>
</div>
</details>
</footer>
</div>;
}
createRoot(document.getElementById('root')!).render(<Fixture />);