UNPKG

@fmidev/smartmet-alert-client

Version:

Web application for viewing weather and flood alerts

151 lines (137 loc) 5 kB
/** * Map snapshot regression scenarios. * * All scenarios share the same on-disk shape: one JSON file per scenario * at tests/fixtures/scenarios/scenario-<N>.json, containing * { meta: { id, label, source, description, sourceUrl?, updateTimeIso? }, * data: { weather_update_time, flood_update_time, * weather_finland_active_all, flood_finland_active_all } } * * meta.source is one of: * 'wfs' — manually generated FMI WFS test data (ids 1..21) * 'cap' — converted from https://alerts.fmi.fi/cap/feed/ (ids 22..28) * 'syke' — converted from the SYKE flood CAP feed (id 29) * * The conversion scripts (scripts/capToWfs.ts, scripts/sykeToWfs.ts, * scripts/build-cap-scenarios.ts) always write into this directory, so * rebuilding scenarios never requires touching this loader. * * For each scenario we derive a deterministic `currentTime` from the * captured update_time so the 5-day window aligns with the warnings that * were active when the snapshot was taken. */ import { readdirSync, readFileSync, existsSync } from 'node:fs' import { resolve } from 'node:path' import type { GeoJSONFeatureCollection, WarningsData } from '@/types' export type MapScenarioSource = 'wfs' | 'cap' | 'syke' export interface MapScenario { /** Stable scenario id. 1..21 are legacy WFS captures; 22+ are derived. */ id: number /** Short label describing the scenario, for snapshot review UX */ label: string /** Where the data originated */ source: MapScenarioSource /** Free-form description shown in review artifacts and build logs */ description: string /** Optional URL the data was downloaded from (CAP/SYKE scenarios) */ sourceUrl?: string /** Shape AlertClient / processWarnings consume */ data: WarningsData /** Pinned "now" (ms since epoch) for deterministic day-bucketing */ currentTime: number /** ISO form of currentTime, useful for debugging / snapshot review */ currentTimeIso: string /** The update_time reported by the weather source, for reference */ weatherUpdateIso: string /** The update_time reported by the flood source, for reference */ floodUpdateIso: string } interface ScenarioEnvelope { meta: { id: number label: string source: MapScenarioSource description: string sourceUrl?: string updateTimeIso?: string } data: WarningsData } const SCENARIOS_DIR = resolve( process.cwd(), 'tests', 'fixtures', 'scenarios' ) function readUpdateTime(collection: GeoJSONFeatureCollection): string { const feature = collection.features[0] const updateTime = feature?.properties?.update_time if (typeof updateTime !== 'string' || updateTime.length === 0) { throw new Error('Missing update_time in fixture') } return updateTime } function envelopeToScenario(envelope: ScenarioEnvelope): MapScenario { const wd = envelope.data if (!wd.weather_update_time || !wd.flood_update_time) { throw new Error( `Scenario ${envelope.meta.id} is missing update_time collections` ) } const weatherUpdateIso = readUpdateTime(wd.weather_update_time) const floodUpdateIso = readUpdateTime(wd.flood_update_time) // Use the later of the two update times + 1 second so neither endpoint // is flagged "outdated" by useWarningsProcessor's maxUpdateDelay check. const currentTime = Math.max( new Date(weatherUpdateIso).getTime(), new Date(floodUpdateIso).getTime() ) + 1000 return { id: envelope.meta.id, label: envelope.meta.label, source: envelope.meta.source, description: envelope.meta.description, sourceUrl: envelope.meta.sourceUrl, data: wd, currentTime, currentTimeIso: new Date(currentTime).toISOString(), weatherUpdateIso, floodUpdateIso, } } function readEnvelope(file: string): ScenarioEnvelope { const payload = JSON.parse(readFileSync(file, 'utf8')) as ScenarioEnvelope if (!payload?.meta || !payload?.data) { throw new Error(`${file}: invalid scenario envelope`) } return payload } function scenarioPath(n: number): string { return resolve(SCENARIOS_DIR, `scenario-${n}.json`) } /** Load one scenario by id. Throws if no such file exists. */ export function loadScenario(n: number): MapScenario { const file = scenarioPath(n) if (!existsSync(file)) { throw new Error( `Scenario ${n} not found at tests/fixtures/scenarios/scenario-${n}.json` ) } return envelopeToScenario(readEnvelope(file)) } /** List scenario ids present on disk, in ascending order. */ export function listScenarioIds(): number[] { if (!existsSync(SCENARIOS_DIR)) return [] return readdirSync(SCENARIOS_DIR) .map((name) => { const m = name.match(/^scenario-(\d+)\.json$/) return m && m[1] ? Number(m[1]) : NaN }) .filter((n) => Number.isInteger(n)) .sort((a, b) => a - b) } /** Load all scenarios present on disk in id order. */ export function allScenarios(): MapScenario[] { return listScenarioIds().map((id) => loadScenario(id)) }