Release v2.17.2: normalize weather providers and fix unit placeholders
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m7s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s

This commit is contained in:
lzstealth committed 2026-10-23 02:39:13 +01:00
1 parent 6ce70ad7cb
commit 5e23d16483
12 files changed
+296 -13

No files matched your search

+30
View File
@@ -39,4 +39,34 @@ test('weather forecast preview only shows the fetch hint without a snapshot', ()
assert.match(template, /id="weather-hourly-forecast" class="d-none"/);
assert.doesNotMatch(template, /Hourly forecast · next 24 hours/);
assert.match(template, /Daily and hourly forecasts will appear here after the first successful fetch\./);
});
test('Pirate Weather fetch persists normalized fields and source units for mixed-unit display', async () => {
const { convertWeatherSnapshot } = require('../src/data/weather-units');
const originalFetch = global.fetch;
let requestUrl;
global.fetch = async function (url) {
requestUrl = new URL(url);
return { ok: true, json: async function () {
return { timezone: 'Europe/London', currently: { time: 1792756800, temperature: 68, windSpeed: 10, precipIntensity: 1, icon: 'clear-day' } };
} };
};
try {
const pool = { query: async function () { return [[{ setting_key: 'weather.pirate_weather_api_key', setting_value: JSON.stringify('test-key') }]]; } };
const result = await fetchWeatherLocationForecast(pool, {
latitude: 51.5, longitude: -0.1, timezone: 'Europe/London', provider: 'pirate-weather',
temperature_unit: 'fahrenheit', wind_unit: 'kmh', precipitation_unit: 'mm'
});
assert.equal(requestUrl.searchParams.get('units'), 'us');
assert.equal(result.snapshot.current.temperature_2m, 68);
assert.equal(result.snapshot.current_units.wind_speed_10m, 'mph');
assert.equal(result.snapshot.current_units.precipitation, 'in');
assert.deepEqual(JSON.parse(result.responseJson), JSON.parse(JSON.stringify(result.snapshot)));
const display = convertWeatherSnapshot(JSON.parse(result.responseJson), { temperature: 'fahrenheit', wind: 'kmh', precipitation: 'mm' });
assert.equal(display.current.temperature_2m, 68);
assert.equal(display.current.wind_speed_10m, 16.1);
assert.equal(display.current.precipitation, 25.4);
} finally {
global.fetch = originalFetch;
}
});
+136 -1
View File
@@ -1,7 +1,142 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const { convertWeatherSnapshot } = require('../src/data/weather-units');
const { convertWeatherSnapshot, normalizeOpenMeteoSnapshot, normalizePirateWeatherSnapshot } = require('../src/data/weather-units');
const placeholderUtils = require('../src/web/public/js/shared/placeholder-utils');
test('Pirate Weather snapshots normalize into shared weather fields and selected units', () => {
const time = Date.parse('2026-10-23T12:00:00Z') / 1000;
const snapshot = {
timezone: 'Europe/London', flags: { units: 'si' },
currently: { time, temperature: 20, apparentTemperature: 18, humidity: 0.8, windSpeed: 10, precipIntensity: 2.54, cloudCover: 0.25, icon: 'partly-cloudy-day' },
hourly: { data: [{ time, temperature: 20, windSpeed: 10, precipIntensity: 2.54, precipProbability: 0.5, icon: 'rain' }, { time: time + 3600, icon: 'snow' }] },
daily: { data: [{ time, temperatureMax: 25, temperatureMin: 15, precipIntensity: 1, icon: 'rain', sunriseTime: time - 21600, sunsetTime: time + 21600 }] }
};
const normalized = normalizePirateWeatherSnapshot(snapshot);
assert.equal(normalized.current.temperature_2m, 20);
assert.equal(normalized.current.relative_humidity_2m, 80);
assert.equal(normalized.current.cloud_cover, 25);
assert.equal(normalized.current.weather_code, 2);
assert.equal(normalized.current.is_day, 1);
assert.equal(normalized.hourly.time[0], '2026-10-23T12:00:00.000Z');
assert.deepEqual(normalized.hourly.weather_code, [61, 71]);
assert.equal(normalized.hourly.precipitation_probability[0], 50);
assert.equal(normalized.daily.time[0], '2026-10-23');
assert.equal(normalized.daily.precipitation_sum[0], 24);
const converted = convertWeatherSnapshot(snapshot, { temperature: 'fahrenheit', wind: 'mph', precipitation: 'inch' });
assert.equal(converted.current.temperature_2m, 68);
assert.equal(converted.current.wind_speed_10m, 22.4);
assert.equal(converted.current.precipitation, 0.1);
assert.equal(converted.hourly.temperature_2m[1], null);
assert.equal(converted.hourly.wind_speed_10m[1], null);
assert.equal(converted.daily.temperature_2m_max[0], 77);
assert.equal(converted.current_units.temperature_2m, '\u00b0F');
assert.deepEqual(convertWeatherSnapshot(converted, { temperature: 'fahrenheit', wind: 'mph', precipitation: 'inch' }), converted);
assert.equal(snapshot.currently.temperature, 20);
});
test('Open-Meteo normalization preserves fields without mutating provider data', () => {
const snapshot = { current: { temperature_2m: 20 }, hourly: { temperature_2m: [21] }, daily: { temperature_2m_max: [25] }, current_units: { temperature_2m: '\u00b0C' } };
const normalized = normalizeOpenMeteoSnapshot(snapshot);
assert.deepEqual(normalized, snapshot);
normalized.current.temperature_2m = 99;
assert.equal(snapshot.current.temperature_2m, 20);
});
test('cached Pirate Weather native unit systems convert consistently with Open-Meteo', () => {
const target = { temperature: 'celsius', wind: 'kmh', precipitation: 'mm' };
const expected = convertWeatherSnapshot({
current: { temperature_2m: 20, wind_speed_10m: 36, precipitation: 25.4 },
current_units: { temperature_2m: '\u00b0C', wind_speed_10m: 'km/h', precipitation: 'mm' }
}, target).current;
for (const native of [
{ units: 'us', temperature: 68, windSpeed: 36 / 1.609344, precipIntensity: 1 },
{ units: 'si', temperature: 20, windSpeed: 10, precipIntensity: 25.4 },
{ units: 'ca', temperature: 20, windSpeed: 36, precipIntensity: 25.4 },
{ units: 'uk', temperature: 20, windSpeed: 36 / 1.609344, precipIntensity: 25.4 }
]) {
const snapshot = { flags: { units: native.units }, currently: native };
const converted = convertWeatherSnapshot(snapshot, target);
for (const field of ['temperature_2m', 'wind_speed_10m', 'precipitation']) assert.equal(converted.current[field], expected[field], native.units + ': ' + field);
assert.deepEqual(convertWeatherSnapshot(converted, target), converted);
}
});
test('Pirate Weather dates and daily rainfall respect timezone and daylight saving', () => {
const start = Date.parse('2026-10-24T23:00:00Z') / 1000;
const next = Date.parse('2026-10-26T00:00:00Z') / 1000;
const normalized = normalizePirateWeatherSnapshot({
timezone: 'Europe/London', flags: { units: 'si' }, currently: { time: start, icon: 'clear-night' },
daily: { data: [{ time: start, precipIntensity: 1 }, { time: next }] }
});
assert.equal(normalized.daily.precipitation_sum[0], 25);
assert.equal(normalized.current.is_day, 0);
const localDate = normalizePirateWeatherSnapshot({ timezone: 'America/Los_Angeles', daily: { data: [{ time: Date.parse('2026-10-23T00:00:00Z') / 1000 }] } });
assert.equal(localDate.daily.time[0], '2026-10-22');
});
test('Pirate Weather missing and unavailable values stay missing, with bounded forecasts', () => {
const time = Date.parse('2026-10-23T12:00:00Z') / 1000;
const snapshot = {
flags: { units: 'si' }, currently: { time, temperature: -999, icon: 'wind', cloudCover: 0.9 },
hourly: { data: [null, ...Array.from({ length: 50 }, (_, index) => ({ time: time + (index - 1) * 3600, temperature: null, windSpeed: index }))] },
daily: { data: [null, ...Array.from({ length: 8 }, (_, index) => ({ time: time + index * 86400 }))] }
};
const converted = convertWeatherSnapshot(snapshot, { temperature: 'fahrenheit' });
assert.equal(converted.current.temperature_2m, null);
assert.equal(converted.current.weather_code, 3);
assert.equal(converted.hourly.time.length, 24);
assert.equal(converted.daily.time.length, 7);
assert.equal(converted.hourly.time[0], '2026-10-23T12:00:00.000Z');
assert.ok(converted.hourly.temperature_2m.every(value => value === null));
assert.equal(converted.daily.wind_speed_10m_max[1], 129.6);
assert.equal(converted.daily.wind_speed_10m_max[6], null);
assert.equal(converted.daily.precipitation_sum[0], null);
});
test('global weather unit placeholders resolve in editor and player without root unit fields', () => {
for (const units of [
{ temperature_unit: 'celsius', wind_unit: 'kmh', precipitation_unit: 'mm', expected: '\u00b0C|km/h|mm' },
{ temperature_unit: 'fahrenheit', wind_unit: 'mph', precipitation_unit: 'inch', expected: '\u00b0F|mph|in' },
{ temperature_unit: 'celsius', wind_unit: 'ms', precipitation_unit: 'mm', expected: '\u00b0C|m/s|mm' }
]) {
const location = { id: 1, ...units, responseJson: { current: {}, hourly: {}, daily: {} } };
const region = { pixelWidth: 600, pixelHeight: 300, baseStyle: '' };
const content = { weather_location_id: 1, value: '<p>{{temp_unit}}|{{wind_unit}}|{{precip_unit}}</p><p>{{current.temperature_unit}}|{{current.wind_speed_unit}}|{{current.precipitation_unit}}</p>' };
for (const target of ['editor', 'player']) {
let renderer;
const registry = { register(_type, module) { renderer = module; } };
const sandbox = {
document: { addEventListener() {} },
window: { placeholderUtils, pulseRegionTypes: registry, pulsePlayerRegionTypes: registry },
initialData: { weatherLocations: [location] },
escapeHtml: value => String(value),
renderEditorJsContent: value => value
};
const script = target === 'editor' ? '../src/web/public/js/regions/type/weather.js' : '../src/player/regions/weather.js';
vm.runInNewContext(fs.readFileSync(require.resolve(script), 'utf8'), sandbox);
const html = target === 'editor'
? renderer.renderPreview(region, content, { weatherLocations: [location] })
: renderer.renderRegion(region, content);
assert.ok(html.includes('<p>' + units.expected + '</p><p>' + units.expected + '</p>'), target + ': ' + units.expected);
assert.doesNotMatch(html, /\{\{/);
}
}
});
test('thumbnail global weather unit aliases resolve to populated current unit fields', () => {
const source = fs.readFileSync(require.resolve('../src/web/lib/media/slide-thumbnails.js'), 'utf8');
const normalization = source.slice(source.indexOf('function normalizeWeatherExpression('), source.indexOf('function weatherIconForCode('));
const sandbox = {};
vm.runInNewContext(normalization, sandbox);
const data = { current: { temperature_unit: '\u00b0F', wind_speed_unit: 'mph', precipitation_unit: 'in' } };
for (const [expression, expected] of [['temp_unit', '\u00b0F'], ['wind_unit', 'mph'], ['precip_unit', 'in']]) {
const normalized = sandbox.normalizeWeatherExpression(expression);
assert.equal(placeholderUtils.resolvePlaceholderExpression(data, normalized), expected);
}
});
test('converts cached weather snapshot values to selected units', () => {
const snapshot = {