June 17, 2026
How to Test Progressive Web App Features: Offline Mode, Caching, and Install Prompts
Learn how to test PWA features with practical checks for offline mode testing, service worker cache behavior, app shell loading, and install prompt flows.
Progressive Web Apps are easy to demo and surprisingly easy to mis-test. A PWA can look correct when the network is fast, the browser is fresh, and the service worker has not yet taken control. The real failures usually show up later, when the app is reopened from a cached shell, the device goes offline mid-flow, or an install prompt appears at an awkward time and the app state becomes inconsistent.
That is why teams that want to test PWA features need more than a happy-path browser check. You need to verify how the app behaves across state transitions, not just within a single load. That means testing offline mode, service worker cache updates, installability, and the app shell, plus the edge cases that make PWAs feel unreliable when they are not carefully designed.
This tutorial focuses on practical failure modes. It is written for frontend engineers, QA engineers, and SDETs who want repeatable checks that reflect how real users experience a PWA across reloads, browser restarts, and network changes.
What makes PWA testing different
A typical web app test assumes a mostly linear flow, page loads, interaction happens, assertions are made, and the test ends. PWA testing has extra state that can survive beyond a single page session:
- a service worker can intercept requests
- the app shell can render without the network
- cached assets can be newer or older than the server
- install prompts can appear based on browser heuristics
- offline and online states can change during the same user journey
If your tests only verify the first load, you are mostly testing the network and not the PWA.
PWA bugs often come from assumptions that hold during development but fail in production, such as:
- the service worker caches the wrong version of a manifest or script
- offline fallback pages work for navigation, but not for asset requests
- the UI shows “offline” but still tries to submit a form
- install prompts are suppressed or duplicated after re-renders
- cached API data outlives the session and creates stale views
A good strategy is to split PWA checks into three layers:
- Installability and browser signals, such as manifest, icons, HTTPS, and service worker registration.
- Runtime behavior, such as offline mode testing and cached navigation.
- Update behavior, such as service worker cache refreshes and version handoff.
The minimum PWA behaviors worth testing
Not every app uses every PWA feature, so do not test blindly. But if the app claims to be installable or offline-capable, these are the checks that matter most.
1. Service worker registration
The app should register a service worker when supported by the browser and the app should not break when registration fails.
Check:
- registration happens after the app loads
- registration does not block the main UI
- the app remains usable if service worker code throws
2. App shell rendering
The app shell is the core layout, navigation, and chrome that can load quickly and stay stable even if data is missing.
Check:
- shell elements render before data-dependent content
- the shell is available offline if that is part of the design
- routes that should work offline actually do so
3. Offline mode behavior
Offline mode testing should verify not just that a banner appears, but that the app behaves correctly when requests fail.
Check:
- the UI detects offline state
- cached pages still open
- forms either queue or fail clearly
- errors are understandable and non-destructive
4. Cache correctness
Service worker cache is one of the most common sources of hidden bugs.
Check:
- static assets are cached as expected
- the app does not use stale JavaScript after deployment
- API responses are handled according to the app’s caching policy
- cache invalidation works after an update
5. Install prompt flow
Install prompt behavior differs across browsers and platforms.
Check:
- the install prompt appears only when the app is eligible
- custom install UI does not appear too early
- accepting or dismissing the prompt does not break the app
- installed mode behaves the same as standalone mode expects
Build a test plan around user states, not just browser features
A PWA is not one state, it is several. Your test plan should cover the transitions between them.
Use these states as a starting model:
- Fresh online visitor: no service worker, no cache, no prompt history
- Returning online visitor: cached assets and registered service worker
- Offline returning visitor: app should recover from cache
- Installed app user: standalone context, different chrome, different behaviors
- Updated app user: old service worker and new deployment coexist temporarily
That gives you a more realistic matrix than “desktop Chrome passed” or “mobile Safari failed.”
A practical test matrix might look like this:
| Scenario | Network | Storage | Expected outcome |
|---|---|---|---|
| First visit | Online | Empty | App loads, service worker registers |
| Second visit | Online | Cached | App shell loads faster, no broken assets |
| Navigate offline | Offline | Cached | Offline fallback or cached route works |
| Refresh offline | Offline | Cached | App shows usable fallback or clear error |
| Update deployed | Online | Old cache | New version activates safely |
| Install prompt eligible | Online | N/A | Prompt appears once and can be dismissed |
How to test offline mode in a controlled way
Offline testing is easy to fake and hard to get right. Simply disconnecting your laptop Wi-Fi is useful for exploration, but not enough for repeatable automation. You want to simulate network loss while observing what the app does with in-memory state, cached responses, and browser events.
Manual checks in DevTools
For exploratory testing, Chrome DevTools is still the fastest way to reproduce offline issues:
- Open the app while online.
- Confirm the service worker is registered.
- Open DevTools, go to the Network tab.
- Enable offline mode.
- Reload the page and navigate through cached routes.
- Watch which requests fail and whether the UI recovers.
The point is not just to see a failed request. The point is to answer these questions:
- Does the app show a graceful offline state?
- Are cached assets enough to render the shell?
- Are data-fetching errors distinct from navigation failures?
- Can the user continue, retry, or save work?
Automating offline transitions with Playwright
For repeatable checks, Playwright is a solid choice because it can control browser context, storage state, and network conditions. A short example:
import { test, expect } from '@playwright/test';
test('renders cached shell while offline', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(‘https://example.com’); await page.waitForLoadState(‘networkidle’);
await context.setOffline(true); await page.reload();
await expect(page.getByRole(‘heading’, { name: /dashboard/i })).toBeVisible(); await expect(page.getByText(/offline/i)).toBeVisible(); });
That test still needs careful thought. If the page depends on a previously populated cache, make sure the setup step actually warms the cache. Otherwise you are testing a first-visit offline failure, which may be valid but is not the same scenario as returning-user offline mode testing.
Cache-warming is part of the test, not a setup detail to ignore.
What to verify when offline
When the network drops, verify more than the headline banner:
- page shell stays mounted
- navigation does not dead-end on blank screens
- stale data is labeled as stale if that matters
- retry buttons actually retry
- mutations either queue safely or fail with explicit messaging
- no fatal JavaScript errors appear because a request promise was unhandled
Service worker cache: what to test, and what usually goes wrong
Service worker cache is powerful because it can make the app load quickly and survive outages. It is also the place where versioning bugs hide.
The most common failures are not in the initial cache put, they happen when the app updates.
Common failure modes
1. Old JavaScript, new HTML
If the HTML points to new assets but the old service worker serves cached scripts, the app can break in ways that are hard to reproduce locally.
Symptoms include:
- blank screen after deployment
- missing module errors
- mismatched runtime and chunk files
2. Cache too aggressive
A static asset stays cached after a deploy and users keep seeing old UI behavior.
Symptoms include:
- a fix appears to deploy but users still hit the old flow
- feature flags or copy changes do not show up
- a stale route keeps rendering the wrong component
3. Cache too weak
The app benefits little from offline support because the service worker caches too few assets or too little data.
Symptoms include:
- app shell cannot render offline
- navigation keeps failing for pages that should be cached
- repeated downloads waste bandwidth
4. Bad update lifecycle
A new service worker installs but never activates, or activates at the wrong time.
Symptoms include:
- old and new tabs behave differently
- a refresh is required to see the update, but users are not told
- the app loses state during activation
Test update behavior deliberately
Your checks should include a known version change. For example:
- Load version A of the app.
- Cache it.
- Deploy version B.
- Reopen or refresh the app.
- Confirm the update path works.
If your app uses a “new version available” toast, test both acceptance and dismissal. If it uses immediate activation, test that the old page does not keep serving stale assets after the update.
A useful assertion is not just “new version loaded,” but “user sees a consistent version of the shell, script bundle, and route data.”
Practical signals to inspect
When debugging service worker cache behavior, inspect:
- Application tab, service worker status
- Cache Storage entries
- network request headers and response sources
- console errors during reload
- manifest and icon fetches
The browser network panel can tell you whether a response came from memory cache, disk cache, service worker, or network. That distinction is often the clue that explains a flaky test.
How to test the app shell without overfitting to one browser
The app shell is the structural frame of a PWA, the top-level navigation, header, footer, menus, and primary layout. If the shell is unstable, offline behavior is nearly irrelevant because the user cannot orient themselves.
The challenge is that shell tests can become too shallow. If you only assert that a logo and nav appear, you may miss route-specific regressions. If you make the test too detailed, it becomes brittle.
A good shell test should answer:
- does the base layout render consistently
- is the shell present on first load and after reload
- does the shell remain visible when data fetches fail
- does responsive layout still hold when the app is installed or opened in standalone mode
For browser coverage, treat shell checks as a good fit for cross-browser testing. A small set of repeatable browser checks across Chromium, Firefox, and WebKit can catch differences in service worker handling, storage persistence, and install prompt availability. If you are building out a tutorial path for this, pair it with your browser testing tutorials and compare results with progressive enhancement experiments.
Install prompt testing, without relying on luck
The install prompt is one of the easiest PWA features to misunderstand because browser eligibility rules vary. A prompt may be supported, deferred, suppressed, or handled through your own custom install button.
What to test
At minimum, validate:
- the app becomes installable only when requirements are met
- the install call is not triggered too early
- custom prompt UI appears only after eligibility is known
- dismissing the prompt does not break later retry flows
- accepting the prompt leads to the expected standalone or installed mode
Pitfalls that show up often
Prompt shown before eligibility
If your UI presents an install button before the browser is ready, users click a dead control.
Prompt shown twice
This usually happens when the app tracks prompt state in component memory instead of durable state, or when the event listener gets attached multiple times.
Install flow breaks app state
Some apps lose context when the browser switches into installed mode. That can affect auth, routing, or feature flags.
A simple manual validation pattern
- Open the app in a supported browser.
- Confirm service worker registration and manifest are present.
- Wait for install eligibility.
- Trigger the prompt.
- Accept it, then launch the installed app.
- Verify the UI matches the expected standalone experience.
- Repeat and dismiss the prompt to ensure the app remains stable.
If your app uses a custom install modal, test that modal like any other critical UI. Make sure it is keyboard accessible, dismissible, and not blocking key interactions unexpectedly.
A focused testing checklist for real failure modes
Use this checklist when you want practical coverage rather than a theoretical PWA spec audit.
Offline mode testing checklist
- warm the cache, then go offline
- reload the page while offline
- navigate to a cached route
- open an uncached route and confirm the fallback behavior
- attempt a form submit or data mutation offline
- verify the app does not crash when fetch fails
Service worker cache checklist
- confirm the service worker registers
- inspect cached assets after first load
- verify the shell loads from cache on a repeat visit
- deploy a new version and test update behavior
- confirm stale assets are not served after activation
- clear site data and confirm first-load behavior still works
Install prompt checklist
- check install eligibility in the target browser
- confirm the prompt is not shown too early
- test accept, dismiss, and repeat attempts
- verify the installed app opens in the expected mode
- check whether user state persists across install and relaunch
App shell checklist
- shell renders before data loads
- navigation remains visible during slow or failed API calls
- branding and primary actions are present
- layout holds at different viewport sizes
- shell still works after a cache update
Automation strategy, what to automate and what to keep manual
Not every PWA behavior should be fully scripted, and not every issue can be validated through one end-to-end path. A balanced approach works better.
Good candidates for automation
- service worker registration presence
- cached shell rendering after an offline transition
- basic navigation while offline
- installability checks where browser support is stable
- update path smoke tests after deployment
Better kept as exploratory or semi-manual checks
- browser-specific install prompt quirks
- tricky update timing races
- visual or animation-heavy transition states
- edge cases involving multiple tabs or restored sessions
A useful pattern is to automate the deterministic parts and keep the ambiguous parts in a small exploratory suite. That still gives you repeatability without pretending every browser and platform behavior is identical.
For teams that want repeatable browser checks across real user states, a platform like Endtest, an agentic AI [Test automation](https://en.wikipedia.org/wiki/Test_automation) platform, can fit naturally into that workflow, especially when you want to validate the same PWA states across browsers without building everything from scratch.
Example: a compact CI smoke test for a PWA
A CI smoke test does not need to prove everything. It should prove the app boots, registers, and survives one basic offline transition.
name: pwa-smoke
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npm run test:pwa-smoke
The implementation behind test:pwa-smoke can be Playwright, Cypress, or another browser runner. The important part is that the job runs against a built artifact, not a dev server that behaves differently from production.
What to watch during debugging
When a PWA test fails, classify the failure before you fix it.
Ask:
- Is this a network problem, a cache problem, or a rendering problem?
- Did the service worker respond incorrectly, or did the app mis-handle the response?
- Is the failure only after update, only offline, or only installed?
- Does it reproduce in a fresh profile?
- Is the bug specific to one browser engine?
That classification helps you avoid false fixes, especially when a stale cache is masking a deeper routing or asset-loading problem.
Closing thoughts
To test PWA features well, you have to think in transitions, not just screens. Offline mode testing is about more than losing network access. Service worker cache testing is about version integrity, not just storage. Install prompt testing is about eligibility and state persistence, not just whether a button appears.
If you design your tests around user states, first visit, returning visit, offline visit, installed visit, and updated visit, you will catch the failures that matter most. Start with a small, repeatable set of browser checks, then expand coverage where your app’s PWA implementation is most fragile.
For teams building this as a learning project, the best next step is to add one real PWA flow to your test suite, then run it across at least two browser engines and one offline transition. That is usually enough to surface the first meaningful bug.
A PWA is only as good as its worst state transition, not its best demo.