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

This commit is contained in:
lzstealth committed 2026-10-11 18:54:43 +01:00
1 parent 635e1a9942
commit c3299abaac
174 files changed
+4483 -1687

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const test = require('node:test');
const assert = require('node:assert/strict');
async function activateWorker(version, cacheKeys) {
const listeners = new Map();
const deleted = [];
let claimed = false;
const sandbox = {
URL,
self: {
location: { href: 'http://localhost/sw.js?v=' + encodeURIComponent(version) },
addEventListener(name, callback) { listeners.set(name, callback); },
clients: { async claim() { claimed = true; } }
},
caches: {
async keys() { return cacheKeys; },
async delete(key) { deleted.push(key); return true; }
}
};
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'sw.js');
vm.runInNewContext(fs.readFileSync(scriptPath, 'utf8'), sandbox, { filename: scriptPath });
let activation;
listeners.get('activate')({ waitUntil(promise) { activation = promise; } });
await activation;
assert.equal(claimed, true);
return deleted;
}
test('player service worker activation preserves caches owned by other applications', async () => {
const deleted = await activateWorker('2.14.0', ['other-app-assets', 'other-app-pages']);
assert.deepEqual(deleted, []);
});
test('player service worker retains its active version and the most recently created other generation', async () => {
const cacheKeys = [
'other-app-cache',
'pulse-signage-player-pages-2.10.0',
'pulse-signage-player-assets-2.10.0',
'pulse-signage-player-media-2.10.0',
'pulse-signage-player-playlists-2.10.0',
'pulse-signage-player-pages-2.9.0',
'pulse-signage-player-assets-2.11.0',
'pulse-signage-player-pages-2.11.0'
];
const deleted = await activateWorker('2.10.0', cacheKeys);
assert.deepEqual(deleted, ['pulse-signage-player-pages-2.9.0']);
});
test('player service worker retention supports opaque versions before active caches exist', async () => {
const deleted = await activateWorker('current-build', [
'pulse-signage-player-pages-z-old-build',
'pulse-signage-player-assets-z-old-build',
'pulse-signage-player-pages-a-newer-build',
'pulse-signage-player-playlists-a-newer-build'
]);
assert.deepEqual(deleted, [
'pulse-signage-player-pages-z-old-build',
'pulse-signage-player-assets-z-old-build'
]);
});
function createFetchWorker({ failure, cachedResponse = null, fetchError = false } = {}) {
const networkResponse = new Response('fresh network body', {
status: 200, headers: { ETag: '"fresh"', 'Content-Type': 'text/plain' }
});
const calls = { fetch: 0, writes: [] };
const listeners = new Map();
const sandbox = {
URL,
Response,
Request,
Headers,
self: {
location: { href: 'http://localhost/sw.js', origin: 'http://localhost' },
addEventListener(name, callback) { listeners.set(name, callback); }
},
async fetch() {
calls.fetch += 1;
if (fetchError) throw new Error('Network unavailable');
return networkResponse;
},
caches: {
async match() { return cachedResponse; },
async open() {
if (failure === 'open') throw new Error('Cache storage unavailable');
return {
async put(key, response) {
if (failure === 'put') throw new Error('QuotaExceededError');
calls.writes.push({ key, response });
}
};
}
}
};
const scriptPath = path.join(__dirname, '..', 'src', 'player', 'public', 'sw.js');
vm.runInNewContext(fs.readFileSync(scriptPath, 'utf8'), sandbox, { filename: scriptPath });
return { sandbox, calls, networkResponse, listeners };
}
test('successful network responses survive cache open and write failures', async () => {
for (const failure of ['open', 'put']) {
for (const strategy of ['networkFirst', 'cacheFirst', 'staleWhileRevalidate']) {
for (const cached of strategy === 'networkFirst' ? [false, true] : [false]) {
const cachedResponse = cached ? new Response('stale body') : null;
const { sandbox, networkResponse } = createFetchWorker({ failure, cachedResponse });
const response = await sandbox[strategy]({ url: 'http://localhost/screen/test' }, 'cache');
assert.equal(response, networkResponse, strategy + ', failure=' + failure + ', cached=' + cached);
assert.equal(response.status, 200);
assert.equal(response.headers.get('ETag'), '"fresh"');
assert.equal(await response.text(), 'fresh network body');
}
}
}
});
test('successful cache writes retain the normalized key and do not consume the network body', async () => {
const { sandbox, calls, networkResponse } = createFetchWorker();
const request = { url: 'http://localhost/api/screens/test/playlist?ts=1' };
const normalizedKey = { url: 'http://localhost/api/screens/test/playlist' };
const response = await sandbox.networkFirst(request, 'cache', normalizedKey);
assert.equal(response, networkResponse);
assert.equal(calls.writes.length, 1);
assert.equal(calls.writes[0].key, normalizedKey);
assert.notEqual(calls.writes[0].response, networkResponse);
assert.equal(await calls.writes[0].response.text(), 'fresh network body');
assert.equal(await response.text(), 'fresh network body');
});
test('asset cache hits do not fetch and offline pages still fall back to cached responses', async () => {
const cachedResponse = new Response('cached body');
const cacheHit = createFetchWorker({ cachedResponse });
assert.equal(await cacheHit.sandbox.cacheFirst({}, 'cache'), cachedResponse);
assert.equal(cacheHit.calls.fetch, 0);
const offlineHit = createFetchWorker({ cachedResponse, fetchError: true });
assert.equal(await offlineHit.sandbox.networkFirst({}, 'cache'), cachedResponse);
const offlineMiss = createFetchWorker({ fetchError: true });
assert.equal((await offlineMiss.sandbox.networkFirst({}, 'cache')).status, 504);
});
test('player script assets refresh from network and retain offline cache fallback', async () => {
const cachedResponse = new Response('old script body');
const online = createFetchWorker({ cachedResponse });
let onlineResponsePromise;
online.listeners.get('fetch')({
request: new Request('http://localhost/assets/player-script/rendering.js?v=2.14.0'),
respondWith(promise) { onlineResponsePromise = promise; }
});
assert.equal(await onlineResponsePromise, online.networkResponse);
assert.equal(online.calls.fetch, 1);
const offline = createFetchWorker({ cachedResponse: cachedResponse, fetchError: true });
let offlineResponsePromise;
offline.listeners.get('fetch')({
request: new Request('http://localhost/assets/player-script/rendering.js?v=2.14.0'),
respondWith(promise) { offlineResponsePromise = promise; }
});
assert.equal(await offlineResponsePromise, cachedResponse);
});