feat: add passenger information and settings components

- Introduced `PassengerInfo.tsx` for displaying trip details, including upcoming trips and fare information.
- Added `PassengerSettings.tsx` for managing user-defined important places and fare settings.
- Implemented validation functions for passenger settings in `passenger-settings.mjs`.
- Created utility functions for transliterating stop names in `transliteration.ts`.
- Developed weather hint functionality in `weather-hint.ts` to provide precipitation probabilities.
- Established a mock server for testing passenger-related features in `passenger-server.mjs`.
- Added comprehensive tests for passenger guide logic and transliteration functionality.
This commit is contained in:
2026-10-12 20:30:49 +03:00
parent 823ab14367
commit e239de45dd
34 changed files with 687 additions and 68 deletions

View File

@@ -8,6 +8,8 @@ const source = await readFile(new URL('../src/lib/storage.ts', import.meta.url),
// Settings used by the actual board; no external project is required for these tests.
const body = source.match(/export const defaultSettings: Settings = ([\s\S]*?);\nexport function/)[1];
const defaults = Function(`return (${body})`)();
// This fixture's device belongs to city 2; Polotsk tariffs must not leak into it.
defaults.fareByType = [];
const value = { state: 'paired', enabled: true, adminUrl: '', device: { id: 'test-device', name: 'Табло', cityId: 2, revision: 1, settings: defaults }, config: { configured: true, cityId: 2, cityName: 'Новополоцк', stopName: 'Остановка', defaultStopId: 1, refreshSeconds: 20, timeZone: 'Europe/Minsk' } };
test('managed configuration validation rejects damaged, mismatched or unsupported settings', () => {
assert.equal(isManagedState(value), true);
@@ -18,6 +20,27 @@ test('managed configuration validation rejects damaged, mismatched or unsupporte
assert.equal(isManagedState(legacy),true);assert.equal(hydrateManagedState(legacy).device.settings.nightMode,'auto');
assert.equal(isManagedState({...value,device:{...value.device,settings:{...defaults,nightWakeMinutes:5}}}),false);
});
test('managed transliteration settings accept legacy devices and reject malformed flags', () => {
const legacy = structuredClone(value); delete legacy.device.settings.journeyTranslitEnabled;
assert.equal(isManagedState(legacy), true);
assert.equal(hydrateManagedState(legacy).device.settings.journeyTranslitEnabled, false);
const enabled = { ...value, device: { ...value.device, settings: { ...defaults, journeyTranslitEnabled: true } } };
assert.equal(isManagedState(enabled), true);
assert.equal(hydrateManagedState(enabled).device.settings.journeyTranslitEnabled, true);
for (const flag of ['true', 1, null]) {
assert.equal(isManagedState({ ...value, device: { ...value.device, settings: { ...defaults, journeyTranslitEnabled: flag } } }), false);
}
});
test('managed passenger settings migrate without leaking another city tariff or accepting damaged arrays', () => {
const legacy = structuredClone(value);
for(const key of ['infoPlacesEnabled','infoFareEnabled','lastTripNoticeEnabled','weatherHintEnabled','places','fareRules','fareByType']) delete legacy.device.settings[key];
assert.equal(isManagedState(legacy),true);assert.deepEqual(hydrateManagedState(legacy).device.settings.fareByType,[]);
const polotsk={...legacy,device:{...legacy.device,cityId:1},config:{...legacy.config,cityId:1}};
assert.equal(hydrateManagedState(polotsk).device.settings.fareByType[0].price,'1,05 BYN'); assert.equal(isManagedState(polotsk),true);
for(const patch of [{places:null},{fareRules:[{routeId:1}]},{weatherHintEnabled:'true'},{fareByType:[{cityId:1,type:'bus',price:'1 BYN',payment:'Наличные',checkedAt:'2026-10-11'}]}]) {
assert.equal(isManagedState({...value,device:{...value.device,settings:{...defaults,...patch}}}),false);
}
});
test('pairing bridge keeps secrets in HttpOnly cookies, binds data to the individual device and rejects cross-site writes', async t => {
const tokenA = 'A'.repeat(43), tokenB = 'B'.repeat(43), calls = [];
let revoked = false;