Testing & Debugging PWAs¶
Testing a Progressive Web App means testing behavior that ordinary web apps do not have: a service worker that persists between visits and can serve stale code, caches that must stay consistent across deployments, an install flow that each browser gates differently, push messages that arrive when no page is open, and an offline mode that only shows up when the network fails. A PWA test strategy has to cover all of it across Chromium, Firefox and WebKit, over time as well as in a single page load, because many of the worst bugs appear only on the second visit, after the next deploy, or after the browser has quietly terminated the worker. This section covers the tools and techniques: DevTools for interactive debugging, automated tests for regressions, and auditing for release gates.
Key takeaways
- PWA bugs cluster in time: the second visit, the next deployment, a worker restart, an eviction. Every test plan needs scenarios that span sessions and versions, not only single page loads.
- The highest-value areas to test are update flows, cache correctness, offline navigation, installability, and push delivery, in roughly that order of how often they break in production.
- Use a test pyramid: unit tests for pure service worker logic (routing, cache naming, payload parsing), browser integration tests for lifecycle and caching against a real server, a few end-to-end journeys, and scheduled manual checks on real devices.
- Service workers need a secure context: test on
localhost(a secure context by definition), on real HTTPS, or through a tunnel. Never on a LAN IP over HTTP. - Always test from a clean profile and from an upgraded state (previous version installed, caches populated). Most update bugs only reproduce in the second.
- Lighthouse no longer has a PWA category (removed in Lighthouse 12, shipped in Chrome 126 DevTools), so installability and offline checks must come from your own tests and DevTools.
Why PWAs need their own testing strategy¶
A classic web app is stateless from the browser's point of view: deploy new HTML and JavaScript, users reload, they get the new code. A PWA breaks that assumption in four ways, and each one creates a class of bugs that a conventional test suite misses.
Code persists on the client. The service worker and everything it precached stay on the device after the tab closes. A buggy worker keeps serving old or broken responses to every returning user until a fixed worker is installed and activated, and activation waits for all controlled clients to close. The mechanics are on Service Worker Lifecycle. A test that only loads the latest build in a fresh browser never sees this.
Multiple versions coexist. During an update, a page running version N can be talking to a worker at version N+1 (after skipWaiting()), or an old worker can serve assets from a cache the new page no longer expects. Messages, cache names, IndexedDB schemas and API contracts all have to tolerate one version of skew. Updating Service Workers covers the patterns; your tests have to exercise the transition itself.
The worker is killed and restarted. Browsers terminate idle service workers after roughly 30 seconds and restart them for the next event. Any state held in globals disappears. DevTools suppresses this termination while it is attached, so the bug is invisible during manual debugging unless you stop the worker deliberately, as described on Browser DevTools.
Browsers disagree. Installability rules, storage quotas and eviction, push delivery, background APIs and display modes differ between Chromium, Firefox and WebKit, and again between desktop and mobile. Platform Support documents the differences; the test plan must decide which ones you verify and how.
What to test¶
The table lists the areas that matter for almost every PWA, what typically goes wrong, and how you detect it. The subsections below expand on each.
| Area | Typical production failures | Primary test method |
|---|---|---|
| Installability | Manifest not linked on some routes, icon 404, wrong start_url or scope, missing id so updates orphan installs | Automated manifest checks plus DevTools Manifest pane; manual install per platform before release |
| Offline behavior | Navigations to uncached routes show the browser error page; API calls hang instead of failing fast | Integration tests with network emulation; manual airplane-mode pass |
| Update flow | Users stuck on an old version; "reload to update" prompt never appears; mixed old and new assets after update | Two-version integration tests (deploy N, then N+1) |
| Cache correctness | Stale HTML referencing deleted hashed assets; caches never cleaned; opaque responses exhausting quota; Vary preventing matches | Integration tests asserting cache contents; quota simulation |
| Push and notifications | Expired subscriptions never pruned; handler assumes JSON; no notification shown for a push; clicks open the wrong window | Unit tests for handlers; CDP-dispatched push in CI; end-to-end push to real devices on a schedule |
| Background sync and periodic sync | Queued requests lost on worker restart; retries never happen; handler assumes a page is open | CDP-dispatched events with and without lastChance; restart the worker between steps |
| Storage and data | IndexedDB migrations fail with open connections; QuotaExceededError unhandled; data lost after eviction | Migration tests from each previous schema; simulated quota |
| Cross-browser and platform | Feature assumed present (Background Sync on Safari), iOS Home Screen differences, desktop window behaviors | Matrix runs across engines plus real-device checks |
| Performance | Worker startup delays navigations; precache competes with first render | Lab measurements with and without the worker; field data by controlled state |
Installability¶
Installability depends on the manifest, the page, and browser-specific rules. Test that every route that can be an entry point links the manifest, that the manifest parses with the members you expect (a JSON parse is not enough; the browser drops invalid members silently), that icons of the declared sizes exist and are the declared sizes, and that id, start_url and scope resolve to the URLs you intend. The criteria and their differences between browsers are on Installability Criteria; how to inspect Chrome's own verdict is in the Manifest pane section of Browser DevTools. The actual install prompt cannot be clicked by automation in most setups, so keep a short manual install checklist per platform: Installation by Platform lists the flows.
Offline and network resilience¶
Test at least three network states: fully offline, online, and slow or flaky, which is where most real users are when they are not online. Offline tests should navigate to a route that was precached, a route that was visited before (runtime-cached), and a route that was never visited, and assert that each produces the intended content or the offline fallback rather than the browser's error page. Emulated offline mode fails requests instantly, so it does not exercise timeouts; add a test with artificial latency (for example a test server route that delays its response) to verify your network-timeout fallbacks from Caching Strategies.
Update flows¶
Update bugs are the most expensive PWA bugs because a broken worker can prevent users from ever receiving the fix. The reliable test is a two-build test: serve build N, load the app and let the worker activate and precache; switch the server to build N+1; trigger an update (reload, or registration.update()); then assert what the user should see at each step: the waiting worker, your update prompt, the page reloaded under the new controller, and old caches removed after activation. Run the same test with two tabs open to prove that the waiting phase behaves as intended. Keep build N+1 minimal (a changed version string is enough) so the test is fast. The DevTools Update on reload option must be off for any manual version of this test, since it skips the waiting phase entirely.
Push notifications¶
Split push testing by layer. The handler (payload parsing, notification options, notificationclick routing to the right window) is ordinary code: unit-test it and exercise it in a browser with CDP's ServiceWorker.deliverPushMessage, which skips the push service. The subscription and delivery layer (VAPID keys, payload encryption, TTL and urgency, 404 and 410 handling to prune expired subscriptions) must be tested against real push services, because the DevTools and CDP paths bypass it. iOS and iPadOS deliver web push only to Home Screen web apps, and only after permission is requested from a user gesture, so the delivery test on Apple platforms is necessarily manual or device-farm based. See Push Notifications, The Web Push Protocol and Web Push on iOS & Safari.
Cache correctness¶
Treat Cache Storage as a database with invariants and assert them: after install, every precache entry exists and matches the build manifest's revisions; after activation of a new version, no cache from a previous version remains (unless intentionally shared); no cached HTML references an asset that is missing from both the cache and the server; runtime caches respect their maximum entries; and no response with a status other than 200 (or an opaque response you did not intend) was cached. The APIs involved are on Cache Storage API and Precaching & Runtime Caching. Also test eviction: delete a cache or the whole origin's storage between steps and assert that the app recovers by fetching from the network rather than crashing.
Storage, quota and data migrations¶
For IndexedDB, test migrations from every schema version still present in the field, not just the previous one, and test them with a second tab holding a connection open so that blocked and versionchange handling is exercised. For quota, Chromium's DevTools can simulate a small quota (Application › Storage › Simulate custom storage quota), which is the practical way to reproduce QuotaExceededError. Limits and persistence are on Storage Quotas & Persistence and data design on Offline-First Data & Sync.
Cross-browser and cross-platform behavior¶
Decide which differences you rely on and test those explicitly with feature detection in mind: that the app works without Background Sync (Firefox and Safari), that it handles storage eviction on WebKit, that standalone display behaves correctly on iOS (no browser back button, different status bar handling), and that desktop app windows behave with Window Controls Overlay if you use it. Automated engines (Playwright's Chromium, Firefox and WebKit builds) cover rendering and much of the API surface, but they are not the shipping browsers and cannot install apps; real devices remain necessary for install, push and OS integration.
The PWA test pyramid¶
The usual pyramid still applies, with PWA-specific content at each layer. The key difference is that the middle layer (integration tests in a real browser against a real server) carries much more weight than in a typical web app, because the service worker only behaves correctly inside a real browser with real Cache Storage and real lifecycle events.
flowchart TD
M["Manual and real-device checks: install on each platform, iOS Home Screen push, OS integration (per release)"]
E["End-to-end journeys: first visit, offline navigation, update prompt, push click-through (few, slow)"]
I["Browser integration tests: lifecycle, precache contents, strategies under network emulation, two-build updates, CDP push and sync (many)"]
U["Unit tests: routing rules, cache naming and expiry, payload parsing, sync queue logic, manifest validation (most, fast)"]
M --> E --> I --> U Unit tests cover logic you can extract from the worker into plain modules: URL-to-strategy routing tables, cache key normalization, expiration policies, push payload parsing and notification option building, outbox queue serialization, and manifest validation (JSON schema plus custom rules such as "the id never changes"). They run in Node with a mocked caches/fetch environment or a service worker mock library, and they should be the bulk of your tests. Keep the worker file itself thin so that most logic is unit-testable.
Browser integration tests run the real worker in a real browser (Playwright or Puppeteer driving Chromium, plus Firefox and WebKit where the API exists) against a local server you control. This layer tests the lifecycle, precache contents, strategies under emulated offline and latency, two-build update flows, and push and sync handlers dispatched through the Chrome DevTools Protocol. Techniques and full examples are on Automated Testing.
End-to-end tests cover a handful of user journeys on a deployed environment: first visit through to offline use, the update prompt after a deploy, and a notification click that opens the right screen. They are slow and brittle, so keep them few.
Manual and real-device checks cover what automation cannot: the install prompt and resulting OS integration on Android, Windows, macOS, ChromeOS and iOS; web push on iPhone; badging, shortcuts and share targets. Run them before each release that touches the manifest, the worker, or native integration, using the recipes on Browser DevTools.
Audits sit beside the pyramid rather than in it: Lighthouse for performance, accessibility, SEO and best practices, plus your own installability and offline checks now that Lighthouse has no PWA category. See Lighthouse & Auditing.
Test environments¶
Secure contexts: localhost, HTTPS and tunnels¶
Service workers, push and most PWA capabilities are only exposed in secure contexts. Your options:
| Environment | Secure context? | Notes |
|---|---|---|
http://localhost, http://127.0.0.1, http://[::1] | Yes | Browsers treat loopback as potentially trustworthy. The default for local development and CI. |
http://*.localhost subdomains | Yes in Chromium (resolved to loopback) | Handy for testing several origins locally; confirm window.isSecureContext in every other browser you target before relying on it. |
https:// with a locally trusted certificate (for example created with mkcert) | Yes | Needed when you must use a real hostname, test cookies with Secure, or reach the machine from other devices. |
| An HTTPS tunnel to localhost | Yes | Lets real phones and push services reach a local build. Treat the tunnel URL as a separate origin: registrations, caches and push subscriptions do not carry over from localhost. |
http://192.168.x.x or another LAN host | No | navigator.serviceWorker is undefined. Use Chrome's port forwarding to map the phone's localhost to your machine instead. |
| Browser flags | Yes, for the listed origin only | Chromium's --unsafely-treat-insecure-origin-as-secure=http://host:port and Firefox's "Enable Service Workers over HTTP (when toolbox is open)" setting are for local debugging only; never rely on them in CI assertions about security behavior. |
Clean state versus upgraded state¶
Run every critical scenario from two starting points. A clean state is a new browser profile with no registration, caches, permissions or installed app; it tests first-visit behavior. An upgraded state is a profile where the previous production build was loaded, activated, precached, and ideally used for a while (runtime caches populated, IndexedDB at the old schema); it tests what real returning users experience. Automation makes both cheap: a fresh browser context is a clean state, and a context that first loads build N from a second server port is an upgraded state. For manual work, keep a dedicated profile per state (Chrome: --user-data-dir=/path/to/profile; Firefox: about:profiles) instead of relying on Clear site data, which does not reset permissions, installed apps or server-side push subscriptions.
Incognito and private windows are poor substitutes. Chrome limits storage quota in Incognito and refuses installation there. Firefox only enabled service workers in private browsing in Firefox 140, using encrypted temporary storage. Safari's private browsing has its own storage behavior. Use them for quick smoke checks, not as the reference environment.
Real devices, emulators and simulators¶
Desktop Chromium's device emulation changes the viewport, user agent and touch input, but it is still desktop Chrome: it does not reproduce Android's install flow, WebAPK minting, or iOS behavior. Use:
- Android: real devices through
chrome://inspectwith USB debugging and port forwarding, or the Android Emulator with a Google Play system image for Chrome and WebAPK installation. - iOS and iPadOS: the Xcode Simulator for layout and most Safari behavior (Web Inspector is always enabled for simulators), and real devices for Home Screen web app push, storage and lifecycle behavior. Every iOS browser uses WebKit, so testing Safari covers the engine for all of them, though browser chrome and install entry points differ.
- Desktop: install the app in Chrome, Edge and Safari (macOS Sonoma and later, File › Add to Dock), and test window behaviors such as title bar, protocol handling and file handling in the installed window rather than in a tab.
Cross-browser test matrix¶
A practical matrix for most PWAs, balancing coverage against cost. Adapt the rows to the capabilities you actually use; the per-browser support details are on Platform Support and the capability pages.
| Scenario | Chromium (automated) | Firefox (automated) | WebKit (automated) | Android Chrome (device) | iOS Safari / Home Screen (device) | Desktop installed app (manual) |
|---|---|---|---|---|---|---|
| Registration, precache, activation | ✅ | ✅ | ✅ | Per release | Per release | Per release |
| Offline navigation and fallbacks | ✅ | ✅ | ✅ | Per release | Per release | Per release |
| Two-build update flow | ✅ | ✅ | ✅ | Spot check | Spot check | Spot check |
| Push handler logic (CDP-dispatched) | ✅ | ❌ | ❌ | Not applicable | Not applicable | Not applicable |
| Real push delivery | Scheduled | Scheduled | ❌ | Per release | Per release | Per release |
| Background Sync / Periodic Sync | ✅ | Not supported | Not supported | Spot check | Not supported | Spot check |
| Install flow and OS integration | ❌ | ❌ | ❌ | Per release | Per release | Per release |
| IndexedDB migrations, quota handling | ✅ | ✅ | ✅ | Spot check | Spot check | Not needed |
Support data as of September 2026. For live API support, check MDN's Service Worker API compatibility data and caniuse.com.
Automation engines are not shipping browsers
Playwright's WebKit build is not Safari and cannot reproduce iOS Home Screen behavior, storage policies or web push. Playwright's service worker inspection features (listing workers, routing requests they make) are documented as Chromium-only. Treat a green WebKit run as necessary, not sufficient.
A manual verification checklist for releases¶
Run this list on real devices for any release that changes the service worker, the manifest or caching configuration. Each item links to the page that explains what "correct" means.
- Fresh profile: the page registers the worker, precaches, and becomes controlled on the next navigation (or immediately, if you use
clients.claim()). See Lifecycle. - Upgraded profile: after deploying, the update prompt appears, the new worker activates when accepted, and old caches are deleted. See Updating Service Workers.
- Offline: previously visited routes load, unvisited routes show the offline fallback, and pending writes queue and later sync. See Offline UX & Fallbacks.
- Installability: Chrome's Manifest pane shows no installability errors and the expected computed app ID. See Browser DevTools.
- Install on Android, desktop Chromium and iOS: correct name, icon (maskable safe area), splash colors,
displaymode and start URL. See Icons & Maskable Icons. - Push: a real push reaches each platform, the notification click opens or focuses the right window, and expired subscriptions are pruned server-side.
- Storage:
navigator.storage.persisted()returns the value you expect after your persistence request flow, and the app recovers when site data is cleared. - Performance: navigation timing with a warm worker is not worse than without it; check worker Startup time in the Network panel's Timing tab. See Measuring Performance.
For a complete pre-launch list beyond testing, see the Production Checklist.
Pages in this section¶
-
Browser DevTools
The Chromium Application panel in depth, push and sync simulation,
chrome://serviceworker-internals, remote Android and iOS debugging, Firefox and Safari tooling, and debugging recipes. -
Automated Testing
Unit-testing service worker logic, Playwright and Puppeteer integration tests for lifecycle, caching, offline and updates, and running them in CI.
-
Lighthouse & Auditing
Using Lighthouse after the PWA category was removed, custom installability and offline audits, and budgets in CI.
Further reading¶
On this site
- Service Worker Lifecycle
- Updating Service Workers
- Pitfalls & Anti-Patterns
- Caching Strategies
- Installability Criteria
- Platform Support
- Production Checklist
External references
- Service Workers specification (W3C)
- Debug Progressive Web Apps (Chrome for Developers)
- Revisiting Chrome's installability criteria (Chrome for Developers)
- Lighthouse changelog (GoogleChrome/lighthouse)
- Service workers in Playwright (Playwright)
- Secure contexts (MDN)
- Firefox 140 release notes (Mozilla)