Tik op een van de drie binnenmodules zet die bovenaan de lijst (de overige twee behouden hun onderlinge volgorde) en vergroot alleen het temperatuurgetal van 50 naar 92px. Nogmaals tikken op de bovenste (vastgezette) module zet 'm weer los. Puur client-side, geen nieuwe API-velden: state.pinnedRoomId + de pure functie pinRoom() (met tests) in format.js. De keuze overleeft de 5-minuten-verversing, reset bij een volledige herlaad. Servicewerker naar v6. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
211 lines
8.2 KiB
JavaScript
211 lines
8.2 KiB
JavaScript
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,
|
||
} 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);
|
||
});
|