Release v2.17.3: roll weather forecasts forward at hour and day boundaries
This commit is contained in:
1 parent
5e23d16483
commit
bc8075379a
13 files changed
+263
-10
No files matched your search
@@ -5,6 +5,124 @@ const vm = require('node:vm');
|
||||
|
||||
const { convertWeatherSnapshot, normalizeOpenMeteoSnapshot, normalizePirateWeatherSnapshot } = require('../src/data/weather-units');
|
||||
const placeholderUtils = require('../src/web/public/js/shared/placeholder-utils');
|
||||
const weatherData = require('../src/web/public/js/shared/weather-data');
|
||||
|
||||
test('hourly weather advances at the local hour boundary without refetching', () => {
|
||||
const snapshot = { hourly: { time: ['2026-10-23T02:00', '2026-10-23T03:00', '2026-10-23T04:00'], temperature_2m: [12, 13, 14], weather_code: [61, 3, 0], temperature_unit: '\u00b0C' } };
|
||||
const before = weatherData.filterHourlySnapshot(snapshot, 'Europe/London', Date.parse('2026-10-23T01:59:59.999Z'));
|
||||
assert.deepEqual(before.hourly.temperature_2m, [12, 13, 14]);
|
||||
const after = weatherData.filterHourlySnapshot(snapshot, 'Europe/London', Date.parse('2026-10-23T02:00:00Z'));
|
||||
assert.deepEqual(after.hourly.time, ['2026-10-23T03:00', '2026-10-23T04:00']);
|
||||
assert.deepEqual(after.hourly.temperature_2m, [13, 14]);
|
||||
assert.deepEqual(after.hourly.weather_code, [3, 0]);
|
||||
assert.equal(after.hourly.temperature_unit, '\u00b0C');
|
||||
assert.equal(snapshot.hourly.time.length, 3);
|
||||
assert.deepEqual(weatherData.filterHourlySnapshot(snapshot, 'Europe/London', Date.parse('2026-10-24T00:00Z')).hourly.time, []);
|
||||
});
|
||||
|
||||
test('hourly weather handles UTC provider timestamps and fractional-offset locations', () => {
|
||||
const snapshot = { hourly: { time: ['2026-10-23T02:00:00.000Z', '2026-10-23T03:00:00.000Z'], temperature_2m: [12, 13] } };
|
||||
assert.deepEqual(weatherData.filterHourlySnapshot(snapshot, 'Asia/Kathmandu', Date.parse('2026-10-23T03:00Z')).hourly.temperature_2m, [13]);
|
||||
const local = { hourly: { time: ['2026-10-23T02:00', '2026-10-23T03:00'], temperature_2m: [12, 13] } };
|
||||
assert.deepEqual(weatherData.filterHourlySnapshot(local, 'Asia/Kathmandu', Date.parse('2026-10-22T21:15Z')).hourly.temperature_2m, [13]);
|
||||
});
|
||||
|
||||
test('daily weather advances at local midnight and keeps forecast columns aligned', () => {
|
||||
const snapshot = { daily: { time: ['2026-10-23', '2026-10-24', '2026-10-25'], temperature_2m_max: [12, 13, 14], weather_code: [61, 3, 0], temperature_unit: '\u00b0C' } };
|
||||
const before = weatherData.filterDailySnapshot(snapshot, 'Europe/London', Date.parse('2026-10-23T22:59:59.999Z'));
|
||||
assert.deepEqual(before.daily.temperature_2m_max, [12, 13, 14]);
|
||||
const after = weatherData.filterDailySnapshot(snapshot, 'Europe/London', Date.parse('2026-10-23T23:00:00Z'));
|
||||
assert.deepEqual(after.daily.time, ['2026-10-24', '2026-10-25']);
|
||||
assert.deepEqual(after.daily.temperature_2m_max, [13, 14]);
|
||||
assert.deepEqual(after.daily.weather_code, [3, 0]);
|
||||
assert.equal(after.daily.temperature_unit, '\u00b0C');
|
||||
assert.equal(snapshot.daily.time.length, 3);
|
||||
assert.deepEqual(weatherData.filterDailySnapshot(snapshot, 'Europe/London', Date.parse('2026-10-26T00:00Z')).daily.time, []);
|
||||
});
|
||||
|
||||
test('daily weather interprets offset timestamps in the location timezone', () => {
|
||||
const snapshot = { daily: { time: ['2026-10-23T07:00:00Z', '2026-10-24T07:00:00Z'], temperature_2m_max: [12, 13] } };
|
||||
assert.deepEqual(weatherData.filterDailySnapshot(snapshot, 'America/Los_Angeles', Date.parse('2026-10-24T06:59:59Z')).daily.temperature_2m_max, [12, 13]);
|
||||
assert.deepEqual(weatherData.filterDailySnapshot(snapshot, 'America/Los_Angeles', Date.parse('2026-10-24T07:00Z')).daily.temperature_2m_max, [13]);
|
||||
});
|
||||
|
||||
for (const mode of ['hourly', 'daily']) test('weather previews and held player regions roll ' + mode + ' placeholders forward and clean up timers', () => {
|
||||
let now = Date.parse(mode === 'hourly' ? '2026-10-23T01:59:59.999Z' : '2026-10-23T22:59:59.999Z');
|
||||
const times = mode === 'hourly' ? ['2026-10-23T02:00', '2026-10-23T03:00'] : ['2026-10-23', '2026-10-24'];
|
||||
const field = mode === 'hourly' ? 'temperature_2m' : 'temperature_2m_max';
|
||||
const alias = mode === 'hourly' ? 'temp' : 'temp_max';
|
||||
const location = { id: 1, timezone: 'Europe/London', responseJson: { [mode]: { time: times, [field]: [12, 13], weather_code: [61, 3] } } };
|
||||
const content = { weather_location_id: 1, value: '<p>{{' + mode + '.0.time}}:{{' + mode + '.0.' + alias + '}}</p>{{' + mode + '.0.icon}}' };
|
||||
const region = { pixelWidth: 600, pixelHeight: 300, baseStyle: '' };
|
||||
const timers = new Map();
|
||||
let timerId = 0;
|
||||
class Clock extends Date { static now() { return now; } }
|
||||
const renderers = {};
|
||||
for (const target of ['editor', 'player']) {
|
||||
const sandbox = {
|
||||
Date: Clock,
|
||||
document: { addEventListener() {} },
|
||||
window: {
|
||||
placeholderUtils,
|
||||
pulseWeatherData: { filterForecastSnapshot(snapshot, timeZone) { return weatherData.filterForecastSnapshot(snapshot, timeZone, now); } },
|
||||
pulseRegionTypes: { register(_type, module) { renderers.editor = module; } },
|
||||
pulsePlayerRegionTypes: { register(_type, module) { renderers.player = module; } },
|
||||
setTimeout(callback, delay) { timers.set(++timerId, { callback, delay }); return timerId; },
|
||||
clearTimeout(id) { timers.delete(id); }
|
||||
},
|
||||
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 before = '<p>' + times[0] + ':12</p>';
|
||||
const after = '<p>' + times[1] + ':13</p>';
|
||||
assert.ok(renderers.editor.renderPreview(region, content, { weatherLocations: [location] }).includes(before));
|
||||
const playerHtml = renderers.player.renderRegion(region, content);
|
||||
assert.ok(playerHtml.includes(before));
|
||||
assert.match(playerHtml, /data-weather-content=/);
|
||||
const output = { innerHTML: '' };
|
||||
const element = { isConnected: true, getAttribute() { return JSON.stringify(content); }, querySelector() { return output; } };
|
||||
const root = { querySelectorAll() { return [element]; } };
|
||||
renderers.player.initRegion(root);
|
||||
assert.ok(output.innerHTML.includes(before));
|
||||
assert.equal(timers.get(element.__weatherTimer).delay, 1);
|
||||
const timer = timers.get(element.__weatherTimer);
|
||||
timers.delete(element.__weatherTimer);
|
||||
now += 1;
|
||||
timer.callback();
|
||||
assert.ok(output.innerHTML.includes(after));
|
||||
assert.ok(!output.innerHTML.includes(before));
|
||||
assert.ok(renderers.editor.renderPreview(region, content, { weatherLocations: [location] }).includes(after));
|
||||
renderers.player.destroyRegion(root);
|
||||
assert.equal(timers.size, 0);
|
||||
now = Date.parse('2026-10-26T00:00Z');
|
||||
const expired = renderers.editor.renderPreview(region, content, { weatherLocations: [location] });
|
||||
assert.doesNotMatch(expired, /bi-cloud|bi-sun|bi-lightning/);
|
||||
});
|
||||
|
||||
for (const mode of ['hourly', 'daily']) test('weather thumbnails filter cached ' + mode + ' fields at capture time', () => {
|
||||
const { buildThumbnailPreviewPayload } = require('../src/web/lib/media/slide-thumbnails');
|
||||
const originalDateNow = Date.now;
|
||||
Date.now = () => Date.parse(mode === 'hourly' ? '2026-10-23T02:00Z' : '2026-10-23T23:00Z');
|
||||
try {
|
||||
const times = mode === 'hourly' ? ['2026-10-23T02:00', '2026-10-23T03:00'] : ['2026-10-23', '2026-10-24'];
|
||||
const field = mode === 'hourly' ? 'temperature_2m' : 'temperature_2m_max';
|
||||
const alias = mode === 'hourly' ? 'temp' : 'temp_max';
|
||||
const slide = {
|
||||
template: { regions: [{ region_key: 'weather', region_type: 'weather', width: 600, height: 300 }] },
|
||||
content: { weather: { type: 'weather', value: '<p>{{' + mode + '.0.time}}:{{' + mode + '.0.' + alias + '}}</p>' } }
|
||||
};
|
||||
const payload = buildThumbnailPreviewPayload(slide, { getItem() {
|
||||
return { timezone: 'Europe/London', [mode]: { time: times, [field]: [12, 13] } };
|
||||
} });
|
||||
assert.ok(payload.html.includes(times[1] + ':13'));
|
||||
assert.ok(!payload.html.includes(times[0] + ':12'));
|
||||
} finally {
|
||||
Date.now = originalDateNow;
|
||||
}
|
||||
});
|
||||
|
||||
test('Pirate Weather snapshots normalize into shared weather fields and selected units', () => {
|
||||
const time = Date.parse('2026-10-23T12:00:00Z') / 1000;
|
||||
|
||||
Reference in new issue
Block a user