Progressive Web Apps (PWA)

Testing PWAs

18 min Lesson 19 of 30

Testing PWAs

Thorough testing ensures your Progressive Web App works reliably across devices and network conditions. This lesson covers essential testing tools and strategies including Lighthouse, service worker testing, offline testing, and automated PWA testing.

Lighthouse Testing

Lighthouse is Google's automated tool for auditing PWA quality, performance, accessibility, and SEO.

// Running Lighthouse from command line // Install Lighthouse globally npm install -g lighthouse // Run basic audit lighthouse https://your-pwa.com --output html --output-path ./report.html // Run PWA-specific audit lighthouse https://your-pwa.com --preset=pwa --output json --output-path ./pwa-audit.json // Run with Chrome flags (e.g., mobile simulation) lighthouse https://your-pwa.com \ --emulated-form-factor=mobile \ --throttling-method=simulate \ --output html // Run only specific categories lighthouse https://your-pwa.com \ --only-categories=performance,pwa,accessibility \ --output json // Custom configuration lighthouse https://your-pwa.com --config-path=./lighthouse-config.js
// lighthouse-config.js - Custom Lighthouse configuration module.exports = { extends: 'lighthouse:default', settings: { onlyCategories: ['performance', 'pwa'], throttlingMethod: 'simulate', throttling: { rttMs: 40, throughputKbps: 10240, cpuSlowdownMultiplier: 1 }, formFactor: 'mobile', screenEmulation: { mobile: true, width: 375, height: 667, deviceScaleFactor: 2 } }, audits: [ 'metrics/first-contentful-paint', 'metrics/largest-contentful-paint', 'metrics/cumulative-layout-shift' ] };
Note: Lighthouse can be run via Chrome DevTools, command line, or integrated into CI/CD pipelines. Aim for a PWA score of 90+ in production.

Chrome DevTools Application Panel

Use Chrome DevTools to inspect and debug PWA features in real-time.

// Using Chrome DevTools for PWA debugging // 1. Open DevTools (F12 or Cmd+Option+I) // 2. Navigate to "Application" tab // Service Workers Section: // - View registered service workers // - Update, unregister, or bypass for network // - Test update flow // - View service worker errors // Manifest Section: // - Validate manifest.json // - Check installability criteria // - Preview app icons and screenshots // - Test theme colors // Storage Section: // - Inspect Cache Storage // - View IndexedDB data // - Clear site data // - Check storage quota // Background Services: // - Monitor background sync events // - Test push notifications // - View background fetch progress
<script> // Programmatic testing in DevTools Console // Check service worker registration navigator.serviceWorker.getRegistrations().then(registrations => { console.log('Service Workers:', registrations); }); // Check cache contents caches.keys().then(cacheNames => { console.log('Cache Names:', cacheNames); return Promise.all( cacheNames.map(name => caches.open(name).then(cache => cache.keys())) ); }).then(allRequests => { console.log('Cached URLs:', allRequests.flat()); }); // Check manifest fetch('/manifest.json').then(r => r.json()).then(manifest => { console.log('Manifest:', manifest); }); // Check install prompt let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { deferredPrompt = e; console.log('Install prompt available'); }); // Check if installed if (window.matchMedia('(display-mode: standalone)').matches) { console.log('Running as installed PWA'); } </script>

Service Worker Testing

Test service worker lifecycle, caching strategies, and offline functionality.

// service-worker-test.js - Jest test for service worker const { setupServer } = require('msw/node'); const { rest } = require('msw'); describe('Service Worker', () => { let server; beforeAll(() => { // Mock service worker environment global.self = { addEventListener: jest.fn(), caches: { open: jest.fn(), match: jest.fn() }, skipWaiting: jest.fn(), clients: { claim: jest.fn() } }; }); test('should install and activate', async () => { const installHandler = jest.fn(); const activateHandler = jest.fn(); self.addEventListener('install', installHandler); self.addEventListener('activate', activateHandler); // Trigger install event const installEvent = new Event('install'); self.dispatchEvent(installEvent); expect(installHandler).toHaveBeenCalled(); }); test('should cache resources on install', async () => { const cache = { addAll: jest.fn().mockResolvedValue(undefined) }; self.caches.open.mockResolvedValue(cache); const urlsToCache = ['/', '/styles.css', '/script.js']; // Simulate cache.addAll await self.caches.open('pwa-v1').then(cache => { return cache.addAll(urlsToCache); }); expect(cache.addAll).toHaveBeenCalledWith(urlsToCache); }); test('should serve from cache when available', async () => { const cachedResponse = new Response('Cached content'); self.caches.match.mockResolvedValue(cachedResponse); const request = new Request('/cached-page'); const response = await self.caches.match(request); expect(response).toBe(cachedResponse); }); });
// Integration testing with Puppeteer const puppeteer = require('puppeteer'); describe('PWA Integration Tests', () => { let browser, page; beforeAll(async () => { browser = await puppeteer.launch({ headless: true }); page = await browser.newPage(); }); afterAll(async () => { await browser.close(); }); test('should register service worker', async () => { await page.goto('https://your-pwa.com'); const registration = await page.evaluate(() => { return navigator.serviceWorker.getRegistration(); }); expect(registration).toBeTruthy(); }); test('should work offline', async () => { await page.goto('https://your-pwa.com'); // Wait for service worker to activate await page.waitForFunction(() => { return navigator.serviceWorker.controller !== null; }); // Go offline await page.setOfflineMode(true); // Navigate to cached page await page.goto('https://your-pwa.com/about'); const content = await page.content(); expect(content).toContain('About'); }); test('should cache API responses', async () => { await page.goto('https://your-pwa.com'); // Make API call await page.evaluate(async () => { await fetch('/api/data'); }); // Check cache const cacheHit = await page.evaluate(async () => { const cache = await caches.open('api-cache-v1'); const response = await cache.match('/api/data'); return response !== undefined; }); expect(cacheHit).toBe(true); }); }); </script>
Tip: Use Puppeteer or Playwright for end-to-end PWA testing. They support offline simulation, service worker inspection, and automated user interactions.

Offline Testing

Test your PWA's behavior under various network conditions and offline scenarios.

<script> // Manual offline testing helpers class OfflineTester { static simulateOffline() { // Disconnect from network if ('connection' in navigator) { Object.defineProperty(navigator, 'onLine', { writable: true, value: false }); window.dispatchEvent(new Event('offline')); } } static simulateOnline() { Object.defineProperty(navigator, 'onLine', { writable: true, value: true }); window.dispatchEvent(new Event('online')); } static async testOfflineAccess(url) { try { const response = await fetch(url); if (response.ok) { console.log(`✓ ${url} accessible offline`); return true; } } catch (error) { console.error(`✗ ${url} not accessible offline:`, error); return false; } } static async testCacheStrategy() { const testUrls = [ '/', '/about', '/styles.css', '/script.js' ]; console.log('Testing offline access...'); for (const url of testUrls) { await this.testOfflineAccess(url); } } static async measureCachePerformance() { const urls = ['/', '/api/data', '/images/logo.png']; const results = []; for (const url of urls) { const start = performance.now(); await fetch(url); const duration = performance.now() - start; results.push({ url, duration: `${duration.toFixed(2)}ms` }); } console.table(results); } } // Run offline tests OfflineTester.testCacheStrategy(); </script>
// Automated offline testing with Cypress describe('Offline Functionality', () => { beforeEach(() => { cy.visit('/'); // Wait for service worker cy.wait(2000); }); it('should load home page offline', () => { // Go offline cy.window().then((win) => { cy.stub(win.navigator, 'onLine').value(false); }); cy.reload(); cy.contains('Home').should('be.visible'); }); it('should show offline indicator', () => { cy.window().then((win) => { win.dispatchEvent(new Event('offline')); }); cy.get('.offline-indicator').should('be.visible'); }); it('should queue requests when offline', () => { cy.window().then((win) => { cy.stub(win.navigator, 'onLine').value(false); }); // Try to submit form cy.get('#submitBtn').click(); // Check if request is queued cy.window().then((win) => { return win.indexedDB.open('offline-queue'); }).then((db) => { // Verify queued request exists expect(db).to.exist; }); }); });

Push Notification Testing

Test push notification subscription, delivery, and user interactions.

<script> // Push notification test utilities class PushTester { static async testSubscription() { if (!(' Notification' in window) || !('serviceWorker' in navigator)) { console.error('Push notifications not supported'); return false; } const permission = await Notification.requestPermission(); console.log('Notification permission:', permission); if (permission === 'granted') { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY' }); console.log('Subscription successful:', subscription); return true; } return false; } static async testPushPayload() { const testPayload = { title: 'Test Notification', body: 'This is a test push notification', icon: '/images/icon-192x192.png', badge: '/images/badge-72x72.png', data: { url: '/test' } }; const registration = await navigator.serviceWorker.ready; // Simulate push event registration.active.postMessage({ type: 'SIMULATE_PUSH', payload: testPayload }); console.log('Test push sent'); } static async testNotificationClick() { // Create test notification const registration = await navigator.serviceWorker.ready; await registration.showNotification('Click Test', { body: 'Click to test notification interaction', data: { url: '/test-page' }, actions: [ { action: 'open', title: 'Open' }, { action: 'dismiss', title: 'Dismiss' } ] }); console.log('Test notification displayed'); } static monitorNotificationEvents() { navigator.serviceWorker.addEventListener('message', (event) => { console.log('Service worker message:', event.data); if (event.data.type === 'NOTIFICATION_CLICKED') { console.log('Notification clicked:', event.data.notification); } }); } } // Run push tests PushTester.testSubscription(); PushTester.monitorNotificationEvents(); </script>
Warning: Push notification testing requires a valid VAPID key and server setup. Test in both development and production environments as behavior may differ.

Automated PWA Testing with Workbox

Use Workbox testing utilities for comprehensive service worker testing.

// workbox-test.js - Testing with Workbox const { cacheNames } = require('workbox-core'); const { CacheFirst, NetworkFirst } = require('workbox-strategies'); describe('Workbox Caching Strategies', () => { test('CacheFirst strategy', async () => { const strategy = new CacheFirst({ cacheName: 'images' }); const request = new Request('https://example.com/image.png'); // First call - should fetch from network const response1 = await strategy.handle({ request }); expect(response1.status).toBe(200); // Second call - should fetch from cache const response2 = await strategy.handle({ request }); expect(response2.status).toBe(200); }); test('NetworkFirst strategy with timeout', async () => { const strategy = new NetworkFirst({ cacheName: 'api', networkTimeoutSeconds: 3 }); const request = new Request('https://api.example.com/data'); const response = await strategy.handle({ request }); expect(response).toBeDefined(); }); });
Exercise:
  1. Run a Lighthouse audit on your PWA and achieve a score above 90
  2. Write unit tests for your service worker's caching strategies
  3. Create an integration test suite using Puppeteer that tests offline functionality
  4. Set up automated push notification testing with mock payloads
  5. Implement a CI/CD pipeline that runs Lighthouse tests on every deployment

Testing Checklist

  • Service Worker: Test install, activate, fetch events, and caching strategies
  • Offline Mode: Verify all critical pages work without network connection
  • Manifest: Validate manifest.json format and installability criteria
  • Push Notifications: Test subscription, delivery, and click handling
  • Performance: Measure Core Web Vitals (LCP, FID, CLS)
  • Accessibility: Run WCAG 2.1 compliance tests
  • Cross-Browser: Test on Chrome, Firefox, Safari, Edge
  • Cross-Device: Test on desktop, tablet, and mobile devices
  • Network Conditions: Test under slow 3G, 4G, and offline
  • Update Flow: Test service worker update and skipWaiting behavior

CI/CD Integration

// .github/workflows/pwa-tests.yml - GitHub Actions workflow name: PWA Tests on: [push, pull_request] jobs: lighthouse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Node uses: actions/setup-node@v2 with: node-version: '18' - name: Install dependencies run: npm ci - name: Build PWA run: npm run build - name: Run Lighthouse run: | npm install -g lighthouse lighthouse https://staging.your-pwa.com \ --preset=pwa \ --output=json \ --output-path=./lighthouse-report.json - name: Check PWA score run: | score=$(cat lighthouse-report.json | jq '.categories.pwa.score * 100') if [ $score -lt 90 ]; then echo "PWA score $score is below 90" exit 1 fi unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Run unit tests run: npm test e2e-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Run Puppeteer tests run: npm run test:e2e