Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
browser-apis/service-workers.md
1# Service Worker Testing23## Table of Contents451. [Service Worker Basics](#service-worker-basics)62. [Registration & Lifecycle](#registration--lifecycle)73. [Cache Testing](#cache-testing)84. [Offline Testing](#offline-testing)95. [Push Notifications](#push-notifications)106. [Background Sync](#background-sync)1112## Service Worker Basics1314### Waiting for Service Worker Registration1516```typescript17test("service worker registers", async ({ page }) => {18await page.goto("/pwa-app");1920// Wait for SW to register21const swRegistered = await page.evaluate(async () => {22if (!("serviceWorker" in navigator)) return false;2324const registration = await navigator.serviceWorker.ready;25return !!registration.active;26});2728expect(swRegistered).toBe(true);29});30```3132### Getting Service Worker State3334```typescript35test("check SW state", async ({ page }) => {36await page.goto("/");3738const swState = await page.evaluate(async () => {39const registration = await navigator.serviceWorker.getRegistration();40if (!registration) return null;4142return {43installing: !!registration.installing,44waiting: !!registration.waiting,45active: !!registration.active,46scope: registration.scope,47};48});4950expect(swState?.active).toBe(true);51expect(swState?.scope).toContain(page.url());52});53```5455### Service Worker Context5657```typescript58test("access service worker", async ({ context, page }) => {59await page.goto("/pwa-app");6061// Get all service workers in context62const workers = context.serviceWorkers();6364// Wait for service worker if not yet available65if (workers.length === 0) {66await context.waitForEvent("serviceworker");67}6869const sw = context.serviceWorkers()[0];70expect(sw.url()).toContain("sw.js");71});72```7374## Registration & Lifecycle7576### Testing SW Update Flow7778```typescript79test("service worker updates", async ({ page }) => {80await page.goto("/pwa-app");8182// Check for update83const hasUpdate = await page.evaluate(async () => {84const registration = await navigator.serviceWorker.ready;85await registration.update();8687return new Promise<boolean>((resolve) => {88if (registration.waiting) {89resolve(true);90} else {91registration.addEventListener("updatefound", () => {92resolve(true);93});94// Timeout if no update95setTimeout(() => resolve(false), 5000);96}97});98});99100// If update found, test skip waiting flow101if (hasUpdate) {102await page.evaluate(async () => {103const registration = await navigator.serviceWorker.ready;104registration.waiting?.postMessage({ type: "SKIP_WAITING" });105});106107// Wait for controller change108await page.evaluate(() => {109return new Promise<void>((resolve) => {110navigator.serviceWorker.addEventListener("controllerchange", () => {111resolve();112});113});114});115}116});117```118119### Testing SW Installation120121```typescript122test("verify SW install event", async ({ context, page }) => {123// Listen for service worker before navigating124const swPromise = context.waitForEvent("serviceworker");125126await page.goto("/pwa-app");127128const sw = await swPromise;129130// Evaluate in SW context131const swVersion = await sw.evaluate(() => {132// Access SW globals133return (self as any).SW_VERSION || "unknown";134});135136expect(swVersion).toBe("1.0.0");137});138```139140### Unregistering Service Workers141142```typescript143test.beforeEach(async ({ page }) => {144await page.goto("/");145146// Unregister all service workers for clean state147await page.evaluate(async () => {148const registrations = await navigator.serviceWorker.getRegistrations();149await Promise.all(registrations.map((r) => r.unregister()));150});151152// Clear caches153await page.evaluate(async () => {154const cacheNames = await caches.keys();155await Promise.all(cacheNames.map((name) => caches.delete(name)));156});157});158```159160## Cache Testing161162### Verifying Cached Resources163164```typescript165test("assets are cached", async ({ page }) => {166await page.goto("/pwa-app");167168// Wait for SW to cache assets169await page.evaluate(async () => {170await navigator.serviceWorker.ready;171});172173// Check cache contents174const cachedUrls = await page.evaluate(async () => {175const cache = await caches.open("app-cache-v1");176const requests = await cache.keys();177return requests.map((r) => r.url);178});179180expect(cachedUrls).toContain(expect.stringContaining("/styles.css"));181expect(cachedUrls).toContain(expect.stringContaining("/app.js"));182});183```184185### Testing Cache Strategies186187```typescript188test("cache-first strategy", async ({ page }) => {189await page.goto("/pwa-app");190191// Wait for initial cache192await page.waitForFunction(async () => {193const cache = await caches.open("app-cache-v1");194const keys = await cache.keys();195return keys.length > 0;196});197198// Block network for cached resources199await page.route("**/styles.css", (route) => route.abort());200201// Reload - should work from cache202await page.reload();203204// Verify page still styled (CSS loaded from cache)205const hasStyles = await page.evaluate(() => {206const body = document.body;207const styles = window.getComputedStyle(body);208return styles.fontFamily !== ""; // Has custom font from CSS209});210211expect(hasStyles).toBe(true);212});213```214215### Testing Cache Updates216217```typescript218test("cache updates on new version", async ({ page }) => {219await page.goto("/pwa-app");220221// Get initial cache222const initialCacheKeys = await page.evaluate(async () => {223const cache = await caches.open("app-cache-v1");224const keys = await cache.keys();225return keys.map((r) => r.url);226});227228// Simulate app update by mocking SW response229await page.route("**/sw.js", (route) => {230route.fulfill({231contentType: "application/javascript",232body: `233const VERSION = 'v2';234self.addEventListener('install', (e) => {235e.waitUntil(caches.open('app-cache-v2'));236self.skipWaiting();237});238`,239});240});241242// Trigger update243await page.evaluate(async () => {244const reg = await navigator.serviceWorker.ready;245await reg.update();246});247248// Verify new cache exists249await page.waitForFunction(async () => {250return await caches.has("app-cache-v2");251});252});253```254255## Offline Testing256257This section covers **offline-first apps (PWAs)** that are designed to work offline using service workers, caching, and background sync. For testing **unexpected network failures** (error recovery, graceful degradation), see [error-testing.md](error-testing.md#offline-testing).258259### Simulating Offline Mode260261```typescript262test("app works offline", async ({ page, context }) => {263await page.goto("/pwa-app");264265// Ensure SW is active and content cached266await page.evaluate(async () => {267await navigator.serviceWorker.ready;268});269await page.waitForTimeout(1000); // Allow caching to complete270271// Go offline272await context.setOffline(true);273274// Navigate to cached page275await page.reload();276277// Verify content loads278await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();279280// Verify offline indicator281await expect(page.locator(".offline-badge")).toBeVisible();282283// Go back online284await context.setOffline(false);285await expect(page.locator(".offline-badge")).not.toBeVisible();286});287```288289### Testing Offline Fallback290291```typescript292test("shows offline page for uncached routes", async ({ page, context }) => {293await page.goto("/pwa-app");294await page.evaluate(() => navigator.serviceWorker.ready);295296// Go offline297await context.setOffline(true);298299// Navigate to uncached page300await page.goto("/uncached-page");301302// Should show offline fallback303await expect(page.getByText("You are offline")).toBeVisible();304await expect(page.getByRole("button", { name: "Retry" })).toBeVisible();305});306```307308### Testing Offline Form Submission309310```typescript311test("queues form submission offline", async ({ page, context }) => {312await page.goto("/pwa-app/form");313314// Go offline315await context.setOffline(true);316317// Submit form318await page.getByLabel("Message").fill("Offline message");319await page.getByRole("button", { name: "Send" }).click();320321// Should show queued status322await expect(page.getByText("Queued for sync")).toBeVisible();323324// Go online325await context.setOffline(false);326327// Trigger sync (or wait for automatic)328await page.evaluate(async () => {329const reg = await navigator.serviceWorker.ready;330// Manually trigger sync for testing331await (reg as any).sync?.register("form-sync");332});333334// Verify submission completed335await expect(page.getByText("Message sent")).toBeVisible({ timeout: 10000 });336});337```338339## Push Notifications340341### Mocking Push Subscription342343```typescript344test("handles push subscription", async ({ page, context }) => {345// Grant notification permission346await context.grantPermissions(["notifications"]);347348await page.goto("/pwa-app");349350// Subscribe to push351const subscription = await page.evaluate(async () => {352const reg = await navigator.serviceWorker.ready;353const sub = await reg.pushManager.subscribe({354userVisibleOnly: true,355applicationServerKey: "test-key",356});357return sub.toJSON();358});359360expect(subscription.endpoint).toBeDefined();361});362```363364### Testing Push Message Handling365366```typescript367test("handles push notification", async ({ context, page }) => {368await context.grantPermissions(["notifications"]);369await page.goto("/pwa-app");370371// Wait for SW372const swPromise = context.waitForEvent("serviceworker");373const sw = await swPromise;374375// Simulate push message to service worker376await sw.evaluate(async () => {377// Dispatch push event378const pushEvent = new PushEvent("push", {379data: new PushMessageData(380JSON.stringify({ title: "Test", body: "Push message" }),381),382});383self.dispatchEvent(pushEvent);384});385386// Note: Actual notification display testing is limited in Playwright387// Focus on verifying the SW handles the push correctly388});389```390391### Testing Notification Click392393```typescript394test("notification click opens page", async ({ context, page }) => {395await context.grantPermissions(["notifications"]);396await page.goto("/pwa-app");397398// Store notification URL target399let notificationUrl = "";400401// Listen for new pages (notification click opens new page)402context.on("page", (newPage) => {403notificationUrl = newPage.url();404});405406// Trigger notification via SW407await page.evaluate(async () => {408const reg = await navigator.serviceWorker.ready;409await reg.showNotification("Test", {410body: "Click me",411data: { url: "/notification-target" },412});413});414415// Simulate clicking notification (via SW)416const sw = context.serviceWorkers()[0];417await sw.evaluate(() => {418self.dispatchEvent(419new NotificationEvent("notificationclick", {420notification: { data: { url: "/notification-target" } } as any,421}),422);423});424425// Verify navigation occurred426await page.waitForTimeout(1000);427// Check if new page opened or current page navigated428});429```430431## Background Sync432433### Testing Background Sync Registration434435```typescript436test("registers background sync", async ({ page }) => {437await page.goto("/pwa-app");438439// Register sync440const syncRegistered = await page.evaluate(async () => {441const reg = await navigator.serviceWorker.ready;442if (!("sync" in reg)) return false;443444await (reg as any).sync.register("my-sync");445return true;446});447448expect(syncRegistered).toBe(true);449});450```451452### Testing Sync Event453454```typescript455test("sync event fires when online", async ({ context, page }) => {456await page.goto("/pwa-app");457458// Queue data while offline459await context.setOffline(true);460461await page.evaluate(async () => {462// Store data in IndexedDB for sync463const db = await openDB();464await db.put("sync-queue", { id: 1, data: "test" });465466// Register sync467const reg = await navigator.serviceWorker.ready;468await (reg as any).sync.register("data-sync");469});470471// Track sync completion472await page.evaluate(() => {473window.syncCompleted = false;474navigator.serviceWorker.addEventListener("message", (e) => {475if (e.data.type === "SYNC_COMPLETE") {476window.syncCompleted = true;477}478});479});480481// Go online482await context.setOffline(false);483484// Wait for sync to complete485await page.waitForFunction(() => window.syncCompleted, { timeout: 10000 });486});487```488489## Anti-Patterns to Avoid490491| Anti-Pattern | Problem | Solution |492| ------------------------------ | ----------------------- | -------------------------------------------- |493| Not clearing SW between tests | Tests affect each other | Unregister SW in beforeEach |494| Not waiting for SW ready | Race conditions | Always await `navigator.serviceWorker.ready` |495| Testing in isolation only | Misses real SW behavior | Test with actual caching |496| Hardcoded timeouts for caching | Flaky tests | Wait for cache to populate |497| Ignoring SW update cycle | Missing update bugs | Test install, activate, update flows |498499## Related References500501- **Network Failures**: See [error-testing.md](error-testing.md#offline-testing) for unexpected network failure patterns502- **Browser APIs**: See [browser-apis.md](browser-apis.md) for permissions503- **Network Mocking**: See [network-advanced.md](../advanced/network-advanced.md) for network interception504- **Browser Extensions**: See [browser-extensions.md](../testing-patterns/browser-extensions.md) for extension service worker patterns505