- New Settings > Data source toggle (Open-Meteo global / NWS US); NWS falls back to Open-Meteo when unavailable or outside its coverage. - src/lib/api/nws.js: /points -> forecast/hourly/grid normalizer producing the app's shared shape, with realistic air highs (NWS reports 104 for the day Open-Meteo over-forecasts at 116); heat-index/feels-like falls back to air temp since NWS returns apparentTemperature as null here. - NWS requires a User-Agent contact; email is a build-time Vite env (VITE_NWS_EMAIL), read from gitignored .env.local, never committed. - Details view now shows which provider served the data. - Fixes the identical UTC date-parse shift for NWS naive local times. - Tests (8) + recorded live fixtures for Rogers County, OK.
88 lines
3.4 KiB
JavaScript
88 lines
3.4 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import { readFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import {
|
|
compassToDeg,
|
|
parseWind,
|
|
shortForecastToWmo,
|
|
dateKey,
|
|
localTimeString,
|
|
normalizeNWS,
|
|
} from '../src/lib/api/nws.js'
|
|
|
|
const FIX = (name) => JSON.parse(readFileSync(path.join('tests/fixtures/nws', name), 'utf8'))
|
|
|
|
describe('nws helpers', () => {
|
|
it('shortForecastToWmo maps text to WMO codes', () => {
|
|
expect(shortForecastToWmo('Sunny')).toBe(0)
|
|
expect(shortForecastToWmo('Mostly Sunny')).toBe(2)
|
|
expect(shortForecastToWmo('Partly Cloudy')).toBe(2)
|
|
expect(shortForecastToWmo('Overcast')).toBe(3)
|
|
expect(shortForecastToWmo('Slight Chance Showers And Thunderstorms')).toBe(95)
|
|
expect(shortForecastToWmo('Heavy Rain')).toBe(65)
|
|
expect(shortForecastToWmo('Light Snow')).toBe(71)
|
|
expect(shortForecastToWmo('Areas of Fog')).toBe(45)
|
|
})
|
|
|
|
it('compassToDeg maps compass points to degrees', () => {
|
|
expect(compassToDeg('N')).toBe(0)
|
|
expect(compassToDeg('NE')).toBe(45)
|
|
expect(compassToDeg('SW')).toBe(225)
|
|
expect(compassToDeg('W')).toBe(270)
|
|
expect(compassToDeg(null)).toBeNull()
|
|
})
|
|
|
|
it('parseWind takes the max speed from a range string', () => {
|
|
expect(parseWind('5 to 10 mph')).toBe(10)
|
|
expect(parseWind('12 mph')).toBe(12)
|
|
expect(parseWind('gusts up to 20 mph')).toBe(20)
|
|
expect(parseWind(null)).toBeNull()
|
|
})
|
|
|
|
it('dateKey and localTimeString produce naive local times (no UTC shift)', () => {
|
|
const tz = 'America/Chicago'
|
|
expect(dateKey('2026-08-16T15:00:00-05:00', tz)).toBe('2026-08-16')
|
|
expect(localTimeString('2026-08-16T15:00:00-05:00', tz)).toBe('2026-08-16T15:00')
|
|
expect(localTimeString('2026-08-19T15:00:00-05:00', tz)).toMatch(/^2026-08-19T/)
|
|
})
|
|
})
|
|
|
|
describe('normalizeNWS (recorded live fixture for Rogers County, OK)', () => {
|
|
const points = FIX('points.json')
|
|
const forecast = FIX('forecast.json')
|
|
const hourly = FIX('hourly.json')
|
|
const grid = FIX('grid.json')
|
|
const timeZone = points.properties.timeZone
|
|
|
|
const data = normalizeNWS({ points, forecast, hourly, grid, timeZone, units: 'imperial' })
|
|
|
|
it('marks the source as nws', () => {
|
|
expect(data._source).toBe('nws')
|
|
})
|
|
|
|
it('normalizes air high (not heat index) for the 7-day, matching NWS values', () => {
|
|
// NWS daytime highs: Today 103, Mon 94, Wed 104 ... (NO 116 outlier)
|
|
expect(data.daily.time).toHaveLength(7)
|
|
expect(data.daily.temperature_2m_max[0]).toBe(103) // Today
|
|
expect(data.daily.temperature_2m_max[1]).toBe(94) // Monday
|
|
expect(data.daily.temperature_2m_max[3]).toBe(104) // Wednesday
|
|
expect(Math.max(...data.daily.temperature_2m_max)).toBeLessThan(115)
|
|
expect(data.daily.temperature_2m_min[0]).toBe(75)
|
|
expect(data.daily.precipitation_probability_max[0]).toBe(24)
|
|
})
|
|
|
|
it('builds a realistic current from the first hourly period', () => {
|
|
expect(data.current.temperature_2m).toBe(102)
|
|
expect(data.current.relative_humidity_2m).toBe(35)
|
|
expect(data.current.wind_direction_10m).toBe(225) // SW
|
|
expect(data.current.time).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)
|
|
})
|
|
|
|
it('normalizes to metric on request', () => {
|
|
const m = normalizeNWS({ points, forecast, hourly, grid, timeZone, units: 'metric' })
|
|
expect(m.current.temperature_2m).toBeCloseTo((102 - 32) * 5 / 9, 1)
|
|
expect(m.daily.temperature_2m_max[0]).toBeCloseTo((103 - 32) * 5 / 9, 1)
|
|
expect(m.current.wind_speed_10m).toBeCloseTo(10 * 1.609344, 1)
|
|
})
|
|
})
|