Files
weerscherm/tests/js/format.test.js
T
theoandClaude Sonnet 5 8e90eb1129 feat: uuroverzicht per dag (temperatuur, regen, wind)
Tik op een dag in de verwachtingsstrook voor een uuroverzicht van
temperatuur, regen en wind (richting + snelheid), om de 2 uur, 12
punten per dag. Sluit na 20 seconden vanzelf of bij een tik op de
overlay zelf.

Backend: Open-Meteo levert hourly-data in dezelfde call als daily
(geen extra verzoek, dezelfde 60min-cache). Nieuwe HourlyForecastMapper
groepeert per kalenderdatum en bemonstert om het andere uur, robuust
voor de zomertijddag (23/25 uur). OpenMeteoClient::dailyForecast()
hernoemd naar forecast(), geeft nu {daily, hourly} terug. Elke dag in
de forecast-array krijgt een hourly-lijst.

Frontend: temperatuurlijn hergebruikt sparklinePath (nu ook als
sparklinePoints voor exacte uitlijning met de 12-koloms grid), regen
als kleine staafjes, windpijl (nieuw icoon) wijst waarheen de wind
waait. 20s-balk via CSS transition met reflow-restart. Servicewerker
naar v5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 10:18:11 +02:00

190 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
f1, fInt, fmm, markerPos, barHeights, wmoIcon, trendLabel,
formatDate, formatTime, dayAbbr, monthName, isOlderThan, msUntil,
} from '../../public/format.js';
const TZ = 'Europe/Berlin';
test('f1 uses a decimal comma and one decimal', () => {
assert.equal(f1(16.4), '16,4');
assert.equal(f1(0), '0,0');
assert.equal(f1(19.75), '19,8');
assert.equal(f1(null), '–');
assert.equal(f1(undefined), '–');
});
test('fInt rounds to whole degrees', () => {
assert.equal(fInt(18.3), '18');
assert.equal(fInt(9.5), '10');
assert.equal(fInt(null), '–');
});
test('fmm shows "0 mm" for zero', () => {
assert.equal(fmm(0), '0 mm');
assert.equal(fmm(0.4), '0,4 mm');
assert.equal(fmm(8.25), '8,3 mm');
assert.equal(fmm(null), '–');
});
test('markerPos interpolates and clamps', () => {
assert.ok(Math.abs(markerPos(16.4, 9.2, 19.8) - 67.92) < 0.01);
assert.equal(markerPos(5, 9.2, 19.8), 0);
assert.equal(markerPos(25, 9.2, 19.8), 100);
assert.equal(markerPos(10, 10, 10), 50);
assert.equal(markerPos(null, 1, 2), 50);
});
test('barHeights scales to the tallest bar with a 2px floor', () => {
const h = barHeights([1.2, 6.8, 0, 2.4]);
assert.ok(Math.abs(h[0] - 12.35) < 0.01);
assert.equal(h[1], 70);
assert.equal(h[2], 2);
assert.ok(Math.abs(h[3] - 24.7) < 0.01);
assert.deepEqual(barHeights([0, 0, 0, 0]), [2, 2, 2, 2]);
assert.deepEqual(barHeights([null, 0, undefined, 0]), [2, 2, 2, 2]);
assert.deepEqual(barHeights([null, 5]), [2, 70]);
});
test('wmoIcon maps every documented code', () => {
assert.equal(wmoIcon(0), 'sun');
assert.equal(wmoIcon(1), 'partly');
assert.equal(wmoIcon(2), 'partly');
assert.equal(wmoIcon(3), 'cloud');
assert.equal(wmoIcon(45), 'fog');
assert.equal(wmoIcon(48), 'fog');
assert.equal(wmoIcon(51), 'drizzle');
assert.equal(wmoIcon(57), 'drizzle');
assert.equal(wmoIcon(61), 'rain');
assert.equal(wmoIcon(67), 'rain');
assert.equal(wmoIcon(80), 'showers');
assert.equal(wmoIcon(82), 'showers');
assert.equal(wmoIcon(71), 'snow');
assert.equal(wmoIcon(77), 'snow');
assert.equal(wmoIcon(85), 'snow');
assert.equal(wmoIcon(86), 'snow');
assert.equal(wmoIcon(95), 'thunder');
assert.equal(wmoIcon(99), 'thunder');
assert.equal(wmoIcon(42), 'cloud');
assert.equal(wmoIcon(null), 'cloud');
});
test('trendLabel', () => {
assert.equal(trendLabel('up'), '↗ stijgend');
assert.equal(trendLabel('down'), '↘ dalend');
assert.equal(trendLabel('stable'), '→ stabiel');
assert.equal(trendLabel(null), '');
});
test('formatDate gives a capitalised Dutch date in the station timezone', () => {
assert.equal(formatDate(new Date('2026-09-11T12:32:05Z'), TZ), 'Vrijdag 11 september');
// 23:30 UTC is al de volgende dag in Berlijn
assert.equal(formatDate(new Date('2026-09-11T23:30:00Z'), TZ), 'Zaterdag 12 september');
});
test('formatTime uses HH:MM in the station timezone', () => {
assert.equal(formatTime('2026-09-11T06:10:00+02:00', TZ), '06:10');
assert.equal(formatTime('2026-09-11T22:05:00Z', TZ), '00:05');
assert.equal(formatTime(null, TZ), '–');
assert.equal(formatTime('kapot', TZ), '–');
});
test('dayAbbr gives two-letter Dutch weekday', () => {
assert.equal(dayAbbr('2026-09-12', TZ), 'Za');
assert.equal(dayAbbr('2026-09-13', TZ), 'Zo');
assert.equal(dayAbbr('2026-09-14', TZ), 'Ma');
assert.equal(dayAbbr('2026-09-15', TZ), 'Di');
assert.equal(dayAbbr('2026-09-16', TZ), 'Wo');
assert.equal(dayAbbr('2026-09-17', TZ), 'Do');
assert.equal(dayAbbr('2026-09-18', TZ), 'Vr');
});
test('monthName', () => {
assert.equal(monthName(new Date('2026-09-11T12:00:00Z'), TZ), 'September');
});
test('isOlderThan', () => {
const now = new Date('2026-09-11T12:32:00Z');
assert.equal(isOlderThan('2026-09-11T12:10:00Z', now, 30), false);
assert.equal(isOlderThan('2026-09-11T11:50:00Z', now, 30), true);
assert.equal(isOlderThan(null, now, 30), true);
});
test('msUntil computes the wait until the next local time', () => {
const now = new Date('2026-09-11T12:32:05.500Z'); // 14:32:05 in Berlijn
assert.equal(msUntil(now, TZ, 0), ((24 - 14) * 3600 - 32 * 60 - 5) * 1000 - 500);
assert.equal(msUntil(now, TZ, 4), ((24 - 14 + 4) * 3600 - 32 * 60 - 5) * 1000 - 500);
assert.equal(msUntil(now, TZ, 15), (28 * 60 - 5) * 1000 - 500);
assert.equal(msUntil(new Date('2026-09-11T22:00:00.000Z'), TZ, 0), 86400000, 'precies op het moment zelf: pas morgen weer');
});
import {
co2Level, fPct, fHpa, seriesRange, sparklinePath, sparklinePoints,
windArrowDeg, fKmh, formatHour, formatFullDate,
} from '../../public/format.js';
test('co2Level uses the Netatmo thresholds', () => {
assert.deepEqual(co2Level(612), { level: 'good', label: 'goed' });
assert.deepEqual(co2Level(1000), { level: 'good', label: 'goed' });
assert.deepEqual(co2Level(1001), { level: 'medium', label: 'matig' });
assert.deepEqual(co2Level(1500), { level: 'medium', label: 'matig' });
assert.deepEqual(co2Level(1501), { level: 'bad', label: 'slecht' });
assert.deepEqual(co2Level(null), { level: 'unknown', label: '–' });
});
test('fPct and fHpa', () => {
assert.equal(fPct(48), '48%');
assert.equal(fPct(47.6), '48%');
assert.equal(fPct(null), '–');
assert.equal(fHpa(1014.2), '1014 hPa');
assert.equal(fHpa(null), '–');
});
test('seriesRange ignores nulls', () => {
assert.deepEqual(seriesRange([1010, null, 1015.5, 1008]), { min: 1008, max: 1015.5 });
assert.equal(seriesRange([null, undefined]), null);
});
test('sparklinePath scales points into the box', () => {
assert.equal(sparklinePath([1000, 1010, 1005], 100, 50, 0), 'M0.0,50.0 L50.0,0.0 L100.0,25.0');
assert.equal(sparklinePath([1000, 1010], 100, 50, 5), 'M5.0,45.0 L95.0,5.0');
assert.equal(sparklinePath([1010, 1010, 1010], 100, 50, 0), 'M0.0,25.0 L50.0,25.0 L100.0,25.0', 'vlakke reeks in het midden');
assert.equal(sparklinePath([1000, null, 1010], 100, 50, 0), 'M0.0,50.0 L100.0,0.0', 'null-punt overgeslagen');
assert.equal(sparklinePath([1000], 100, 50), '');
assert.equal(sparklinePath([], 100, 50), '');
});
test('sparklinePoints gives x for every index and null y for unknown values', () => {
const points = sparklinePoints([1000, null, 1010], 100, 50, 0);
assert.deepEqual(points.map((p) => p.x), [0, 50, 100]);
assert.equal(points[0].y, 50);
assert.equal(points[1].y, null);
assert.equal(points[2].y, 0);
});
test('windArrowDeg rotates 180 degrees so the arrow points where the wind blows to', () => {
assert.equal(windArrowDeg(0), 180);
assert.equal(windArrowDeg(270), 90);
assert.equal(windArrowDeg(200), 20);
assert.equal(windArrowDeg(null), null);
});
test('fKmh rounds to whole km/u', () => {
assert.equal(fKmh(8.4), '8 km/u');
assert.equal(fKmh(8.6), '9 km/u');
assert.equal(fKmh(null), '–');
});
test('formatHour gives a two-digit hour in the station timezone', () => {
assert.equal(formatHour('2026-09-14T00:00:00+02:00', TZ), '00');
assert.equal(formatHour('2026-09-14T14:00:00+02:00', TZ), '14');
assert.equal(formatHour('2026-09-13T23:00:00Z', TZ), '01', '23:00 UTC is 01:00 de volgende dag in Berlijn');
assert.equal(formatHour(null, TZ), '–');
});
test('formatFullDate matches formatDate for the same calendar date', () => {
assert.equal(formatFullDate('2026-09-14', TZ), 'Maandag 14 september');
assert.equal(formatFullDate('kapot', TZ), '–');
});