Files
weerscherm/tests/js/format.test.js
T
theoandClaude Opus 5 5d61b6ec90 feat: tijdlabels op hele uren in plaats van de ruwe meettijden
De labels toonden de werkelijke meettijdstippen van Netatmo (11:41,
17:41, 23:41), wat onrustig leest. Nu markeringen op hele uren die op de
klok zijn uitgelijnd: elke 6 uur vanaf middernacht, dus 00:00, 06:00,
12:00, 18:00, elk op zijn echte tijdspositie in het venster. "nu" blijft
op de rechterrand; een markering die daar te dicht bij komt valt weg.

axisMarks() is vervangen door hourTicks(), die uur voor uur door het
venster loopt en telkens de lokale klok raadpleegt. Daardoor blijven de
labels ook op de zomertijdwissel hele lokale uren, waar optellen in vaste
stappen er een uur naast zou zitten; daar is een test voor. Een label
vlak op de linkerrand wordt niet gecentreerd, zodat het binnen de
grafiek blijft. Servicewerker naar v9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:33:14 +02:00

247 lines
10 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, pinRoom, hourTicks,
} 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), '–');
});
test('pinRoom moves the matching room to the front, keeping the others in order', () => {
const rooms = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
assert.deepEqual(pinRoom(rooms, 'b').map((r) => r.id), ['b', 'a', 'c']);
assert.deepEqual(pinRoom(rooms, 'c').map((r) => r.id), ['c', 'a', 'b']);
});
test('pinRoom returns the original array unchanged without a match, id, or when already first', () => {
const rooms = [{ id: 'a' }, { id: 'b' }];
assert.equal(pinRoom(rooms, null), rooms);
assert.equal(pinRoom(rooms, undefined), rooms);
assert.equal(pinRoom(rooms, 'x'), rooms, 'onbekend id: geen match, ongewijzigd terug');
assert.equal(pinRoom(rooms, 'a'), rooms, 'al vooraan: geen kopie nodig');
});
test('pinRoom does not mutate the input array', () => {
const rooms = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
const original = rooms.slice();
pinRoom(rooms, 'c');
assert.deepEqual(rooms, original);
});
test('hourTicks puts marks on clock-aligned whole hours inside the window', () => {
const ticks = hourTicks('2026-09-12T11:41:00+02:00', '2026-09-13T11:41:00+02:00', TZ);
assert.deepEqual(ticks.map((t) => formatTime(t.at, TZ)), ['12:00', '18:00', '00:00', '06:00']);
assert.deepEqual(ticks.map((t) => Number(t.percent.toFixed(2))), [1.32, 26.32, 51.32, 76.32]);
});
test('hourTicks drops a mark that would collide with the "nu" label', () => {
// Venster eindigt om 12:05, dus de markering van 12:00 zit op 99,7% en valt weg.
const ticks = hourTicks('2026-09-12T12:05:00+02:00', '2026-09-13T12:05:00+02:00', TZ);
assert.deepEqual(ticks.map((t) => formatTime(t.at, TZ)), ['18:00', '00:00', '06:00']);
});
test('hourTicks stays on local whole hours across the daylight saving change', () => {
// 25 oktober 2026: de klok gaat om 03:00 terug naar 02:00, dus dat venster duurt 25 uur.
// Zonder de lokale klok te raadplegen zou 00:00 hier op 01:00 uitkomen.
const ticks = hourTicks('2026-10-24T12:00:00+02:00', '2026-10-25T12:00:00+01:00', TZ);
assert.deepEqual(ticks.map((t) => formatTime(t.at, TZ)), ['12:00', '18:00', '00:00', '06:00']);
assert.equal(ticks[0].percent, 0, 'een venster dat precies op een heel uur begint krijgt daar een markering');
});
test('hourTicks honours a different step', () => {
const ticks = hourTicks('2026-09-13T09:30:00+02:00', '2026-09-13T21:30:00+02:00', TZ, 3);
assert.deepEqual(ticks.map((t) => formatTime(t.at, TZ)), ['12:00', '15:00', '18:00']);
});
test('hourTicks gives nothing for an empty or invalid window', () => {
assert.deepEqual(hourTicks('2026-09-13T10:00:00+02:00', '2026-09-13T10:00:00+02:00', TZ), []);
assert.deepEqual(hourTicks('kapot', '2026-09-13T10:00:00+02:00', TZ), []);
assert.deepEqual(hourTicks(undefined, undefined, TZ), []);
});