import { describe, it, expect } from 'vitest' import { classifyAdvisoryEvent, nwsSeverity, advisoryId, normalizeAdvisories, } from '../../../src/lib/api/nws.js' import { mergeAdvisories } from '../../../src/lib/stores/notifications.svelte.js' // A realistic raw feature from /alerts/active const heat = { properties: { id: 'urn:oid:2.49.0.1.840.0.e6e2f1.001.2', event: 'Excessive Heat Warning', severity: 'Severe', headline: 'Excessive Heat Warning issued September 7 at 7:02PM CDT until September 8 at 7:00PM CDT by NWS Tulsa OK', areaDesc: 'Rogers; Tulsa; Creek', effective: '2026-09-07T19:02:00-05:00', expires: '2026-09-08T19:15:00-05:00', }, } const wind = { properties: { id: 'urn:oid:2.49.0.1.840.0.9ab.001.1', event: 'Wind Advisory', severity: 'Moderate', headline: 'Wind Advisory issued', areaDesc: 'Benton; Washington', effective: '2026-09-07T12:00:00-05:00', expires: '2026-09-07T20:00:00-05:00', }, } const flood = { properties: { id: 'urn:oid:2.49.0.1.840.0.7c.001.3', event: 'Flash Flood Warning', severity: 'Severe', headline: 'Flash Flood Warning issued', areaDesc: 'Rogers; Delaware', effective: '2026-09-08T01:00:00-05:00', expires: '2026-09-08T04:00:00-05:00', }, } const unknown = { properties: { id: 'urn:oid:2.49.0.1.840.0.xyz.001.9', event: 'Special Weather Statement', severity: 'Unknown', headline: 'Special Weather Statement issued', areaDesc: 'Rogers', effective: '2026-09-07T10:00:00-05:00', expires: '2026-09-07T13:00:00-05:00', }, } describe('classifyAdvisoryEvent', () => { it('maps NWS events to app alert types', () => { expect(classifyAdvisoryEvent('Excessive Heat Warning')).toBe('heat') expect(classifyAdvisoryEvent('Heat Advisory')).toBe('heat') expect(classifyAdvisoryEvent('Wind Advisory')).toBe('wind') expect(classifyAdvisoryEvent('High Wind Warning')).toBe('wind') expect(classifyAdvisoryEvent('Winter Storm Warning')).toBe('cold') expect(classifyAdvisoryEvent('Blizzard Warning')).toBe('cold') expect(classifyAdvisoryEvent('Severe Thunderstorm Warning')).toBe('storm') expect(classifyAdvisoryEvent('Tornado Warning')).toBe('storm') expect(classifyAdvisoryEvent('Flash Flood Warning')).toBe('precip') }) it('falls back to advisory type for unrecognized events', () => { expect(classifyAdvisoryEvent('Special Weather Statement')).toBe('advisory') expect(classifyAdvisoryEvent('Air Quality Alert')).toBe('advisory') }) }) describe('nwsSeverity', () => { it('maps NWS severities', () => { expect(nwsSeverity('Extreme')).toBe('danger') expect(nwsSeverity('Severe')).toBe('danger') expect(nwsSeverity('Moderate')).toBe('warning') expect(nwsSeverity('Minor')).toBe('info') expect(nwsSeverity('Unknown')).toBe('info') }) }) describe('advisoryId', () => { it('derives a stable short id from the URN', () => { const a = advisoryId('urn:oid:2.49.0.1.840.0.e6e2f1.001.2') const b = advisoryId('urn:oid:2.49.0.1.840.0.e6e2f1.001.2') expect(a).toBe(b) expect(a.length).toBeGreaterThan(0) }) it('handles missing ids', () => { expect(advisoryId('')).toBe('adv') expect(advisoryId(null)).toBe('adv') }) }) describe('normalizeAdvisories', () => { it('produces app-shaped alert entries tagged source nws', () => { const out = normalizeAdvisories({ features: [heat, wind, flood, unknown] }, 'America/Chicago') expect(out).toHaveLength(4) const h = out.find((a) => a.event === 'Excessive Heat Warning') expect(h.type).toBe('heat') expect(h.severity).toBe('danger') expect(h.source).toBe('nws') expect(h.id).toMatch(/^nws-/) expect(h.effectiveDate).toBe('2026-09-07') expect(h.expiresDate).toBe('2026-09-08') expect(h.icon).toBe('🔥') expect(h.message).toContain('Excessive Heat Warning issued') }) it('handles empty feed', () => { expect(normalizeAdvisories({ features: [] })).toEqual([]) expect(normalizeAdvisories(null)).toEqual([]) }) it('dedupes duplicate ids within the feed', () => { const out = normalizeAdvisories({ features: [heat, heat] }, 'UTC') expect(out).toHaveLength(1) }) }) describe('mergeAdvisories (dedup)', () => { const forecastAlerts = [ // derived heat for today and tomorrow (idx 0 and 1) { id: 'heat-0', type: 'heat', date: '2026-09-07', source: 'forecast' }, { id: 'heat-1', type: 'heat', date: '2026-09-08', source: 'forecast' }, { id: 'rain-0', type: 'precip', date: '2026-09-07', source: 'forecast' }, { id: 'wind-0', type: 'wind', date: '2026-09-07', source: 'forecast' }, ] const advisories = [ // NWS heat warning covering 9/7 only (effective..expires that one day) { id: 'nws-h', type: 'heat', source: 'nws', effectiveDate: '2026-09-07', expiresDate: '2026-09-07' }, { id: 'nws-flood', type: 'precip', source: 'nws', effectiveDate: '2026-09-08', expiresDate: '2026-09-08' }, ] it('drops the derived alert whose date sits under an advisory of the same type', () => { const merged = mergeAdvisories(forecastAlerts, advisories) // heat-0 (9/7) is under the 9/7 heat advisory -> gone // heat-1 (9/8) is NOT under the heat advisory (ends 9/7) -> kept const ids = merged.map((a) => a.id) expect(ids).not.toContain('heat-0') expect(ids).toContain('heat-1') expect(ids).toContain('nws-h') }) it('keeps derived alerts of types/flags without an overlapping advisory', () => { const merged = mergeAdvisories(forecastAlerts, advisories) // rain-0 is on 9/7 but flood advisory is 9/8 -> kept // wind has no advisory -> kept const ids = merged.map((a) => a.id) expect(ids).toContain('rain-0') expect(ids).toContain('wind-0') // flood advisory itself appended expect(ids).toContain('nws-flood') }) it('does not drop derived alerts when no advisory overlaps', () => { const merged = mergeAdvisories(forecastAlerts, []) expect(merged.map((a) => a.id)).toEqual(forecastAlerts.map((a) => a.id)) }) it('respects per-type toggles (disabled advisories are not appended)', () => { const merged = mergeAdvisories(forecastAlerts, advisories, { heat: false, precip: true, wind: true, cold: true, uv: true, storm: true, advisory: true }) const ids = merged.map((a) => a.id) expect(ids).not.toContain('nws-h') expect(ids).toContain('nws-flood') }) })