Release 2.5.2

This commit is contained in:
2026-08-03 19:18:30 +01:00
parent 37345f3949
commit 9d5029eff6
21 changed files with 195 additions and 49 deletions
+1
View File
@@ -3,6 +3,7 @@ media/
!src/web/lib/media/
!src/web/lib/media/**
docker-compose.dev.yml
/dev-demo-seed.js
.vscode/
.env
npm-debug.log*
+12
View File
@@ -2,6 +2,18 @@
All notable changes to this project will be documented in this file.
## 2.5.2 - 2026-08-03
### Changed
- Player page auth now writes a cookie for websocket reuse, and player and announcement sockets accept that cookie so they no longer depend on query-string tokens.
- RSS and API region placeholder panels now use shared field-aware chip rendering, with updated spacing and defaults across the slide editor.
- RTMP regions now default audio to enabled in the editor, and the slide rich-text editor no longer allows anchor tags.
### Fixed
- Player websocket auth now falls back to the `pulse_page_auth` cookie when the auth query parameter is unavailable.
## 2.5.1 - 2026-08-03
### Added
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulse-signage",
"version": "2.5.1",
"version": "2.5.2",
"private": false,
"description": "Pulse Signage application with MySQL and media storage",
"repository": {
+1 -1
View File
@@ -5,7 +5,7 @@ async function bootstrapDatabase(pool) {
const [canvasSizeCountRows] = await pool.query('SELECT COUNT(*) AS canvas_size_count FROM c_canvas_sizes');
if (!canvasSizeCountRows.length || Number(canvasSizeCountRows[0].canvas_size_count) === 0) {
await pool.query(`
INSERT INTO c_canvas_sizes (name, width, height) VALUES
INSERT IGNORE INTO c_canvas_sizes (name, width, height) VALUES
('Full HD', 1920, 1080),
('HD', 1280, 720),
('4K UHD', 3840, 2160),
+39 -13
View File
@@ -55,16 +55,7 @@ const VERSIONED_MIGRATIONS = [
}
// Recreate the screen-to-player foreign key after the column exists and legacy data is copied over.
const [screenPlayerFkRows] = await pool.query(
`SELECT COUNT(*) AS fk_count
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'd_screens'
AND CONSTRAINT_NAME = 'fk_screens_player'`
);
if (Number(screenPlayerFkRows && screenPlayerFkRows[0] && screenPlayerFkRows[0].fk_count) === 0) {
await pool.query('ALTER TABLE d_screens ADD CONSTRAINT fk_screens_player FOREIGN KEY (player_id) REFERENCES d_players(device_id) ON DELETE RESTRICT');
}
await ensureForeignKey(pool, 'd_screens', 'fk_screens_player', 'player_id', 'd_players', 'device_id', 'RESTRICT');
if (await columnExists(pool, 'c_template_regions', 'font_family')) {
await pool.query('ALTER TABLE c_template_regions DROP COLUMN font_family');
@@ -243,24 +234,59 @@ async function ensureColumn(pool, tableName, columnName, columnDefinition, after
}
const afterClause = afterColumn ? ' AFTER ' + afterColumn : '';
try {
await pool.query('ALTER TABLE ' + tableName + ' ADD COLUMN ' + columnName + ' ' + columnDefinition + afterClause);
} catch (error) {
if (!error || (error.code !== 'ER_DUP_FIELDNAME' && error.errno !== 1060)) {
throw error;
}
}
}
async function ensureForeignKey(pool, tableName, constraintName, columnName, referencedTable, referencedColumn, onDeleteAction) {
const [rows] = await pool.query(
const [existingFkRows] = await pool.query(
`SELECT COUNT(*) AS fk_count
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?
AND REFERENCED_TABLE_NAME IS NOT NULL`,
[tableName, columnName]
);
if (Number(existingFkRows && existingFkRows[0] && existingFkRows[0].fk_count) > 0) {
return;
}
const [rows] = await pool.query(
`SELECT COUNT(*) AS fk_count
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND CONSTRAINT_NAME = ?`,
[tableName, constraintName]
[constraintName]
);
if (Number(rows && rows[0] && rows[0].fk_count) > 0) {
return;
}
await pool.query('ALTER TABLE ' + tableName + ' ADD CONSTRAINT ' + constraintName + ' FOREIGN KEY (' + columnName + ') REFERENCES ' + referencedTable + '(' + referencedColumn + ') ON DELETE ' + onDeleteAction);
const fallbackNames = [
constraintName,
tableName + '_' + columnName + '_fk',
tableName + '_' + columnName + '_fk_2',
tableName + '_' + columnName + '_fk_3'
];
for (const candidateName of fallbackNames) {
try {
await pool.query('ALTER TABLE ' + tableName + ' ADD CONSTRAINT ' + candidateName + ' FOREIGN KEY (' + columnName + ') REFERENCES ' + referencedTable + '(' + referencedColumn + ') ON DELETE ' + onDeleteAction);
return;
} catch (error) {
if (!error || (error.code !== 'ER_FK_DUP_NAME' && error.errno !== 1826)) {
throw error;
}
}
}
}
async function dropColumnIfExists(pool, tableName, columnName) {
@@ -101,6 +101,7 @@
gap: 2.5rem;
min-width: 100%;
width: max-content;
backface-visibility: hidden;
will-change: transform;
animation: lower-third-scroll var(--announcement-scroll-duration, 20s) linear infinite;
}
@@ -144,10 +145,10 @@
@keyframes lower-third-scroll {
from {
transform: translateX(0);
transform: translate3d(0, 0, 0);
}
to {
transform: translateX(-50%);
transform: translate3d(-50%, 0, 0);
}
}
</style>
@@ -103,6 +103,7 @@
gap: 2.5rem;
min-width: 100%;
width: max-content;
backface-visibility: hidden;
will-change: transform;
animation: top-banner-scroll var(--announcement-scroll-duration, 20s) linear infinite;
}
@@ -146,10 +147,10 @@
@keyframes top-banner-scroll {
from {
transform: translateX(0);
transform: translate3d(0, 0, 0);
}
to {
transform: translateX(-50%);
transform: translate3d(-50%, 0, 0);
}
}
</style>
+1 -6
View File
@@ -215,12 +215,7 @@
return;
}
var socketUrl = new URL(announcementSocketPath, window.location.origin);
if (window.__pulsePageAuthToken) {
socketUrl.searchParams.set('auth', window.__pulsePageAuthToken);
}
var socket = new WebSocket(socketUrl.toString());
var socket = new WebSocket(new URL(announcementSocketPath, window.location.origin).toString());
announcementSocket = socket;
socket.onopen = function () {
+4 -1
View File
@@ -214,6 +214,8 @@ body.thumbnail-preview .player-offline-banner {
height: 100%;
opacity: 0;
transition: opacity 560ms ease;
will-change: opacity;
backface-visibility: hidden;
}
.slide-shell.is-visible {
@@ -243,7 +245,8 @@ body.screen-blackout #app {
height: var(--player-canvas-height, 100vh);
max-width: 100vw;
max-height: 100vh;
transform: translate(-50%, -50%);
transform: translate3d(-50%, -50%, 0);
backface-visibility: hidden;
z-index: 9999;
pointer-events: none;
display: flex;
+1 -6
View File
@@ -526,12 +526,7 @@ function connectCommandSocket() {
if (commandSocket && (commandSocket.readyState === WebSocket.OPEN || commandSocket.readyState === WebSocket.CONNECTING)) {
return;
}
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var socketUrl = new URL(commandSocketPath, window.location.origin);
if (window.__pulsePageAuthToken) {
socketUrl.searchParams.set('auth', window.__pulsePageAuthToken);
}
var socket = new WebSocket(socketUrl.toString());
var socket = new WebSocket(new URL(commandSocketPath, window.location.origin).toString());
commandSocket = socket;
socket.onopen = function () {
+1 -1
View File
@@ -132,7 +132,7 @@ function renderPlayerPage(slug, initialData) {
bodyClass: bodyClass,
body: '<div id="app"><div class="empty">Loading screen...</div></div>',
stylesheets: fontStylesheetHref ? [fontStylesheetHref] : [],
script: createPageFetchAuthScript(pageAuthToken) + hlsScriptTag + serviceWorkerScript + createThumbnailPreviewBootstrapScript(initialData) + '<script>' + offlineScript + '</script>' + '<script>' + playlistScript + '</script>' + '<script>' + commandScript + '</script>' + '<script>' + renderingScript + '</script>' + '<script>' + playbackScript + '</script>' + '<script>' + getAnnouncementIconsDataScript() + '</script>' + '<script>' + getPlayerAnnouncementTemplatesDataScript() + '</script>' + '<script>' + getPlayerAnnouncementTemplatesScript() + '</script>' + onboardingScript + script + '<script>' + getPlayerPageAnnouncementsScript()() + '</script>'
script: createPageFetchAuthScript(pageAuthToken, '/ws/screens/' + encodeURIComponent(slug || '')) + hlsScriptTag + serviceWorkerScript + createThumbnailPreviewBootstrapScript(initialData) + '<script>' + offlineScript + '</script>' + '<script>' + playlistScript + '</script>' + '<script>' + commandScript + '</script>' + '<script>' + renderingScript + '</script>' + '<script>' + playbackScript + '</script>' + '<script>' + getAnnouncementIconsDataScript() + '</script>' + '<script>' + getPlayerAnnouncementTemplatesDataScript() + '</script>' + '<script>' + getPlayerAnnouncementTemplatesScript() + '</script>' + onboardingScript + script + '<script>' + getPlayerPageAnnouncementsScript()() + '</script>'
});
}
+31 -2
View File
@@ -4,6 +4,7 @@ const crypto = require('crypto');
const { WebSocketServer, WebSocket } = require('ws');
const { isClientNameAvailable } = require('#src/data/client-name-check');
const { verifyPageAuthToken, verifyRequestAuth } = require('#src/request-auth');
const PAGE_AUTH_COOKIE_NAME = 'pulse_page_auth';
function normalizePlayerPublicBaseUrl(pageUrl) {
const value = String(pageUrl || '').trim();
@@ -43,6 +44,34 @@ function createPlayerRuntime(options) {
return ip;
}
function parseCookies(cookieHeader) {
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
if (!pair) {
return cookies;
}
const separatorIndex = pair.indexOf('=');
if (separatorIndex === -1) {
return cookies;
}
const name = decodeURIComponent(pair.slice(0, separatorIndex).trim());
const value = decodeURIComponent(pair.slice(separatorIndex + 1).trim());
if (name) {
cookies[name] = value;
}
return cookies;
}, {});
}
function readPageAuthToken(request) {
const queryToken = String(new URL(request.url, 'http://localhost').searchParams.get('auth') || '').trim();
if (queryToken) {
return queryToken;
}
const cookies = parseCookies(request.headers && request.headers.cookie || '');
return String(cookies[PAGE_AUTH_COOKIE_NAME] || '').trim();
}
function getConnectionBucket(slug) {
const key = String(slug || '').trim();
if (!key) {
@@ -314,7 +343,7 @@ function createPlayerRuntime(options) {
}
if (playerMatch) {
const authToken = String(new URL(request.url, 'http://localhost').searchParams.get('auth') || '').trim();
const authToken = readPageAuthToken(request);
const payload = verifyPageAuthToken(authToken);
if (!payload || String(payload.scope || '').trim() !== 'player') {
socket.destroy();
@@ -323,7 +352,7 @@ function createPlayerRuntime(options) {
}
if (announcementMatch) {
const authToken = String(new URL(request.url, 'http://localhost').searchParams.get('auth') || '').trim();
const authToken = readPageAuthToken(request);
const payload = verifyPageAuthToken(authToken);
if (!payload || String(payload.scope || '').trim() !== 'player') {
socket.destroy();
+22 -1
View File
@@ -246,12 +246,13 @@ function verifyRequestAuth(req) {
return timingSafeEqualHex(expectedSignature, signature);
}
function createPageFetchAuthScript(token) {
function createPageFetchAuthScript(token, cookiePath) {
const normalizedToken = String(token && typeof token === 'object' ? token.token : token || '').trim();
if (!normalizedToken) {
return '';
}
const normalizedCookiePath = String(cookiePath || '').trim();
const pageAuthExpiresAt = token && typeof token === 'object' && Number.isFinite(Number(token.expiresAt))
? Number(token.expiresAt)
: null;
@@ -262,6 +263,8 @@ function createPageFetchAuthScript(token) {
' (function () {',
' var pageAuthToken = ' + JSON.stringify(normalizedToken) + ';',
' var pageAuthExpiresAt = ' + JSON.stringify(pageAuthExpiresAt) + ';',
' var pageAuthCookiePath = ' + JSON.stringify(normalizedCookiePath) + ';',
' var pageAuthCookieName = "pulse_page_auth";',
' var pageAuthRenewalTimer = null;',
' var pageAuthRenewalInFlight = null;',
' var pageAuthRenewalSkewMs = ' + JSON.stringify(renewSkewMs) + ';',
@@ -287,11 +290,28 @@ function createPageFetchAuthScript(token) {
' });',
' }, delayMs);',
' }',
' function writePageAuthCookie(nextToken, nextExpiresAt) {',
' if (!pageAuthCookiePath) {',
' return;',
' }',
' var cookieParts = [pageAuthCookieName + "=" + encodeURIComponent(String(nextToken || "").trim()), "Path=" + pageAuthCookiePath, "SameSite=Lax"];',
' var expiresInMs = Number(nextExpiresAt || 0) - Date.now();',
' if (Number.isFinite(expiresInMs) && expiresInMs > 0) {',
' cookieParts.push("Max-Age=" + Math.max(1, Math.floor(expiresInMs / 1000)));',
' } else {',
' cookieParts.push("Max-Age=0");',
' }',
' if (window.location.protocol === "https:") {',
' cookieParts.push("Secure");',
' }',
' document.cookie = cookieParts.join("; ");',
' }',
' function setPageAuthToken(nextToken, nextExpiresAt) {',
' pageAuthToken = String(nextToken || "").trim();',
' pageAuthExpiresAt = Number(nextExpiresAt || 0) || null;',
' window.__pulsePageAuthToken = pageAuthToken;',
' window.__pulsePageAuthExpiresAt = pageAuthExpiresAt;',
' writePageAuthCookie(pageAuthToken, pageAuthExpiresAt);',
' schedulePageAuthRenewal();',
' }',
' async function renewPageAuthToken() {',
@@ -326,6 +346,7 @@ function createPageFetchAuthScript(token) {
' window.__pulseRenewPageAuthToken = renewPageAuthToken;',
' window.__pulsePageAuthToken = pageAuthToken;',
' window.__pulsePageAuthExpiresAt = pageAuthExpiresAt;',
' writePageAuthCookie(pageAuthToken, pageAuthExpiresAt);',
' window.addEventListener("focus", function () {',
' schedulePageAuthRenewal();',
' });',
+5 -1
View File
@@ -1743,6 +1743,7 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
gap: 0;
min-height: 3.25rem;
}
@@ -1756,6 +1757,7 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
.template-field-head strong {
font-size: 0.95rem;
min-width: 0;
}
.template-field-head .chip {
@@ -1869,6 +1871,7 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
display: grid;
gap: 0.5rem;
padding-top: 0.25rem;
margin-bottom: 0.75rem;
}
.api-region-placeholder-title {
@@ -1880,7 +1883,8 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
}
.api-region-sample-accordion {
padding: 0.85rem 1rem;
padding: 0.85rem 1rem 1rem;
margin-bottom: 0.75rem;
border: 1px solid var(--bs-border-color);
border-radius: 0.85rem;
background: var(--bs-secondary-bg);
+2 -1
View File
@@ -191,6 +191,7 @@
var region = context.region;
var current = context.current || {};
var config = context.config || {};
var placeholderFields = Array.isArray(context.placeholderFields) ? context.placeholderFields : [];
var placeholderChips = String(context.placeholderChips || '');
var sourceOptions = String(context.sourceOptions || '');
var itemsPathValue = context.itemsPath !== undefined
@@ -234,7 +235,7 @@
'<div class="muted slide-image-file">Use transforms like {{name.upper()}}, {{name.title()}}, or {{name.lower()}} on leaf fields.</div>' +
'<div class="api-region-placeholder-section">' +
'<div class="api-region-placeholder-title">Available placeholders</div>' +
'<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + (placeholderChips || '<span class="muted slide-image-file">No JSON fields available.</span>') + '</div>' +
'<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + ((placeholderFields.length && window.placeholderChips && typeof window.placeholderChips.renderChips === 'function') ? window.placeholderChips.renderChips(placeholderFields) : placeholderChips || '<span class="muted slide-image-file">No JSON fields available.</span>') + '</div>' +
'</div>' +
'<details class="api-region-sample-accordion" data-api-sample-data-accordion>' +
'<summary>Data</summary>' +
+5 -2
View File
@@ -218,7 +218,7 @@
'<span class="chip">RSS</span>' +
'</div>' +
'</div>' +
'<div class="card-body p-3 d-grid">' +
'<div class="card-body p-3 d-grid gap-3 pb-0">' +
'<div class="editor-holder" data-region-id="' + region.id + '">' +
'<textarea class="editor-source" rows="10">' + escapeHtml(current.value !== undefined ? current.value : '') + '</textarea>' +
'</div>' +
@@ -237,9 +237,12 @@
'</div>' +
'<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(context.fontSize || '') + '" />' +
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current.value !== undefined ? current.value : '') + '" />' +
'<div class="muted slide-image-file">Use placeholders like {{title.upper()}}, {{title.title()}}, or {{title.lower()}}. Available placeholders:</div>' +
'<div class="muted slide-image-file">Use transforms like {{title.upper()}}, {{title.title()}}, or {{title.lower()}} on leaf fields.</div>' +
'<div class="api-region-placeholder-section">' +
'<div class="api-region-placeholder-title">Available placeholders</div>' +
'<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + (placeholderChips || '<span class="muted slide-image-file">No RSS fields available.</span>') + '</div>' +
'</div>' +
'</div>' +
'</div>';
}
+1 -1
View File
@@ -21,7 +21,7 @@
function renderEditorCard(context) {
var region = context.region;
var current = String(context.current || '');
var disableAudio = context.disableAudio === undefined ? true : Boolean(context.disableAudio);
var disableAudio = context.disableAudio === undefined ? false : Boolean(context.disableAudio);
return '' +
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
'<div class="card-header template-field-head">' +
@@ -233,7 +233,7 @@ export function createSlideFormEditorController(options) {
promotion: false,
statusbar: true,
resize: true,
plugins: 'lists link code advlist fullscreen table',
plugins: 'lists code advlist fullscreen table',
toolbar: 'undo redo | fontfamily fontsizeinput | forecolor backcolor bold italic underline strikethrough subscript superscript removeformat | align lineheight indent outdent bullist numlist table | fullscreen',
toolbar_mode: 'sliding',
license_key: 'gpl',
@@ -244,6 +244,7 @@ export function createSlideFormEditorController(options) {
content_style: 'body { font-family: ' + defaultEditorFontFamily + '; font-size: 32px; line-height: 1.5; background-color: ' + getEditorBackgroundColorValue() + '; } p { margin: 1em 0; } p:first-child { margin-top: 0; } p:last-child { margin-bottom: 1em; } table { border-collapse: collapse; width: 100%; } td, th { border: 1px solid currentColor; padding: 0.35em 0.5em; vertical-align: top; } th { font-weight: 700; }' + (editorContentStyle ? ' ' + editorContentStyle : ''),
font_family_formats: getFontFamilyFormats(),
font_size_input_default_unit: 'px',
invalid_elements: 'a',
forced_root_block: 'p',
force_br_newlines: false,
newline_behavior: 'default',
+21 -4
View File
@@ -200,7 +200,16 @@ export function createSlideFormRegionHelpers(options) {
function updateRssPlaceholderChips(regionId, feedId) {
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
updatePlaceholderChipList(card, getRssFieldList(feedId), 'No RSS fields available.');
if (!card) {
return;
}
var container = card.querySelector('[data-placeholder-chips]');
if (!container) {
return;
}
container.innerHTML = buildLimitedPlaceholderChipMarkup(getRssFieldList(feedId), 'No RSS fields available.');
}
function getCurrentRssConfig(region) {
@@ -502,6 +511,11 @@ export function createSlideFormRegionHelpers(options) {
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
}).join('')
: buildLimitedPlaceholderChipMarkup(getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path), 'No JSON fields available.'),
placeholderFields: region.region_type === 'api'
? getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path)
: region.region_type === 'rss'
? getRssFieldList(getCurrentRssConfig(region).feed_id)
: [],
sampleDataPreview: buildApiSampleDataMarkup(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).item_number, getCurrentApiConfig(region).items_path),
timetableGroups: timetableGroups
}, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : apiSources);
@@ -533,10 +547,13 @@ export function createSlideFormRegionHelpers(options) {
return '<option value="' + escapeHtml(source.id) + '"' + selected + '>' + escapeHtml(source.name || ('Source ' + source.id)) + '</option>';
}).join(''),
placeholderChips: region.region_type === 'rss'
? getRssFieldList(rssConfig.feed_id).map(function (field) {
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
}).join('')
? buildLimitedPlaceholderChipMarkup(getRssFieldList(rssConfig.feed_id), 'No RSS fields available.')
: buildLimitedPlaceholderChipMarkup(getApiFieldList(apiConfig.source_id, apiItemsPath), 'No JSON fields available.'),
placeholderFields: region.region_type === 'api'
? getApiFieldList(apiConfig.source_id, apiItemsPath)
: region.region_type === 'rss'
? getRssFieldList(rssConfig.feed_id)
: [],
sampleDataPreview: buildApiSampleDataMarkup(apiConfig.source_id, apiConfig.item_number, apiItemsPath),
timetableGroups: timetableGroups
};
+1 -1
View File
@@ -138,7 +138,7 @@
<template id="region-card-template">
<div class="card card-outline card-secondary admin-form-card region-item">
<div class="card-header template-field-head"><strong data-region-title>Region</strong><span class="chip" data-region-chip>Text</span></div>
<div class="card-header template-field-head d-flex align-items-center flex-nowrap gap-2"><strong class="flex-grow-1 text-truncate" data-region-title>Region</strong><span class="chip ms-auto" data-region-chip>Text</span></div>
<div class="card-body p-3 d-grid gap-3 pb-0">
<div class="region-field-grid region-field-grid--identity">
<label>Region name<input class="form-control" name="region_name[]" value="" placeholder="region_1" required /></label>
+36
View File
@@ -87,6 +87,42 @@ test('player runtime snapshots websocket state and checks live names', async ()
}
});
test('player runtime accepts websocket auth from cookies', async () => {
process.env.PULSE_SIGNAGE_SHARED_SECRET = 'runtime-secret';
const runtime = createPlayerRuntime({ pool: null });
const server = http.createServer();
runtime.installWebsocket(server);
await new Promise((resolve) => server.listen(0, resolve));
const { port } = server.address();
const token = createPageAuthToken({ scope: 'player', slug: 'cookie-test' });
const client = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/cookie-test`, {
headers: {
Cookie: `pulse_page_auth=${encodeURIComponent(token)}`
}
});
try {
await new Promise((resolve, reject) => {
client.once('open', resolve);
client.once('error', reject);
});
client.send(JSON.stringify({ type: 'state', clientId: 'cookie-client', clientName: 'Cookie Player', deviceId: 'device-cookie' }));
await waitFor(() => {
const snapshot = runtime.snapshotConnections('cookie-test')[0];
return snapshot && snapshot.clientName === 'Cookie Player' ? snapshot : null;
});
assert.equal(runtime.snapshotConnections('cookie-test')[0].clientName, 'Cookie Player');
} finally {
client.close();
await new Promise((resolve) => server.close(resolve));
}
});
test('player runtime sends targeted and broadcast commands to live sockets', async () => {
process.env.PULSE_SIGNAGE_SHARED_SECRET = 'runtime-secret';