Release v2.14.1
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m53s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
lzstealth committed 2026-10-11 18:54:43 +01:00
1 parent 635e1a9942
commit c3299abaac
174 files changed
+4483 -1687

No files matched your search

+203
View File
@@ -18,6 +18,209 @@ function loadHtmlScript(scriptPath, sandbox) {
vm.runInNewContext(script, sandbox, { filename: scriptPath });
}
test('media preload cleanup releases unpromoted video elements on every early exit', () => {
for (const reason of ['empty', 'single', 'missing-slide', 'missing-renderer', 'missing-document']) {
const calls = [];
const sandbox = {
window: {},
mediaPreloadSignature: 'old-signature',
mediaPreloadElements: [{
tagName: 'VIDEO',
pause() { calls.push('pause'); },
removeAttribute(name) { calls.push('remove:' + name); },
load() { calls.push('load'); }
}],
document: {},
primeSlideMarkup() {}
};
const slides = reason === 'empty' ? [] : reason === 'single' ? [{ id: 1 }] : [{ id: 1 }, { id: 2 }];
if (reason === 'missing-renderer') delete sandbox.primeSlideMarkup;
if (reason === 'missing-document') delete sandbox.document;
loadScript(path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playlist.js'), sandbox);
sandbox.syncSlideMediaPreloads(slides, reason === 'missing-slide' ? 0.5 : 0);
assert.deepEqual(calls, ['pause', 'remove:src', 'load'], reason);
assert.equal(sandbox.mediaPreloadSignature, '');
assert.equal(sandbox.mediaPreloadElements.length, 0);
sandbox.syncSlideMediaPreloads([], 0);
assert.equal(calls.length, 3);
}
});
test('warmed video node is promoted into its slide instead of fetched again', () => {
const createdVideos = [];
let nextSource = '';
const holder = {
innerHTML: '',
querySelectorAll() {
return [{ tagName: 'VIDEO', getAttribute(name) { return name === 'data-pulse-video-src' ? nextSource : null; } }];
}
};
const sandbox = {
window: { innerWidth: 1920, innerHeight: 1080, location: { origin: 'http://localhost' } },
URL,
currentPlaylistSignature: 'playlist',
mediaPreloadSignature: '',
mediaPreloadElements: [],
primeSlideMarkup(slide) {
nextSource = '/media/slide-' + slide.id + '.mp4';
return '<video src="' + nextSource + '"></video>';
},
document: {
createElement(tag) {
if (tag !== 'video') return holder;
const attributes = new Map();
const video = {
tagName: 'VIDEO',
preload: '',
muted: false,
src: '',
loadCalls: 0,
attributes,
setAttribute(name, value) { attributes.set(name, String(value)); },
getAttribute(name) { return attributes.get(name) || null; },
removeAttribute(name) { attributes.delete(name); },
load() { this.loadCalls += 1; },
pause() {}
};
createdVideos.push(video);
return video;
}
}
};
loadScript(path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playlist.js'), sandbox);
const slides = [{ id: 1 }, { id: 2 }, { id: 3 }];
sandbox.syncSlideMediaPreloads(slides, 0);
const warmedVideo = createdVideos[0];
assert.equal(warmedVideo.src, 'http://localhost/media/slide-2.mp4');
let promotedNode = null;
const activeVideo = {
tagName: 'VIDEO',
src: 'http://localhost/media/slide-2.mp4',
attributes: [{ name: 'class', value: 'video' }, { name: 'data-pulse-video-src', value: '/media/slide-2.mp4' }],
getAttribute(name) { return name === 'data-pulse-video-src' ? '/media/slide-2.mp4' : null; },
parentNode: {
replaceChild(replacement, oldNode) {
assert.equal(oldNode, activeVideo);
promotedNode = replacement;
}
}
};
const root = { querySelectorAll() { return [activeVideo]; } };
assert.equal(sandbox.promotePreloadedVideoElements(root, 2), 1);
assert.equal(promotedNode, warmedVideo);
assert.equal(warmedVideo.loadCalls, 1);
sandbox.syncSlideMediaPreloads(slides, 1);
assert.equal(createdVideos[1].src, 'http://localhost/media/slide-3.mp4');
assert.notEqual(sandbox.mediaPreloadElements[0], warmedVideo);
});
test('media preloads next-slide images without creating duplicate video loads', () => {
const calls = [];
const nextImage = {};
Object.defineProperty(nextImage, 'src', { set(value) { calls.push(value); } });
const holder = {
innerHTML: '',
querySelectorAll(selector) {
assert.equal(selector, 'img[src], video[data-pulse-video-src]');
return [{ tagName: 'IMG', getAttribute() { return '/media/next.jpg'; } }];
}
};
const sandbox = {
window: { innerWidth: 1920, innerHeight: 1080, location: { origin: 'http://localhost' } },
URL,
currentPlaylistSignature: 'playlist',
mediaPreloadSignature: '',
mediaPreloadElements: [],
primeSlideMarkup() { return '<img src="/media/next.jpg"><video src="/media/next.mp4"></video>'; },
Image: function () { return nextImage; },
document: {
createElement() { return holder; }
}
};
loadScript(path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playlist.js'), sandbox);
const slides = [{ id: 1 }, { id: 2 }];
sandbox.syncSlideMediaPreloads(slides, 0);
assert.deepEqual(calls, ['http://localhost/media/next.jpg']);
assert.equal(sandbox.mediaPreloadElements[0], nextImage);
sandbox.syncSlideMediaPreloads(slides, 0);
assert.equal(calls.length, 1);
});
test('media preloader keeps a warmed next-slide video node for promotion', () => {
const preloadedVideo = {
tagName: 'VIDEO',
loadCalls: 0,
load() { this.loadCalls += 1; }
};
const sourceVideo = { tagName: 'VIDEO', getAttribute() { return '/media/next.mp4'; } };
const holder = {
innerHTML: '',
querySelectorAll(selector) {
assert.equal(selector, 'img[src], video[data-pulse-video-src]');
return [sourceVideo];
}
};
const sandbox = {
window: { innerWidth: 1920, innerHeight: 1080, location: { origin: 'http://localhost' } },
URL,
currentPlaylistSignature: 'playlist',
mediaPreloadSignature: '',
mediaPreloadElements: [],
primeSlideMarkup() { return '<video src="/media/next.mp4"></video>'; },
document: {
createElement(tag) { return tag === 'video' ? preloadedVideo : holder; }
}
};
loadScript(path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playlist.js'), sandbox);
sandbox.syncSlideMediaPreloads([{ id: 1 }, { id: 2 }], 0);
assert.equal(preloadedVideo.src, 'http://localhost/media/next.mp4');
assert.equal(preloadedVideo.preload, 'auto');
assert.equal(preloadedVideo.loadCalls, 1);
assert.equal(sandbox.mediaPreloadElements[0], preloadedVideo);
assert.equal(preloadedVideo.__pulsePreloadSlideId, '2');
});
test('offline playlist snapshots preserve and apply the RTMP skip option', () => {
const values = new Map();
const sandbox = {
window: { localStorage: {
setItem(key, value) { values.set(key, value); },
getItem(key) { return values.get(key) || null; }
} },
playlistSnapshotStorageKey: 'snapshot',
normalizeSlide(slide) { return slide; },
slides: [],
currentPlaylistSignature: '',
currentPlaylistFadeBetweenSlides: false,
currentPlaylistSkipUnavailableRtmp: false,
currentPlaylistEtag: '',
pendingPlaylistUpdate: null,
activeSlidesCacheKey: '', activeSlidesCacheValue: [],
slideMarkupCache: {}, templateLayoutCache: {}, templateRenderPlanCache: {},
renderCacheViewportKey: '', index: 0
};
loadScript(path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playlist.js'), sandbox);
for (const skipUnavailableRtmp of [true, false]) {
sandbox.savePlaylistSnapshot({
slides: [{ id: 1 }], signature: 'revision', fadeBetweenSlides: true,
skipUnavailableRtmp, etag: '"etag"'
});
const snapshot = sandbox.loadPlaylistSnapshot();
assert.equal(snapshot.skipUnavailableRtmp, skipUnavailableRtmp);
assert.equal(sandbox.applyPlaylistSnapshot(snapshot), true);
assert.equal(sandbox.currentPlaylistSkipUnavailableRtmp, skipUnavailableRtmp);
assert.equal(sandbox.currentPlaylistFadeBetweenSlides, true);
assert.equal(sandbox.currentPlaylistEtag, '"etag"');
}
values.set('snapshot', JSON.stringify({ slides: [{ id: 1 }] }));
assert.equal(sandbox.loadPlaylistSnapshot().skipUnavailableRtmp, false);
});
test('webpage preloading only targets the next slide', () => {
const sandbox = {
window: null,