Build-an-X workflow that emits per-SW state-transition tests covering the six `ServiceWorkerState` values per [w3c-github-io/ServiceWorker][sw-spec] (`parsed → installing → installed → activating → activated → redundant`), the `install` / `activate` / `fetch` event handlers per [MDN Service Worker API][mdn-sw], `event.waitUntil()` lifetime extension, `ServiceWorkerGlobalScope.skipWaiting()` and `Clients.claim()` upgrade-path semantics, the `statechange` event on `ServiceWorker` objects, `ServiceWorkerRegistration.update()`, and `navigator.serviceWorker.controller` checks. Output: a Playwright spec file with one test per transition plus a clean upgrade-path test (v1 active → v2 installed/waiting → v2 activated, with claim()). Use when authoring the baseline lifecycle spec, or when a deploy leaves users stuck on the old service worker - scoped to the state machine and the `skipWaiting` / `clients.claim` upgrade path, not to general service-worker assertion or cache-strategy patterns.
69
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Companion detail for service-worker-lifecycle-tests. The per-transition test
cells (Steps 2 to 8) are the runnable core and stay in SKILL.md; this file holds
the coverage matrix and the worked upgrade-path spec.
tests/sw-lifecycle-coverage.yaml maps each spec to its state-machine cell.
CI gates on every matrix row having at least one passing test.
# tests/sw-lifecycle-coverage.yaml
matrix:
fresh_install:
spec: "first install transitions parsed → installing → installed → activating → activated"
states: [parsed, installing, installed, activating, activated]
ref: sw-spec ServiceWorkerState enum
waituntil:
spec: "SW install with slow precache stays in installing until waitUntil resolves"
states: [installing]
ref: mdn-sw waitUntil semantics
skipwaiting:
spec: "skipWaiting() makes v2 active without page reload"
states: [installed, activating, activated]
ref: mdn-skipwaiting
claim:
spec: "clients.claim() makes v2 control the page mid-session"
states: [activated]
ref: mdn-claim
redundant:
spec: "old SW transitions to redundant after v2 activates"
states: [redundant]
ref: sw-spec ServiceWorkerState enum
controller_semantics:
spec: "controller is null on first hard-reload, set after activation"
ref: mdn-sw controller property
updatefound:
spec: "updatefound fires when a new SW is found"
ref: mdn-sw updatefound eventFor an SW that uses skipWaiting() + Clients.claim():
// tests/sw-upgrade-path.spec.ts
import { test, expect } from '@playwright/test';
test('v1 → v2 upgrade: skip waiting + claim, old cache deleted', async ({ page, context }) => {
// 1. Land on v1 and confirm it controls the page.
await page.goto('https://localhost:3000/?sw-version=1');
await page.waitForFunction(() => navigator.serviceWorker.controller !== null);
const v1Cache = await page.evaluate(async () => {
const names = await caches.keys();
return names.find(n => n.endsWith('-v1'));
});
expect(v1Cache).toBeTruthy();
// 2. Trigger v2 deploy.
await page.evaluate(async () => {
const reg = await navigator.serviceWorker.getRegistration();
await reg!.update();
});
// 3. Wait for the controller to flip to v2 (skipWaiting + claim).
const v2Controller = await page.waitForFunction(() => {
const c = navigator.serviceWorker.controller;
return c && c.scriptURL.endsWith('sw-v2.js') ? c.scriptURL : null;
}, { timeout: 10_000 });
expect(await v2Controller.jsonValue()).toMatch(/sw-v2/);
// 4. Confirm v1 caches are deleted by v2's activate handler.
const remainingCaches = await page.evaluate(() => caches.keys());
expect(remainingCaches.some((n: string) => n.endsWith('-v1'))).toBe(false);
expect(remainingCaches.some((n: string) => n.endsWith('-v2'))).toBe(true);
});This single test exercises four state transitions (installed → activating in v2, activated → redundant in v1) plus the cache-cleanup convention.
Source references: