#!/bin/bash set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" run_node_test <<'JS' const weather = requireFromRoot('shell/plugins/panels/weather/Model.js') assertDeepEqual( weather.parseWeatherStatus('{"text":"☀","class":"sunny"}'), { label: '☀', klass: 'sunny' }, 'weather parses pill status JSON' ) assertDeepEqual(weather.parseWeatherStatus('{'), { label: '', klass: '' }, 'weather handles invalid pill status JSON') assertEqual(weather.roundedTemp('21.6'), '22', 'weather rounds temperatures') assertEqual(weather.roundedTemp('nope'), '', 'weather ignores invalid temperatures') assertEqual(weather.formatTemp(72, true), '72°F', 'weather formats imperial temperatures') assertEqual(weather.formatTemp(22, false), '22°C', 'weather formats metric temperatures') assertEqual(weather.shouldUseImperial('', 'en_US', ''), true, 'weather falls back to US locale for imperial units') assertEqual(weather.shouldUseImperial('', 'en_US', 'Denmark'), false, 'weather prefers reported metric country over US locale') assertEqual(weather.shouldUseImperial('', 'da_DK', 'United States of America'), true, 'weather prefers reported imperial country over metric locale') assertEqual(weather.shouldUseImperial('metric', 'en_US', 'United States of America'), false, 'weather metric override wins') assertEqual(weather.shouldUseImperial('imperial', 'da_DK', 'Denmark'), true, 'weather imperial override wins') assertEqual(weather.dayName('2026-05-25'), 'Monday', 'weather derives day names') const openMeteo = { daily: { time: ['2026-05-25', '2026-05-26', '2026-05-27', '2026-05-28', '2026-05-29'], temperature_2m_max: [20.1, 21.6, 18.2, 17.9, 22.4], temperature_2m_min: [12.2, 13.1, 10.8, 9.2, 11.5], weather_code: [0, 63, 95, 3, 1] } } assertDeepEqual( weather.openMeteoForecastDays(openMeteo, '2026-05-25').map(day => ({ date: day.date, maxtempC: day.maxtempC, mintempF: day.mintempF, code: day.openMeteoWeatherCode })), [ { date: '2026-05-26', maxtempC: '22', mintempF: '56', code: 63 }, { date: '2026-05-27', maxtempC: '18', mintempF: '51', code: 95 }, { date: '2026-05-28', maxtempC: '18', mintempF: '49', code: 3 } ], 'weather builds future Open-Meteo forecast days' ) const wttr = { weather: [ { date: '2026-05-25', maxtempC: '20', mintempC: '12' }, { date: '2026-05-26', maxtempC: '22', mintempC: '13' } ] } assertEqual(weather.buildForecastDays(wttr, {}, '2026-05-25')[0].date, '2026-05-26', 'weather falls back to wttr forecast') assertEqual(weather.bareTempForDay({ maxtempC: '22', mintempC: '13', maxtempF: '72', mintempF: '55' }, 'max', false), '22°', 'weather formats forecast metric highs') assertEqual(weather.bareTempForDay({ maxtempC: '22', mintempC: '13', maxtempF: '72', mintempF: '55' }, 'min', true), '55°', 'weather formats forecast imperial lows') assert(weather.dayIcon({ openMeteoWeatherCode: 95 }).length > 0, 'weather maps Open-Meteo weather icons') assertEqual( weather.dayIcon({ hourly: [{ time: '900', weatherCode: 113 }, { time: '1200', weatherCode: 389 }, { time: '1800', weatherCode: 116 }] }), weather.iconForCode(389, false), 'weather picks hourly forecast icon nearest noon' ) JS