Release v2.5.9

This commit is contained in:
2026-08-05 21:05:43 +01:00
parent 38d82d2ac6
commit d3e059a878
10 changed files with 240 additions and 17 deletions
+136
View File
@@ -0,0 +1,136 @@
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const test = require('node:test');
const assert = require('node:assert/strict');
test('playlist refresh queues updates until the next slide transition', async () => {
const calls = {
showCurrent: 0,
logDebug: [],
scheduleSlideAdvance: []
};
const sandbox = {
window: null,
location: { origin: 'http://localhost', href: 'http://localhost/screen/test2' },
Date,
JSON,
Math,
Number,
String,
Boolean,
Array,
Object,
Promise,
setTimeout,
clearTimeout,
console,
currentPlaylistSignature: 'old-signature',
currentPlaylistFadeBetweenSlides: false,
currentPlaylistSkipUnavailableRtmp: false,
slug: 'test2',
pendingPlaylistUpdate: null,
slides: [{ id: 1, duration_seconds: 12 }],
lastRenderedSlide: { id: 1, duration_seconds: 12 },
index: 0,
timer: null,
slideExpiresAt: null,
pausedRemainingMs: null,
currentPlaylistEtag: '',
activeSlidesCacheKey: '',
activeSlidesCacheValue: [],
renderCacheViewportKey: '',
slideMarkupCache: Object.create(null),
templateLayoutCache: Object.create(null),
templateRenderPlanCache: Object.create(null),
getCurrentActiveSlides() {
return sandbox.slides;
},
getPlaylistRevision(data) {
return data.signature;
},
normalizeSlide(slide) {
return slide;
},
getActiveSlidesFrom(slideList) {
return slideList;
},
savePlaylistSnapshot() {},
markRefreshHealthy() {},
setOfflineBannerVisible() {},
scheduleRefreshRetry() {},
syncWebpagePreloads() {},
syncRtmpWarmups() {},
showCurrent() {
calls.showCurrent += 1;
},
sendCommandState() {},
scheduleSlideAdvance(delayMs) {
calls.scheduleSlideAdvance.push(delayMs);
},
logDebug(...args) {
calls.logDebug.push(args.join(' '));
}
};
sandbox.window = sandbox;
class XhrStub {
open(method, url) {
this.method = method;
this.url = url;
}
setRequestHeader() {}
getResponseHeader(name) {
return name === 'ETag' ? '"next-etag"' : '';
}
send() {
this.readyState = 4;
this.status = 200;
this.responseText = JSON.stringify({
signature: 'next-signature',
slides: [{ id: 1, duration_seconds: 12, disable_audio: false }],
playlist: { fade_between_slides: false, skip_unavailable_rtmp: false }
});
if (typeof this.onreadystatechange === 'function') {
this.onreadystatechange();
}
}
}
sandbox.XMLHttpRequest = XhrStub;
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playback.js');
const script = fs.readFileSync(scriptPath, 'utf8');
const prelude = `
var pendingPlaylistUpdate = null;
var slides = [{ id: 1, duration_seconds: 12 }];
var currentPlaylistSignature = 'old-signature';
var currentPlaylistFadeBetweenSlides = false;
var currentPlaylistSkipUnavailableRtmp = false;
var currentPlaylistEtag = '';
var lastRenderedSlide = { id: 1, duration_seconds: 12 };
var index = 0;
var timer = null;
var slideExpiresAt = null;
var pausedRemainingMs = null;
var activeSlidesCacheKey = '';
var activeSlidesCacheValue = [];
var renderCacheViewportKey = '';
var slideMarkupCache = Object.create(null);
var templateLayoutCache = Object.create(null);
var templateRenderPlanCache = Object.create(null);
`;
vm.runInNewContext(prelude + '\n' + script, sandbox, { filename: scriptPath });
await sandbox.refresh();
assert.equal(calls.showCurrent, 0);
assert.equal(sandbox.currentPlaylistSignature, 'old-signature');
assert.equal(sandbox.slides[0].disable_audio, undefined);
assert.equal(calls.logDebug.some((entry) => entry.includes('Unable to load screen playlist.')), false);
assert.equal(calls.logDebug.some((entry) => entry.includes('applying on next slide transition')), true);
});
+20 -2
View File
@@ -68,6 +68,22 @@ test('buildScreenPlaylist assembles slides, templates, and derived values', asyn
canvas_size_height: 1080,
canvas_width: 1920,
canvas_height: 1080
}, {
id: 102,
title: 'Live Feed',
template_id: 33,
content_json: '{"rtmpRegion":{"type":"rtmp","value":"rtmp://example/live"}}',
modified_at: '2026-08-03T00:00:01.000Z',
position: 2,
duration_seconds: 15,
use_video_duration: 0,
disable_audio: 1,
template_name: 'Template 33',
canvas_size_name: 'HD',
canvas_size_width: 1920,
canvas_size_height: 1080,
canvas_width: 1920,
canvas_height: 1080
}]];
}
if (sql.includes('FROM c_playlist_slide_schedule_rules')) {
@@ -77,11 +93,11 @@ test('buildScreenPlaylist assembles slides, templates, and derived values', asyn
]];
}
if (sql.includes('FROM c_templates st')) {
assert.deepEqual(params, [[33]]);
assert.deepEqual(params, [[33, 33]]);
return [[{ id: 33, name: 'Template 33', canvas_size_id: 4, canvas_size_width: 1920, canvas_size_height: 1080, background_image_path: '/media/bg.png', background_color: '#111111', modified_at: '2026-08-03T00:00:02.000Z' }]];
}
if (sql.includes('FROM c_template_regions')) {
assert.deepEqual(params, [[33]]);
assert.deepEqual(params, [[33, 33]]);
return [[{ id: 900, template_id: 33, region_key: 'textRegion', region_type: 'text', label: 'Text Region', font_family: 'Arial', x: 10, y: 20, width: 300, height: 200, z_index: 1, modified_at: '2026-08-03T00:00:03.000Z' }]];
}
throw new Error(`unexpected query: ${sql}`);
@@ -111,6 +127,8 @@ test('buildScreenPlaylist assembles slides, templates, and derived values', asyn
assert.equal(payload.slides[0].disable_audio, true);
assert.equal(payload.slides[0].content.videoRegion.disable_audio, true);
assert.equal(payload.slides[0].content.videoRegion.cache_bust, '2026-08-03T00:00:01.000Z');
assert.equal(payload.slides[1].disable_audio, true);
assert.equal(payload.slides[1].content.rtmpRegion.disable_audio, true);
assert.equal(typeof payload.slides[0].content.qrRegion.qr_preview, 'string');
assert.match(payload.slides[0].content.qrRegion.qr_preview, /^data:image\/svg\+xml/);
assert.equal(payload.slides[0].scheduleRules.length, 2);
+57
View File
@@ -0,0 +1,57 @@
const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const { buildPlaylistFormViewModel } = require('../src/web/routes/signage/playlists/form-view-model');
test('playlist rows show mute controls for RTMP slides', () => {
const model = buildPlaylistFormViewModel(
{ id: 22, name: 'Playlist 22' },
{
playlistSlides: [
{
id: 1,
playlist_id: 22,
slide_id: 101,
position: 1,
title: 'Video slide',
content_json: '{"videoRegion":{"type":"video","value":"/media/video.mp4"}}',
duration_seconds: 10,
use_video_duration: 0,
disable_audio: 1
},
{
id: 2,
playlist_id: 22,
slide_id: 102,
position: 2,
title: 'RTMP slide',
content_json: '{"rtmpRegion":{"type":"rtmp","value":"rtmp://example/live"}}',
duration_seconds: 10,
use_video_duration: 0,
disable_audio: 1
},
{
id: 3,
playlist_id: 22,
slide_id: 103,
position: 3,
title: 'Image slide',
content_json: '{"imageRegion":{"type":"image","value":"/media/image.png"}}',
duration_seconds: 10,
use_video_duration: 0,
disable_audio: 1
}
],
slides: []
},
'',
null,
{ isEdit: true }
);
assert.equal(model.playlistSlides[0].showMuteButton, true);
assert.equal(model.playlistSlides[1].showMuteButton, true);
assert.equal(model.playlistSlides[2].showMuteButton, false);
});