Release v2.10.6
This commit is contained in:
@@ -33,3 +33,37 @@ test('playlist notifications use the player URL reported by the screen connectio
|
||||
command: 'refresh'
|
||||
}]);
|
||||
});
|
||||
|
||||
test('playlist notifications use the bridge device route for remote player connections', async () => {
|
||||
const calls = [];
|
||||
const notify = createNotifyPlayerScreens(
|
||||
async function () {
|
||||
calls.push({ type: 'fallback' });
|
||||
return { ok: true };
|
||||
},
|
||||
async function () {
|
||||
return {
|
||||
connections: [{ playerDeviceId: 'remote-player-1', playerPublicBaseUrl: 'https://remote.example' }]
|
||||
};
|
||||
},
|
||||
async function () {
|
||||
calls.push({ type: 'base-url' });
|
||||
return { ok: true };
|
||||
},
|
||||
async function () {
|
||||
return 'http://player:8081';
|
||||
},
|
||||
async function (deviceId, payload) {
|
||||
calls.push({ deviceId, payload });
|
||||
return { ok: true };
|
||||
}
|
||||
);
|
||||
|
||||
const count = await notify(['remote-screen'], 'refresh');
|
||||
|
||||
assert.equal(count, 1);
|
||||
assert.deepEqual(calls, [{
|
||||
deviceId: 'remote-player-1',
|
||||
payload: { command: 'refresh', screenSlug: 'remote-screen' }
|
||||
}]);
|
||||
});
|
||||
|
||||
@@ -5,9 +5,15 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
function loadCommandsScript(sandbox) {
|
||||
const animationPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-animation.js');
|
||||
const mediaPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-media.js');
|
||||
const transitionPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-transition.js');
|
||||
const commandsPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-commands.js');
|
||||
const animationScript = fs.readFileSync(animationPath, 'utf8');
|
||||
const mediaScript = fs.readFileSync(mediaPath, 'utf8');
|
||||
const transitionScript = fs.readFileSync(transitionPath, 'utf8');
|
||||
const commandsScript = fs.readFileSync(commandsPath, 'utf8');
|
||||
vm.runInNewContext(commandsScript, sandbox, { filename: commandsPath });
|
||||
vm.runInNewContext(animationScript + '\n' + mediaScript + '\n' + transitionScript + '\n' + commandsScript, sandbox, { filename: commandsPath });
|
||||
}
|
||||
|
||||
function createSandbox() {
|
||||
@@ -77,7 +83,8 @@ function createSandbox() {
|
||||
templateRenderPlanCache: Object.create(null),
|
||||
renderCacheViewportKey: '',
|
||||
slideTransitionTimer: null,
|
||||
slideFadeDurationMs: 560,
|
||||
slideFadeLengthMs: 560,
|
||||
slideFadeOffsetMs: 280,
|
||||
slideExpiresAt: null,
|
||||
pausedRemainingMs: null,
|
||||
timer: null,
|
||||
@@ -119,6 +126,9 @@ function createSandbox() {
|
||||
test('renderSlideMarkup runs post-render setup immediately', () => {
|
||||
const { sandbox, calls, rafCallbacks, app } = createSandbox();
|
||||
loadCommandsScript(sandbox);
|
||||
sandbox.playRegionAnimations = function () {
|
||||
calls.playRegionAnimations += 1;
|
||||
};
|
||||
|
||||
const returned = sandbox.renderSlideMarkup('<div class="slide">visible</div>', false);
|
||||
|
||||
|
||||
@@ -149,6 +149,125 @@ test('playlist refresh queues updates until the next slide transition', async ()
|
||||
assert.equal(calls.logDebug.some((entry) => entry.includes('applying on next slide transition')), true);
|
||||
});
|
||||
|
||||
test('playlist refresh reloads source data after a cached snapshot receives 304', async () => {
|
||||
const requests = [];
|
||||
const sandbox = {
|
||||
window: null,
|
||||
location: { origin: 'http://localhost' },
|
||||
Date,
|
||||
JSON,
|
||||
Math,
|
||||
Number,
|
||||
String,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
Promise,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
console,
|
||||
slug: 'test',
|
||||
initialData: null,
|
||||
currentPlaylistEtag: '"cached-etag"',
|
||||
currentPlaylistSignature: 'cached-signature',
|
||||
currentPlaylistFadeBetweenSlides: false,
|
||||
currentPlaylistSkipUnavailableRtmp: false,
|
||||
pendingPlaylistUpdate: null,
|
||||
slides: [{ id: 1, duration_seconds: 10 }],
|
||||
lastRenderedSlide: null,
|
||||
activeSlidesCacheKey: '',
|
||||
activeSlidesCacheValue: [],
|
||||
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() {},
|
||||
sendCommandState() {},
|
||||
scheduleSlideAdvance() {},
|
||||
logDebug() {}
|
||||
};
|
||||
sandbox.window = sandbox;
|
||||
|
||||
class XhrStub {
|
||||
open(method, url) {
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
requests.push(this);
|
||||
}
|
||||
|
||||
setRequestHeader(name, value) {
|
||||
this.headers = this.headers || {};
|
||||
this.headers[name] = value;
|
||||
}
|
||||
|
||||
getResponseHeader() { return ''; }
|
||||
|
||||
send() {
|
||||
this.readyState = 4;
|
||||
if (requests.length === 1) {
|
||||
this.status = 304;
|
||||
this.responseText = '';
|
||||
} else {
|
||||
this.status = 200;
|
||||
this.responseText = JSON.stringify({
|
||||
signature: 'fresh-signature',
|
||||
slides: [{ id: 1, duration_seconds: 10 }],
|
||||
rssFeeds: [],
|
||||
apiSources: [{ id: 1 }],
|
||||
timetableGroups: [],
|
||||
weatherLocations: [{ id: 1 }],
|
||||
playlist: { fade_between_slides: false, skip_unavailable_rtmp: false }
|
||||
});
|
||||
}
|
||||
this.onreadystatechange();
|
||||
}
|
||||
}
|
||||
|
||||
sandbox.XMLHttpRequest = XhrStub;
|
||||
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playback.js');
|
||||
vm.runInNewContext(fs.readFileSync(scriptPath, 'utf8'), sandbox, { filename: scriptPath });
|
||||
|
||||
await sandbox.refresh();
|
||||
|
||||
assert.equal(requests.length, 2);
|
||||
assert.equal(requests[1].headers && requests[1].headers['If-None-Match'], undefined);
|
||||
assert.deepEqual(sandbox.initialData.apiSources, [{ id: 1 }]);
|
||||
assert.deepEqual(sandbox.initialData.weatherLocations, [{ id: 1 }]);
|
||||
});
|
||||
|
||||
test('centralized slide advance timing preserves the configured slide duration', () => {
|
||||
const sandbox = {
|
||||
window: null,
|
||||
currentPlaylistFadeBetweenSlides: false,
|
||||
slideFadeLengthMs: 560,
|
||||
slideFadeOffsetMs: 280,
|
||||
getSlideHoldDelay(value) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
sandbox.window = sandbox;
|
||||
|
||||
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playback.js');
|
||||
const script = fs.readFileSync(scriptPath, 'utf8');
|
||||
vm.runInNewContext(script, sandbox, { filename: scriptPath });
|
||||
|
||||
assert.equal(sandbox.getSlideAdvanceDelay({ duration_seconds: 12 }), 12000);
|
||||
sandbox.currentPlaylistFadeBetweenSlides = true;
|
||||
assert.equal(sandbox.getSlideAdvanceDelay({ duration_seconds: 10 }), 9720);
|
||||
assert.equal(sandbox.getSlideAdvanceDelay({ duration_seconds: 10, use_video_duration: true }), 9440);
|
||||
assert.equal(sandbox.getSlideAdvanceDelay({ duration_seconds: 12 }), 11720);
|
||||
assert.equal(sandbox.getSlideAdvanceDelay({ duration_seconds: 12, use_video_duration: true }), 11440);
|
||||
});
|
||||
|
||||
test('single-slide playlists re-render the active slide instead of refreshing after a queued update', async () => {
|
||||
const calls = {
|
||||
showCurrent: 0,
|
||||
@@ -247,8 +366,10 @@ test('single-slide playlists re-render the active slide instead of refreshing af
|
||||
sandbox.XMLHttpRequest = XhrStub;
|
||||
|
||||
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-playback.js');
|
||||
const transitionPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-transition.js');
|
||||
const commandsPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-commands.js');
|
||||
const playbackScript = fs.readFileSync(scriptPath, 'utf8');
|
||||
const transitionScript = fs.readFileSync(transitionPath, 'utf8');
|
||||
const commandsScript = fs.readFileSync(commandsPath, 'utf8');
|
||||
const prelude = `
|
||||
var pendingPlaylistUpdate = null;
|
||||
@@ -270,7 +391,7 @@ test('single-slide playlists re-render the active slide instead of refreshing af
|
||||
var templateLayoutCache = Object.create(null);
|
||||
var templateRenderPlanCache = Object.create(null);
|
||||
`;
|
||||
vm.runInNewContext(prelude + '\n' + playbackScript + '\n' + commandsScript, sandbox, { filename: scriptPath });
|
||||
vm.runInNewContext(prelude + '\n' + transitionScript + '\n' + playbackScript + '\n' + commandsScript, sandbox, { filename: scriptPath });
|
||||
sandbox.showCurrent = function () {
|
||||
calls.showCurrent += 1;
|
||||
};
|
||||
|
||||
@@ -99,7 +99,8 @@ test('fqdn player registration wins over a local configured player target for me
|
||||
|
||||
assert.equal(success, true);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0].url, 'https://pulse-dev-bridge.lzstealth.com/api/media/uploads%2Fsample.bin?deviceId=player-remote');
|
||||
assert.equal(fetchCalls[0].url, 'https://pulse-dev-bridge.lzstealth.com/api/media/uploads%2Fsample.bin');
|
||||
assert.equal(fetchCalls[0].init.headers['x-pulse-player-device-id'], 'player-remote');
|
||||
} finally {
|
||||
global.fetch = originalFetch;
|
||||
fs.rmSync(uploadDir, { recursive: true, force: true });
|
||||
@@ -453,9 +454,46 @@ test('slide update sync queues one media task per live player and targets each p
|
||||
assert.deepEqual(fetchCalls.map(function (call) {
|
||||
return call.url;
|
||||
}).sort(), [
|
||||
'http://player-one:8081/api/media/uploads%2Fsample.bin?deviceId=player-one',
|
||||
'http://player-two:8081/api/media/uploads%2Fsample.bin?deviceId=player-two'
|
||||
'http://player-one:8081/api/media/uploads%2Fsample.bin',
|
||||
'http://player-two:8081/api/media/uploads%2Fsample.bin'
|
||||
].sort());
|
||||
assert.deepEqual(fetchCalls.map(function (call) {
|
||||
return call.init.headers['x-pulse-player-device-id'];
|
||||
}).sort(), ['player-one', 'player-two']);
|
||||
} finally {
|
||||
global.fetch = originalFetch;
|
||||
fs.rmSync(uploadDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('remote media sync uses the bridge device route', async () => {
|
||||
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-bridge-'));
|
||||
fs.mkdirSync(path.join(uploadDir, 'uploads'), { recursive: true });
|
||||
fs.writeFileSync(path.join(uploadDir, 'uploads', 'sample.bin'), Buffer.from('hello world'));
|
||||
const activeLastSeenAt = new Date(Date.now() - 10_000).toISOString();
|
||||
const fetchCalls = [];
|
||||
const originalFetch = global.fetch;
|
||||
global.fetch = async function (url, init) {
|
||||
fetchCalls.push({ url, init });
|
||||
return { ok: true, status: 200, statusText: 'OK', headers: { get() { return null; } }, async text() { return ''; } };
|
||||
};
|
||||
|
||||
const uploadSyncService = createUploadSyncService({
|
||||
common: {},
|
||||
bridgeInternalBaseUrl: 'http://player-bridge:8090',
|
||||
pool: {
|
||||
async query() {
|
||||
return [[{ identifier: 'player-remote', internal_base_url: 'https://remote-player.example', last_seen_at: activeLastSeenAt }]];
|
||||
}
|
||||
},
|
||||
playerSnapshotCache: new Map(),
|
||||
notifyPlayerScreens: async () => {}
|
||||
});
|
||||
|
||||
try {
|
||||
assert.equal(await uploadSyncService.pushUploadFileToPlayer('/media/uploads/sample.bin', uploadDir), true);
|
||||
assert.equal(fetchCalls[0].url, 'http://player-bridge:8090/api/media/uploads%2Fsample.bin');
|
||||
assert.equal(fetchCalls[0].init.headers['x-pulse-player-device-id'], 'player-remote');
|
||||
} finally {
|
||||
global.fetch = originalFetch;
|
||||
fs.rmSync(uploadDir, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user