Release v2.10.3
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m13s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s

This commit is contained in:
2026-08-29 01:56:45 +01:00
parent 30814f3f46
commit 3f4f57020a
60 changed files with 1075 additions and 523 deletions
+72
View File
@@ -96,6 +96,52 @@ test('player runtime snapshots websocket state and checks live names', async ()
}
});
test('player runtime suffixes a reconnecting client name already in use', async () => {
process.env.PULSE_SIGNAGE_SHARED_SECRET = 'runtime-secret';
const persistedNames = [];
const runtime = createPlayerRuntime({
pool: {
async query(_sql, params) {
return [[params[0] === 'Lobby' && params[1] !== 'device-a' ? { device_id: 'device-a' } : undefined].filter(Boolean)];
}
},
persistClientName(deviceId, clientName) {
persistedNames.push({ deviceId, clientName });
return Promise.resolve();
}
});
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: 'reconnect-test' });
const clientA = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/reconnect-test?auth=${encodeURIComponent(token)}`);
const clientB = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/reconnect-test?auth=${encodeURIComponent(token)}`);
try {
await Promise.all([
new Promise((resolve, reject) => { clientA.once('open', resolve); clientA.once('error', reject); }),
new Promise((resolve, reject) => { clientB.once('open', resolve); clientB.once('error', reject); })
]);
clientA.send(JSON.stringify({ type: 'state', clientId: 'client-a', clientName: 'Lobby', deviceId: 'device-a' }));
await waitFor(() => runtime.snapshotConnections('reconnect-test').some((connection) => connection.clientName === 'Lobby'));
clientB.send(JSON.stringify({ type: 'state', clientId: 'client-b', clientName: 'Lobby', deviceId: 'device-b' }));
await waitFor(() => {
const connections = runtime.snapshotConnections('reconnect-test');
return connections.length === 2 && connections.some((connection) => connection.clientName === 'Lobby (1)');
});
assert.deepEqual(persistedNames, [{ deviceId: 'device-b', clientName: 'Lobby (1)' }]);
} finally {
clientA.close();
clientB.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';
@@ -198,4 +244,30 @@ test('player runtime sends targeted and broadcast commands to live sockets', asy
clientB.close();
await new Promise((resolve) => server.close(resolve));
}
});
test('player runtime removes browser connections that stop sending state', async () => {
process.env.PULSE_SIGNAGE_SHARED_SECRET = 'runtime-secret';
const runtime = createPlayerRuntime({ pool: null, staleConnectionMs: 50 });
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: 'stale-test' });
const client = new WebSocket(`ws://127.0.0.1:${port}/ws/screens/stale-test?auth=${encodeURIComponent(token)}`);
try {
await new Promise((resolve, reject) => {
client.once('open', resolve);
client.once('error', reject);
});
client.send(JSON.stringify({ type: 'state', clientId: 'stale-client', clientName: 'Stale Player' }));
await waitFor(() => runtime.snapshotConnections('stale-test').length === 1);
await waitFor(() => runtime.snapshotConnections('stale-test').length === 0, 1000);
} finally {
client.close();
await new Promise((resolve) => server.close(resolve));
}
});