161 lines
6.7 KiB
JavaScript
161 lines
6.7 KiB
JavaScript
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);
|
|
}); |