weather_app/tests/components/storm-watch.test.js
hermes-explorigin 4616c297a9
All checks were successful
Test, Build & Deploy / test-and-build (push) Successful in 24s
Test, Build & Deploy / deploy (push) Successful in 18s
feat(storms): NWS alerts, SPC outlook and IEM storm reports
Adds a Storms tab driven by three keyless, CORS-enabled feeds:
- api.weather.gov/alerts/active?point=… for watches/warnings/advisories,
  with a banner above the content for tornado/severe/Extreme alerts
- SPC Day 1-3 categorical outlooks plus Day 1 tornado/hail/wind
  probabilities, resolved by point-in-polygon (nolyr GeoJSON)
- NWS Local Storm Reports via IEM (bounding box + radius cut), nearest first

New: src/lib/api/storm.js (pure helpers + cached fetchers),
src/lib/stores/storm.svelte.js (5-min polling, stale-response guard),
src/components/StormWatch.svelte. Router/nav gain a 'storms' view.
Tests: geometry/parsing (16), store (7), component render (3).
Docs: AGENTS.md storm section + API notes.
2026-10-17 18:10:46 +00:00

105 lines
3.9 KiB
JavaScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mount, unmount, tick } from 'svelte'
import StormWatch from '../../src/components/StormWatch.svelte'
import { storm } from '../../src/lib/stores/storm.svelte.js'
import { app } from '../../src/lib/stores/app.svelte.js'
const risk = (key, label, short, color) => ({ key, label, short, color, props: null })
describe('StormWatch component', () => {
let target
let instance
beforeEach(() => {
app.locations = [{ id: 'x', name: 'Home', lat: 36.3567, lon: -95.8146, isCurrent: true }]
app.selectedLocationId = 'x'
app.settings = { ...app.settings, units: 'imperial' }
app.forecastData = { timezone: 'America/Chicago' }
storm.updatedAt = Date.now()
target = document.createElement('div')
document.body.appendChild(target)
})
afterEach(async () => {
if (instance) await unmount(instance)
target?.remove()
instance = null
document.body.innerHTML = ''
})
it('renders alerts, the outlook and nearby reports', async () => {
storm.alerts = [{
id: 'a1', event: 'Tornado Warning', type: 'storm', severity: 'Extreme',
severityRank: 3, headline: 'Tornado Warning until 5:30 PM',
description: 'Take shelter now.', instruction: 'Go to an interior room.',
areaDesc: 'Rogers, OK', senderName: 'NWS Tulsa OK',
ends: '2026-10-17T22:30:00Z', expires: '2026-10-17T22:45:00Z',
}]
storm.outlook = {
day1: risk('SLGT', 'Slight risk', 'SLGT', '#ffff00'),
day2: risk('TSTM', 'General thunderstorms', 'TSTM', '#55bb55'),
day3: risk('NONE', 'No outlook', '—', '#7b8eb5'),
probabilities: { tornado: 5, wind: 15, hail: 30 },
issued: '2026-10-17T16:14:00Z',
}
storm.reports = [{
id: 'r1', type: 'HAIL', label: 'Hail', icon: '🧊', magnitude: '1.75',
city: 'Claremore', county: 'Rogers', state: 'OK', source: 'Trained Spotter',
remark: 'Quarter size hail on Highway 20', valid: '2026-10-17T21:00:00Z',
lat: 36.3, lon: -95.6, distanceKm: 12.4,
}]
instance = mount(StormWatch, { target })
await tick()
const text = target.textContent
expect(text).toContain('Storm Watch')
expect(text).toContain('Tornado Warning')
expect(text).toContain('Active alerts')
expect(text).toContain('Slight risk')
expect(text).toContain('SLGT')
expect(text).toContain('15%') // wind probability
expect(text).toContain('Claremore')
expect(text).toContain('Hail')
expect(text).toContain('Rogers Co.')
expect(text).toContain('Quarter size hail')
expect(target.querySelectorAll('.alert-item')).toHaveLength(1)
expect(target.querySelectorAll('.report-item')).toHaveLength(1)
expect(target.querySelectorAll('.outlook-day')).toHaveLength(3)
})
it('shows all-clear states when nothing is active', async () => {
storm.alerts = []
storm.reports = []
storm.outlook = {
day1: risk('NONE', 'No outlook', '—', '#7b8eb5'),
day2: risk('NONE', 'No outlook', '—', '#7b8eb5'),
day3: risk('NONE', 'No outlook', '—', '#7b8eb5'),
probabilities: { tornado: 0, wind: 0, hail: 0 },
issued: null,
}
instance = mount(StormWatch, { target })
await tick()
const text = target.textContent
expect(text).toContain('No active watches, warnings or advisories')
expect(text).toContain('No severe-weather area covers this location')
expect(text).toContain('No storm reports within')
})
it('renders the non-US note instead of an outlook', async () => {
app.locations = [{ id: 'y', name: 'London', lat: 51.5, lon: -0.12, isCurrent: true }]
app.selectedLocationId = 'y'
storm.alerts = []
storm.reports = []
storm.outlook = null
instance = mount(StormWatch, { target })
await tick()
expect(target.textContent).toContain('SPC outlooks cover the United States only')
expect(target.textContent).toContain('Local Storm Reports cover the United States only')
})
})