Progressive Web Apps (PWA)
Testing PWAs
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:
- Run a Lighthouse audit on your PWA and achieve a score above 90
- Write unit tests for your service worker's caching strategies
- Create an integration test suite using Puppeteer that tests offline functionality
- Set up automated push notification testing with mock payloads
- 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