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); });