CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

testland/service-worker-lifecycle-tests

Use when a service worker needs lifecycle tests, users are stuck on an old version after deploy, or caching and offline behavior is untested. Tests each state change from installing to activated, waitUntil, skipWaiting and clients.claim, update() and a clean v1 to v2 upgrade with Playwright.

Quality

92%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

test-fixtures.mdreferences/

Test fixtures: versioned SW server

Companion to service-worker-lifecycle-tests. Lifecycle tests need a server whose /sw.js changes between v1 and v2 while the page stays open, and a slow-install worker. A query parameter on the page does not do this: registration.update() re-fetches the registered script URL, so the script bytes must change on the server.

Server

The test flips the version with POST /__test/sw-version/<n>; the SW reports its version over postMessage, and its cache is named after it. Cache-Control: no-store keeps the update check from reading a cached script. Serving from http://localhost is enough: browsers treat localhost as a secure context, which service workers require (MDN secure contexts).

// tests/fixtures/sw-server.mjs - serves the page and a versioned SW
import http from 'node:http';

let version = 1;                       // flipped by the test, never by the page
const page = `<!doctype html><title>sw</title>
<script>navigator.serviceWorker.register('/sw.js')</script>`;

const sw = (v) => `
const VERSION = ${v};
const CACHE = 'app-v' + VERSION;
self.addEventListener('install', (e) => {
  e.waitUntil(caches.open(CACHE).then((c) => c.addAll(['/'])));
  self.skipWaiting();                  // remove to test the waiting path
});
self.addEventListener('activate', (e) => {
  e.waitUntil((async () => {
    for (const k of await caches.keys()) if (k !== CACHE) await caches.delete(k);
    await self.clients.claim();
  })());
});
self.addEventListener('message', (e) => {
  if (e.data === 'version') e.ports[0].postMessage(VERSION);
});
`;

const slowSw = `
self.addEventListener('install', (e) => {
  e.waitUntil(new Promise((r) => setTimeout(r, 2000)));   // slow precache
});
`;

http.createServer((req, res) => {
  if (req.method === 'POST' && req.url.startsWith('/__test/sw-version/')) {
    version = Number(req.url.split('/').pop());
    return res.end(String(version));
  }
  if (req.url === '/sw.js' || req.url === '/sw-slow-install.js') {
    res.setHeader('Content-Type', 'text/javascript');
    res.setHeader('Cache-Control', 'no-store');
    return res.end(req.url === '/sw.js' ? sw(version) : slowSw);
  }
  res.setHeader('Content-Type', 'text/html');
  res.end(page);
}).listen(3000);

The server holds one version for everyone, so run lifecycle specs with one worker (--workers=1, or a Playwright project with workers: 1). With parallel workers, one test's flip to v2 lands in another test's v1 assertions.

Playwright config

// playwright.config.mjs (lifecycle project)
export default {
  testMatch: /sw-lifecycle\.spec\.mjs/,
  workers: 1,
  use: { browserName: 'chromium' },
  webServer: { command: 'node tests/fixtures/sw-server.mjs', port: 3000 },
};

webServer starts the server before the tests and stops it after (Playwright web server).

SKILL.md

tile.json