Files
pulse-signage/test/player-runtime.test.js
T
2026-08-03 19:18:30 +01:00

192 lines
7.1 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const http = require('http');
const WebSocket = require('ws');
require('../src/common');
const { createPageAuthToken } = require('../src/request-auth');
const { createPlayerRuntime } = require('../src/player/runtime');
const originalSecret = process.env.PULSE_SIGNAGE_SHARED_SECRET;
test.after(() => {
if (originalSecret === undefined) {
delete process.env.PULSE_SIGNAGE_SHARED_SECRET;
} else {
process.env.PULSE_SIGNAGE_SHARED_SECRET = originalSecret;
}
});
function waitFor(predicate, timeoutMs = 1000) {
const start = Date.now();
return new Promise((resolve, reject) => {
const tick = () => {
const value = predicate();
if (value) {
return resolve(value);
}
if (Date.now() - start > timeoutMs) {
return reject(new Error('Timed out waiting for runtime state.'));
}
setTimeout(tick, 25);
};
tick();
});
}
test('player runtime snapshots websocket state and checks live names', 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: 'test2' });
const client = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/test2?auth=${encodeURIComponent(token)}`);
try {
await new Promise((resolve, reject) => {
client.once('open', resolve);
client.once('error', reject);
});
client.send(JSON.stringify({
type: 'state',
clientId: 'client-1',
clientName: ' Lobby Player ',
deviceId: 'device 123!?',
userAgent: 'Mozilla/5.0 (unit test)',
viewport: { width: 1920, height: 1080 },
page: 'http://localhost:8081/screen/test2',
paused: true,
blackout: false,
currentSlide: { id: 9, title: 'Intro', kind: 'slide', playlistSignature: 'sig' }
}));
await waitFor(() => {
const snapshot = runtime.snapshotConnections('test2')[0];
return snapshot && snapshot.clientName === 'Lobby Player' ? snapshot : null;
});
const snapshot = runtime.snapshotConnections('test2')[0];
assert.equal(snapshot.clientName, 'Lobby Player');
assert.equal(snapshot.deviceId, 'device123');
assert.match(snapshot.label, /Lobby Player/);
assert.equal(snapshot.paused, true);
assert.equal(snapshot.currentSlideId, 9);
assert.equal(snapshot.currentSlideTitle, 'Intro');
assert.equal(await runtime.isClientNameAvailableOnScreen(null, 'Lobby Player', 'other-device'), false);
assert.equal(await runtime.isClientNameAvailableOnScreen(null, 'Lobby Player', 'device123'), true);
assert.equal(await runtime.isClientNameAvailableOnScreen(null, 'Other Name', 'device123'), true);
} finally {
client.close();
await new Promise((resolve) => server.close(resolve));
}
});
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';
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: 'test2' });
const clientA = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/test2?auth=${encodeURIComponent(token)}`);
const clientB = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/test2?auth=${encodeURIComponent(token)}`);
function openClient(client) {
return new Promise((resolve, reject) => {
client.once('open', resolve);
client.once('error', reject);
});
}
function waitForMessage(client) {
return new Promise((resolve) => {
client.once('message', (raw) => {
resolve(JSON.parse(String(raw)));
});
});
}
try {
await Promise.all([openClient(clientA), openClient(clientB)]);
clientA.send(JSON.stringify({ type: 'state', clientId: 'client-a', clientName: 'Alpha', deviceId: 'device-a', page: 'http://localhost:8081/screen/test2' }));
clientB.send(JSON.stringify({ type: 'state', clientId: 'client-b', clientName: 'Beta', deviceId: 'device-b', page: 'http://localhost:8081/screen/test2' }));
await waitFor(() => {
const connections = runtime.snapshotConnections('test2');
return connections.length === 2 && connections.every((connection) => connection.clientName);
});
const [firstConnection, secondConnection] = runtime.snapshotConnections('test2');
const targetedMessagePromise = waitForMessage(clientA);
const targetedCount = await runtime.sendCommandToConnection('test2', firstConnection.id, { action: 'pause' });
assert.equal(targetedCount, 1);
const targetedMessage = await targetedMessagePromise;
assert.equal(targetedMessage.type, 'command');
assert.equal(targetedMessage.action, 'pause');
assert.equal(targetedMessage.targetConnectionId, firstConnection.id);
const broadcastPromises = [waitForMessage(clientA), waitForMessage(clientB)];
const broadcastCount = await runtime.broadcastCommand('test2', 'resume');
assert.equal(broadcastCount, 2);
const [broadcastA, broadcastB] = await Promise.all(broadcastPromises);
assert.equal(broadcastA.type, 'command');
assert.equal(broadcastA.command, 'resume');
assert.equal(broadcastB.type, 'command');
assert.equal(broadcastB.command, 'resume');
assert.equal(broadcastA.targetConnectionId, undefined);
assert.equal(broadcastB.targetConnectionId, undefined);
assert.equal(await runtime.sendCommandToConnection('test2', 'missing', 'pause'), 0);
assert.equal(secondConnection.clientName, 'Beta');
} finally {
clientA.close();
clientB.close();
await new Promise((resolve) => server.close(resolve));
}
});