Modern QA2026Service Worker Testing — tiles
Log inJoin
20 / 66 · 09 Mobile & Cross-Platform Testing · PWA Testing← prev⊞ allnext →☰ Read as one page

4.2Service Worker Testing

Service workers intercept network requests and enable offline functionality. They operate in a lifecycle that must be tested independently:

// tests/pwa/service-worker.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Service Worker Lifecycle', () => {
    test('service worker registers on first visit', async ({ page }) => {
        await page.goto('/');

        // Wait for SW registration
        const swRegistered = await page.evaluate(async () => {
            const registration = await navigator.serviceWorker.ready;
            return registration.active !== null;
        });
        expect(swRegistered).toBe(true);
    });

    test('app works offline after caching', async ({ page, context }) => {
        // First visit: let the SW cache resources
        await page.goto('/');
        await page.waitForLoadState('networkidle');

        // Verify critical resources are cached
        const cachedUrls = await page.evaluate(async () => {
            const cache = await caches.open('app-shell-v1');
            const keys = await cache.keys();
            return keys.map(k => new URL(k.url).pathname);
        });
        expect(cachedUrls).toContain('/');
        expect(cachedUrls).toContain('/styles/main.css');
        expect(cachedUrls).toContain('/scripts/app.js');

        // Go offline
        await context.setOffline(true);

        // Navigate -- should work from cache
        await page.goto('/');
        await expect(page.locator('h1')).toBeVisible();

        // Verify offline indicator is shown
        await expect(page.locator('[data-testid="offline-banner"]')).toBeVisible();

        // Restore connectivity
        await context.setOffline(false);
    });

    test('stale content is updated when back online', async ({ page, context }) => {
        await page.goto('/');
        await page.waitForLoadState('networkidle');

        // Go offline, navigate
        await context.setOffline(true);
        await page.goto('/dashboard');
        const offlineContent = await page.locator('[data-testid="data-timestamp"]').textContent();

        // Come back online
        await context.setOffline(false);
        await page.reload();
        await page.waitForLoadState('networkidle');
        const onlineContent = await page.locator('[data-testid="data-timestamp"]').textContent();

        // Content should be fresher after reconnection
        expect(onlineContent).not.toBe(offlineContent);
    });

    test('service worker updates when new version deployed', async ({ page }) => {
        await page.goto('/');
        await page.waitForLoadState('networkidle');

        // Check if the SW detects a new version
        const updateAvailable = await page.evaluate(async () => {
            const reg = await navigator.serviceWorker.getRegistration();
            return new Promise((resolve) => {
                if (reg?.waiting) {
                    resolve(true);
                    return;
                }
                reg?.addEventListener('updatefound', () => {
                    resolve(true);
                });
                setTimeout(() => resolve(false), 5000);
            });
        });

        // If an update is available, verify the update prompt appears
        if (updateAvailable) {
            await expect(page.locator('[data-testid="update-prompt"]')).toBeVisible();
        }
    });
});