Apply remaining changes
This commit is contained in:
@@ -37,6 +37,7 @@ test('playlist refresh queues updates until the next slide transition', async ()
|
||||
timer: null,
|
||||
slideExpiresAt: null,
|
||||
pausedRemainingMs: null,
|
||||
app: null,
|
||||
currentPlaylistEtag: '',
|
||||
activeSlidesCacheKey: '',
|
||||
activeSlidesCacheValue: [],
|
||||
@@ -117,6 +118,7 @@ test('playlist refresh queues updates until the next slide transition', async ()
|
||||
var timer = null;
|
||||
var slideExpiresAt = null;
|
||||
var pausedRemainingMs = null;
|
||||
var app = null;
|
||||
var activeSlidesCacheKey = '';
|
||||
var activeSlidesCacheValue = [];
|
||||
var renderCacheViewportKey = '';
|
||||
@@ -133,4 +135,273 @@ test('playlist refresh queues updates until the next slide transition', async ()
|
||||
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);
|
||||
});
|
||||
|
||||
test('single-slide playlists re-render the active slide instead of refreshing after a queued update', async () => {
|
||||
const calls = {
|
||||
showCurrent: 0,
|
||||
refresh: 0
|
||||
};
|
||||
const timers = [];
|
||||
|
||||
const sandbox = {
|
||||
window: null,
|
||||
location: { origin: 'http://localhost', href: 'http://localhost/screen/test2' },
|
||||
Date,
|
||||
JSON,
|
||||
Math,
|
||||
Number,
|
||||
String,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
Promise,
|
||||
setTimeout(fn) {
|
||||
timers.push(fn);
|
||||
return timers.length;
|
||||
},
|
||||
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() {},
|
||||
clearActiveSlidesCache() {},
|
||||
showCurrent() {},
|
||||
sendCommandState() {},
|
||||
refresh() {
|
||||
calls.refresh += 1;
|
||||
},
|
||||
logDebug() {}
|
||||
};
|
||||
sandbox.window = sandbox;
|
||||
|
||||
class XhrStub {
|
||||
open() {}
|
||||
|
||||
setRequestHeader() {}
|
||||
|
||||
getResponseHeader(name) {
|
||||
return name === 'ETag' ? '"next-etag"' : '';
|
||||
}
|
||||
|
||||
send() {
|
||||
this.readyState = 4;
|
||||
this.status = 200;
|
||||
this.responseText = JSON.stringify({
|
||||
signature: 'next-signature',
|
||||
slides: [{ id: 2, 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 commandsPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-commands.js');
|
||||
const playbackScript = fs.readFileSync(scriptPath, 'utf8');
|
||||
const commandsScript = fs.readFileSync(commandsPath, '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 app = 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' + playbackScript + '\n' + commandsScript, sandbox, { filename: scriptPath });
|
||||
sandbox.showCurrent = function () {
|
||||
calls.showCurrent += 1;
|
||||
};
|
||||
|
||||
sandbox.pendingPlaylistUpdate = {
|
||||
slides: [{ id: 2, duration_seconds: 12 }],
|
||||
signature: 'next-signature',
|
||||
fadeBetweenSlides: false,
|
||||
skipUnavailableRtmp: false
|
||||
};
|
||||
|
||||
sandbox.scheduleSlideAdvance(100);
|
||||
assert.equal(timers.length, 1);
|
||||
timers.shift()();
|
||||
|
||||
assert.equal(calls.refresh, 0);
|
||||
assert.equal(calls.showCurrent, 1);
|
||||
assert.equal(sandbox.currentPlaylistSignature, 'next-signature');
|
||||
assert.deepEqual(sandbox.slides, [{ id: 2, duration_seconds: 12 }]);
|
||||
});
|
||||
|
||||
test('removing the currently visible slide from a two-slide playlist applies the one-slide update immediately', async () => {
|
||||
const calls = {
|
||||
showCurrent: 0,
|
||||
logDebug: []
|
||||
};
|
||||
|
||||
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 }, { id: 2, duration_seconds: 12 }],
|
||||
lastRenderedSlide: { id: 2, duration_seconds: 12 },
|
||||
index: 1,
|
||||
timer: null,
|
||||
slideExpiresAt: null,
|
||||
pausedRemainingMs: null,
|
||||
app: 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() {},
|
||||
clearActiveSlidesCache() {},
|
||||
showCurrent() {
|
||||
calls.showCurrent += 1;
|
||||
},
|
||||
sendCommandState() {},
|
||||
refresh() {},
|
||||
logDebug(...args) {
|
||||
calls.logDebug.push(args.join(' '));
|
||||
}
|
||||
};
|
||||
sandbox.window = sandbox;
|
||||
|
||||
class XhrStub {
|
||||
open() {}
|
||||
|
||||
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 playbackScript = fs.readFileSync(scriptPath, 'utf8');
|
||||
const prelude = `
|
||||
var pendingPlaylistUpdate = null;
|
||||
var slides = [{ id: 1, duration_seconds: 12 }, { id: 2, duration_seconds: 12 }];
|
||||
var currentPlaylistSignature = 'old-signature';
|
||||
var currentPlaylistFadeBetweenSlides = false;
|
||||
var currentPlaylistSkipUnavailableRtmp = false;
|
||||
var currentPlaylistEtag = '';
|
||||
var lastRenderedSlide = { id: 2, duration_seconds: 12 };
|
||||
var index = 1;
|
||||
var timer = null;
|
||||
var slideExpiresAt = null;
|
||||
var pausedRemainingMs = null;
|
||||
var app = 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' + playbackScript, sandbox, { filename: scriptPath });
|
||||
|
||||
await sandbox.refresh();
|
||||
|
||||
assert.equal(sandbox.currentPlaylistSignature, 'next-signature');
|
||||
assert.equal(sandbox.pendingPlaylistUpdate, null);
|
||||
assert.deepEqual(sandbox.slides, [{ id: 1, duration_seconds: 12, disable_audio: false }]);
|
||||
assert.equal(calls.logDebug.some((entry) => entry.includes('applying on next slide transition')), false);
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const registerPlaylistRoutes = require('../src/web/routes/admin/playlists');
|
||||
|
||||
function createAppAndHandlers() {
|
||||
const handlers = {};
|
||||
const app = {
|
||||
post(path, ...routeHandlers) {
|
||||
handlers[path] = routeHandlers;
|
||||
},
|
||||
get() {}
|
||||
};
|
||||
|
||||
return { app, handlers };
|
||||
}
|
||||
|
||||
function createResponse() {
|
||||
return {
|
||||
redirectedTo: '',
|
||||
statusCode: 200,
|
||||
redirect(url) {
|
||||
this.redirectedTo = url;
|
||||
},
|
||||
status(code) {
|
||||
this.statusCode = code;
|
||||
return this;
|
||||
},
|
||||
send() {
|
||||
return this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test('playlist create and update redirect to the canonical edit path', async () => {
|
||||
const calls = [];
|
||||
const connection = {
|
||||
async beginTransaction() {},
|
||||
async commit() {},
|
||||
async rollback() {},
|
||||
release() {},
|
||||
async query(sql) {
|
||||
if (sql.startsWith('INSERT INTO c_playlists')) {
|
||||
return [{ insertId: 4 }];
|
||||
}
|
||||
if (sql.startsWith('UPDATE c_playlists SET')) {
|
||||
calls.push('update-playlist');
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.startsWith('DELETE FROM c_playlist_slides')) {
|
||||
calls.push('delete-slides');
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
return [[]];
|
||||
}
|
||||
};
|
||||
|
||||
const { app, handlers } = createAppAndHandlers();
|
||||
registerPlaylistRoutes(app, {
|
||||
pool: {
|
||||
async getConnection() {
|
||||
return connection;
|
||||
}
|
||||
},
|
||||
common: {
|
||||
async fetchDuplicateName() { return null; },
|
||||
async fetchCanvasSizeById() { return { id: 7 }; },
|
||||
async fetchPlaylistById() { return { id: 4, canvas_id: 7 }; }
|
||||
},
|
||||
pages: {},
|
||||
fetchOrderedPlaylistSlides: async () => [],
|
||||
fetchScreensByPlaylistId: async () => [],
|
||||
fetchPlaylistCanvasId: async () => null,
|
||||
readArrayField: () => [],
|
||||
getAuditUserId: () => 12,
|
||||
redirectAfterSave(req, res, url) {
|
||||
calls.push({ kind: 'redirectAfterSave', url, path: req.path });
|
||||
res.redirect(url);
|
||||
},
|
||||
notifyPlayerScreens: async () => {},
|
||||
broadcastDashboardState: async () => {},
|
||||
getPlaylistDeleteBlockMessage: async () => '',
|
||||
requirePermission() {
|
||||
return function (_req, _res, next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const createHandlers = handlers['/playlists'];
|
||||
const updateHandlers = handlers['/playlists/:id'];
|
||||
assert.equal(Array.isArray(createHandlers), true);
|
||||
assert.equal(Array.isArray(updateHandlers), true);
|
||||
|
||||
const createRes = createResponse();
|
||||
await createHandlers[1]({
|
||||
path: '/playlists',
|
||||
body: {
|
||||
name: 'Lunch Loop',
|
||||
canvas_size_id: '7',
|
||||
slide_id: []
|
||||
}
|
||||
}, createRes, () => {});
|
||||
|
||||
const updateRes = createResponse();
|
||||
await updateHandlers[1]({
|
||||
path: '/playlists/4',
|
||||
params: { id: '4' },
|
||||
body: {
|
||||
name: 'Lunch Loop',
|
||||
slide_id: []
|
||||
}
|
||||
}, updateRes, () => {});
|
||||
|
||||
assert.equal(createRes.redirectedTo, '/playlists/4/edit');
|
||||
assert.equal(updateRes.redirectedTo, '/playlists/4/edit');
|
||||
assert.deepEqual(calls, [
|
||||
'delete-slides',
|
||||
{ kind: 'redirectAfterSave', url: '/playlists/4/edit', path: '/playlists' },
|
||||
'update-playlist',
|
||||
'delete-slides',
|
||||
{ kind: 'redirectAfterSave', url: '/playlists/4/edit', path: '/playlists/4' }
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user