本文へ移動
cccskills
無料GitHub で公開

service-worker-lifecycle-tests

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()). The general Playwright SW harness, `service-worker-mock` unit tests, and cache-strategy assertions live in references/. Use when authoring the baseline lifecycle spec, when a deploy leaves users stuck on the old service worker, or when a site's caching / offline behavior is uncovered.

日本語の概要は準備中です。原文の説明を表示しています。

testland/qaスター 13(GitHub 13 + cccskills 0)
連携ツールPlaywright

実行スコア 未検証

インストール方法を見る

含まれるファイル(5)

  • SKILL.md16.3 KB
  • .tessl-plugin/plugin.json1.2 KB
  • references/advanced-service-worker-tests.md2.5 KB
  • references/playwright-sw-harness.md6.2 KB
  • references/upgrade-path.md3.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

service-worker-lifecycle-tests

Overview

A service worker moves through six formal states per the W3C spec sw-spec: "parsed, installing, installed, activating, activated, redundant". Most "PWA broke after deploy" bugs are lifecycle bugs - a v2 SW stuck in installed (waiting) behind a v1 that won't release control; a skipWaiting() that activates v2 but leaves v1's caches alive; a Clients.claim() race against a hot- reload that flips the navigator.serviceWorker.controller mid-fetch.

This skill produces the per-SW lifecycle spec - a Playwright file with one test per transition cell plus a worked v1 → v2 upgrade-path test. The builder itself is laser-focused on the state machine; the general Playwright harness (context.serviceWorkers() + waitForEvent('serviceworker') patterns, service-worker-mock unit tests) and per-cache-strategy assertions are in references/playwright-sw-harness.md.

Composes with:

  • add-to-homescreen-flow-tests (references/install-flow-reference.md) - the install-gate Stage 1 service-worker-registered prerequisite, which this builder takes as input (assumes registration already works).
  • workbox-tests - the workbox-window event vocabulary (installed, waiting, controlling, activated, redundant) is the page-side observable for the same state machine asserted here from the SW side.

When to use

  • New PWA - author the baseline lifecycle spec before the team ships any SW logic that mutates state.
  • Upgrade-path regression - a deploy left users on v1 because v2's skipWaiting() was missing; emit the per-transition test cells to catch it next time.
  • "Stale UI after deploy" reports - the test cells localize whether the bug is skipWaiting, Clients.claim, or cache invalidation.
  • Migrating from Workbox workbox-window to a hand-rolled registration helper - assert the same five events still fire.

Workflow

Step 1 - Capture the SW under test

Read the SW file the team ships and record three facts:

FactWhere to find
Registration URL<script> tag or navigator.serviceWorker.register('/sw.js') in the page bundle
Whether skipWaiting() is called in installself.skipWaiting() inside an install listener
Whether Clients.claim() is called in activateself.clients.claim() inside an activate listener
# Inventory
grep -E "skipWaiting|clients\.claim" src/sw.ts > sw-lifecycle-inventory.txt

Per mdn-sw, skipWaiting() activates sooner and Clients.claim() claims existing pages. The combination matters - skipWaiting without claim activates the new SW but leaves current tabs uncontrolled until reload.

Step 2 - Test: state machine entry - fresh install

On first access to a SW-controlled page the worker downloads and installs immediately per mdn-sw. The first-install test:

import { test, expect } from '@playwright/test';

test('first install transitions parsed → installing → installed → activating → activated', async ({ context, page }) => {
  await page.goto('https://localhost:3000/');

  // Capture statechange events as soon as the SW is reachable
  const observed = await page.evaluate(() => new Promise<string[]>((resolve) => {
    const states: string[] = [];
    navigator.serviceWorker.register('/sw.js').then(reg => {
      const w = reg.installing ?? reg.waiting ?? reg.active;
      if (!w) { resolve(states); return; }
      states.push(w.state);
      w.addEventListener('statechange', () => {
        states.push(w.state);
        if (w.state === 'activated' || w.state === 'redundant') resolve(states);
      });
    });
    // Hard timeout
    setTimeout(() => resolve(states), 10_000);
  }));

  // Per sw-spec, the formal enum is parsed / installing / installed / activating / activated / redundant.
  // Expect at minimum installed and activated in the trace.
  expect(observed).toContain('installed');
  expect(observed).toContain('activated');
});

statechange fires on the corresponding ServiceWorker object whenever its state attribute changes per sw-spec.

Step 3 - Test: event.waitUntil extends the install phase

Per mdn-sw, waitUntil() on an install / activate event holds functional events (fetch, push) until its promise resolves - so a slow precache keeps the SW in installing.

test('SW install with slow precache stays in installing until waitUntil resolves', async ({ page, context }) => {
  await page.goto('https://localhost:3000/');

  const phase = await page.evaluate(() => new Promise<string>((resolve) => {
    navigator.serviceWorker.register('/sw-slow-install.js').then(reg => {
      const w = reg.installing;
      if (!w) { resolve('no installing'); return; }
      // Sample state at ~500ms - the slow install should still be 'installing'
      setTimeout(() => resolve(w.state), 500);
    });
  }));

  expect(['installing', 'installed']).toContain(phase);
});

This requires a slow-install SW fixture under tests/fixtures/sw-slow-install.js that calls event.waitUntil(new Promise(r => setTimeout(r, 2000))) inside its install handler.

Step 4 - Test: skipWaiting() collapses the waiting phase

Per mdn-skipwaiting, skipWaiting() "causes the waiting service worker to become the active service worker." Test the transition:

test('skipWaiting() makes v2 active without page reload', async ({ context, page }) => {
  // Load v1
  await page.goto('https://localhost:3000/?sw-version=1');
  await page.waitForFunction(() => navigator.serviceWorker.controller !== null);

  // Deploy v2 (the test server flips the SW response based on a query param header)
  await page.evaluate(async () => {
    const reg = await navigator.serviceWorker.getRegistration();
    await reg!.update();
  });

  const waitingThenActive = await page.evaluate(() => new Promise<string>(async (resolve) => {
    const reg = await navigator.serviceWorker.getRegistration();
    // v2 should land in waiting…
    if (reg!.waiting) {
      // …then transition to activating when skipWaiting() fires
      reg!.waiting.addEventListener('statechange', e => {
        resolve((e.target as ServiceWorker).state);
      });
    } else {
      resolve(reg!.active?.state ?? 'unknown');
    }
  }));

  // After skipWaiting(), v2 reaches activated without manual reload
  expect(['activating', 'activated']).toContain(waitingThenActive);
});

If the SW under test does not call skipWaiting(), this test must assert v2 stays in installed/waiting until all v1-controlled tabs close - flip the expectation accordingly.

Step 5 - Test: Clients.claim() flips the controller

Per mdn-claim, Clients.claim() lets an active SW set itself as the controller for all in-scope clients.

test('clients.claim() makes v2 control the page mid-session', async ({ page, context }) => {
  // v1 is active and controlling
  await page.goto('https://localhost:3000/?sw-version=1');
  const v1ScriptURL = await page.evaluate(() => navigator.serviceWorker.controller?.scriptURL);
  expect(v1ScriptURL).toMatch(/sw-v1/);

  // Trigger v2 deploy + claim
  await page.evaluate(async () => {
    const reg = await navigator.serviceWorker.getRegistration();
    await reg!.update();
  });

  // After claim() in v2's activate handler, controller flips
  const v2ScriptURL = await page.waitForFunction(() => {
    const c = navigator.serviceWorker.controller;
    return c && c.scriptURL.includes('sw-v2') ? c.scriptURL : null;
  });
  expect(await v2ScriptURL.jsonValue()).toMatch(/sw-v2/);
});

Per mdn-sw, skipWaiting() and claim() together force-activate the new SW; one without the other leaves a gap (see Step 1).

Step 6 - Test: old SW transitions to redundant

Per sw-spec, redundant is the terminal state - the old SW enters it when superseded. The transition is the cleanup signal the activate handler typically uses to drop old caches:

test('old SW transitions to redundant after v2 activates', async ({ context, page }) => {
  await page.goto('https://localhost:3000/?sw-version=1');

  const v1 = await page.evaluate(async () => {
    const reg = await navigator.serviceWorker.getRegistration();
    return reg!.active;
  });

  const finalState = await page.evaluate(() => new Promise<string>(async (resolve) => {
    const reg = await navigator.serviceWorker.getRegistration();
    const oldSW = reg!.active;
    if (!oldSW) { resolve('no old'); return; }
    oldSW.addEventListener('statechange', () => {
      if (oldSW.state === 'redundant') resolve('redundant');
    });
    // Trigger v2 update path
    await reg!.update();
    setTimeout(() => resolve(oldSW.state), 8_000);
  }));

  expect(finalState).toBe('redundant');
});

Step 7 - Test: navigator.serviceWorker.controller semantics

Per mdn-sw, navigator.serviceWorker.controller returns the SW controlling the current page, or null if no SW controls it (e.g. hard-reload, force-bypass, or fresh first load before activation). Test the boundary cases:

test('controller is null on first hard-reload, set after activation', async ({ page, context }) => {
  await page.goto('https://localhost:3000/');

  // First load: controller may be null until claim() runs (or until next navigation)
  const initialController = await page.evaluate(() => navigator.serviceWorker.controller?.scriptURL ?? null);
  // Either null (no claim) or set (claim called in activate)

  // After a reload, the SW must be controlling
  await page.reload();
  const reloadedController = await page.evaluate(() => navigator.serviceWorker.controller?.scriptURL);
  expect(reloadedController).toBeTruthy();
});

Per mdn-sw: a hard-reload (Ctrl+Shift+R) bypasses the SW - controller is null for that page even if an SW is registered. Playwright's page.reload({ waitUntil: 'networkidle' }) is a soft reload; the SW controls it.

Step 8 - Test: updatefound event on registration

Per mdn-sw, the registration object fires updatefound when a new SW is in the installing state. This is the canonical "deploy detected" event for "Update available" banners:

test('updatefound fires when a new SW is found', async ({ page, context }) => {
  await page.goto('https://localhost:3000/?sw-version=1');

  const found = await page.evaluate(() => new Promise<boolean>(async (resolve) => {
    const reg = await navigator.serviceWorker.getRegistration();
    reg!.addEventListener('updatefound', () => resolve(true));
    await reg!.update();
    setTimeout(() => resolve(false), 5_000);
  }));

  expect(found).toBe(true);
});

Step 9 - Emit the lifecycle spec artifact

Write tests/sw-lifecycle.spec.ts with all eight test cells above, paired with a tests/sw-lifecycle-coverage.yaml matrix mapping each spec to its state-machine cell and reference. The full matrix and the worked v1 -> v2 upgrade-path spec are in references/upgrade-path.md:

# 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, skipwaiting, claim, redundant, controller_semantics, updatefound

CI gates on every matrix row having at least one passing test.

Worked example: a v1 → v2 upgrade-path test

The full worked tests/sw-upgrade-path.spec.ts for an SW using skipWaiting()

  • Clients.claim() is in references/upgrade-path.md. It exercises four state transitions (installed → activating in v2, activated → redundant in v1) plus the cache-cleanup convention. Pair it with the per-transition cells from Steps 2 - 8 for the full lifecycle surface.

Anti-patterns

Anti-patternWhy it failsFix
Assert state by polling reg.installing vs reg.waiting vs reg.activeRace: the field flips between samplesListen on statechange (Step 2)
Skip the waitUntil testSlow installs that block fetch/push are invisible until prodStep 3 with a fixture SW
Test only the skipWaiting() halfWithout claim(), current tabs stay on v1 forever per mdn-claimStep 5 covers the second half
Hard-reload between v1 and v2Bypasses the SW per mdn-sw; loses the lifecycle signalUse reg.update() (Steps 5, 6)
Assume updatefound fires every navigationPer mdn-sw, only when a new SW is foundStep 8 explicitly drives update()
Treat redundant as an errorIt's the terminal cleanup state per sw-spec for superseded SWsStep 6 asserts it as success
Skip the per-version cache-cleanup testA v2 that activates but doesn't delete v1 caches doubles storageInclude in the worked upgrade path test
Pin the exact transition orderingThe spec allows intermediate states to be observed or not depending on timingAssert presence with toContain, not exact array equality (Step 2)

Limitations

  • waitUntil test timing is heuristic. Step 3 samples at 500ms; faster machines may see installed already. Use waitForFunction with a state predicate for production-grade tests.
  • controller on first load can be null or set depending on whether the SW calls claim() per mdn-sw; Step 7 covers both branches. Tests that hard-pin to one will flake.
  • Cross-tab lifecycle isn't observed by this builder. Two open tabs share the SW registration but each has its own serviceWorker.controller; a full multi-tab assertion needs a second context.newPage().
  • Hard-reload (Ctrl+Shift+R) behavior can't be triggered programmatically in Playwright - page.reload() is always soft. Manual smoke covers this cell.
  • The push and fetch events that waitUntil gates aren't tested here directly; pair with web-push-tests (push side) and the cache-strategy tests in references/playwright-sw-harness.md (fetch side).
  • Browser variance. Firefox and WebKit implement the state machine but report statechange with slightly different intermediate samples per sw-spec; the assertions here use toContain to absorb the variance.

References

  • W3C Service Worker spec (ServiceWorkerState enum, formal state values, statechange event semantics) - sw-spec.
  • MDN Service Worker API (lifecycle prose, waitUntil, skipWaiting + claim pairing, controller semantics) - mdn-sw.
  • MDN ServiceWorkerGlobalScope.skipWaiting() - mdn-skipwaiting.
  • MDN Clients.claim() - mdn-claim.
  • General Playwright SW harness, service-worker-mock unit tests, cache-strategy assertions - references/playwright-sw-harness.md and references/advanced-service-worker-tests.md.
  • Composes: add-to-homescreen-flow-tests, workbox-tests.
  • Sibling builders: offline-fallback-tests, add-to-homescreen-flow-tests.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

Builds a CI gate that fails the build on **new** WCAG / a11y violations introduced by a PR while grandfathering pre-existing violations on a per-rule / per-page baseline. Aggregates verdicts from axe-core / pa11y / Lighthouse a11y / WAVE / IBM Equal Access scans. Use when a project has accumulated a11y debt and a strict "zero violations" gate would block every PR - the ratchet pattern lets the team ship while preventing regressions.

日本語の概要は準備中です。原文の説明を表示しています。

testland/qa132026年10月20日 更新

Workflow skill that builds an A/B-test validity checklist from an experiment proposal, walking the canonical design-correctness gates - pre-registered OEC/power/guardrails, randomization unit + SRM check, assignment integrity, telemetry, peeking discipline, novelty/primacy, post-experiment SRM re-check - into a per-experiment checklist + sign-off form. Use when launching, auditing, or governing an experiment. For pitfall mechanics (guardrails, peeking) see experiment-results-interpreter's references; to read an already-valid result use experiment-results-interpreter; for per-SDK harness tests use experiment-sdk-testing - this gates DESIGN, not SDK code.

日本語の概要は準備中です。原文の説明を表示しています。

testland/qa132026年10月20日 更新

The single install-flow skill: the reference contract for the PWA install flow (installability gate fields, `beforeinstallprompt` handshake, per-platform paths - Android WebAPK / iOS Share menu / Firefox no-op - and the `display-mode` post-install signal, in references/install-flow-reference.md) plus the build-an-X workflow that emits the Add-to-Home-Screen suite. Walks the four-stage timeline, emitting one test per gate cell per [web.dev/articles/install-criteria][install-criteria], the deferred-prompt → `prompt()` → `userChoice` chain per [web.dev/articles/customize-install][customize-install], the iOS Safari manual-metadata branch (`apple-touch-icon`) per [web.dev/learn/pwa/installation][learn-pwa], and the post-install `(display-mode: standalone)` MQ assertion. Output: a Playwright spec with per-stage cells plus a coverage matrix. Use when a PWA's manifest, icons, or install handler change, when install conversion drops at an unknown stage, or when triaging a flaky install assertion.

日本語の概要は準備中です。原文の説明を表示しています。

testland/qa132026年10月20日 更新

Build-an-X workflow that uses an LLM to map existing tests to spec sections - given a spec doc + the test suite, the LLM identifies which tests cover which sections, surfaces uncovered sections (gap), and recommends specific tests to add. Output is a coverage matrix per spec ID. Scope is mapping tests that already exist and naming the gaps, not authoring tests for new acceptance criteria. Use when a spec doc and a test suite both exist but nobody can say which requirements are actually covered - before a release sign-off, an audit, or a decision about where to spend the next round of test effort.

日本語の概要は準備中です。原文の説明を表示しています。

testland/qa132026年10月20日 更新

Generates tests from natural-language specs (acceptance criteria, user stories, requirements) using an LLM, with confidence scoring per test case (LLM self-assessment plus heuristics: assertion quality, naming, completeness), batching uncertain cases for human review, and integration with the team's existing test framework. Use when the user asks to generate unit tests from acceptance criteria, convert user stories to test cases, automate test creation from requirements, or augment a spec-driven test suite with AI-generated stubs that are then curated before merge.

日本語の概要は準備中です。原文の説明を表示しています。

testland/qa132026年10月20日 更新

Configures Allure Report (test-runner adapter install, `allure-results` directory wiring, `categories.json` for failure classification, `history-trend.json` retention via the copy-history-between-runs pattern), runs the Allure CLI to convert `allure-results` to a static HTML site, and uploads the report as a CI artifact. Use when the team needs richer test reporting than JUnit XML - step-level attachments, per-test history, retry tracking, and severity / epic / feature labeling across framework-agnostic adapters (pytest, Jest, JUnit, TestNG, NUnit, Mocha). As a rich static HTML report generator, it is the open-source alternative to the sunset ExtentReports (JVM/.NET per-test HTML narrative); for hosted cross-run flakiness analytics rather than a static per-run report use currents-integration.

日本語の概要は準備中です。原文の説明を表示しています。

testland/qa132026年10月20日 更新

testland のスキルをすべて見る

このスキルの問題を報告する