How to Test Offline Mode on Web (Complete Guide)
Modern web apps rely on service workers, caching strategies, and background sync to deliver a usable experience when the network disappears. Users expect forms to stay editable, media to keep playing,
Why Offline Mode Testing Matters for Web Applications
Modern web apps rely on service workers, caching strategies, and background sync to deliver a usable experience when the network disappears. Users expect forms to stay editable, media to keep playing, and critical actions to queue for later submission. When the offline path is broken, the result is lost data, frustrated users, and a spike in support tickets.
Testing offline behavior is not a nicety; it is a risk‑mitigation activity. A missing fallback for a fetch handler can turn a simple navigation into a blank page. A mis‑configured cache can serve stale JSON that violates business rules. A button that relies on a live API call may throw an unhandled promise rejection, causing the JavaScript runtime to crash the tab.
Because offline mode touches every layer—HTML, CSS, JavaScript, service workers, IndexedDB, and server‑side APIs—defects often hide behind unit tests that mock network success. Only when the real browser encounters a true “no‑network” state do those assumptions break. A disciplined offline test plan catches those gaps before they reach production.
Common Failure Modes When Going Offline
| Failure Category | Typical Symptom | Root Cause |
|---|---|---|
| UI freeze / blank screen | White page, spinner never stops | Service worker fetch handler throws, or navigator.onLine check blocks rendering |
| Data loss | Form inputs reset after reconnection | Optimistic UI updates not persisted to IndexedDB before offline |
| Stale data display | Cached API response shows outdated info | Cache‑first strategy without versioning or cache‑busting |
| Accessibility break | Screen reader announces “button disabled” incorrectly | ARIA states tied to network status not updated when offline |
| Security exposure | Sensitive token leaked in console error | Uncaught fetch rejection logs full request URL with auth header |
| Infinite retry loop | Browser repeatedly attempts fetch, draining battery | Missing if (!navigator.onLine) return; guard in retry logic |
| Broken deep link | URL opens to error page instead of app shell | Service worker fails to match navigation request to cached fallback |
Each of these symptoms can be reproduced deliberately, but many only appear under specific timing conditions (e.g., network drops mid‑fetch, or when the user toggles airplane mode while a POST is in flight).
Test Matrix for Offline Mode
Below is a comprehensive matrix that covers happy paths, error paths, edge cases, accessibility, and security/privacy concerns. Use it as a checklist when designing manual or automated tests.
| Test ID | Scenario | Preconditions | Steps | Expected Result | Pass/Fail Criteria |
|---|---|---|---|---|---|
| O-01 | App loads with no network | Device offline, service worker registered | Open URL | App shell loads, cached UI visible | UI renders within 2 s, no console errors |
| O-02 | Navigation to uncached route | Device offline, SW with fallback to offline.html | Click link to /profile | offline.html displayed | Correct fallback shown, no 404 |
| O-03 | Form submission while offline | Device offline, form with client‑side validation | Fill form, click Submit | Data stored in IndexedDB, UI shows “queued” | Entry appears in DB, UI reflects pending state |
| O-04 | Form submission after reconnection | Device goes online after O-03 | Wait for background sync or manual retry | Queued request sent, server responds 200, UI updates to “sent” | Request appears in network tab, DB entry cleared |
| O-05 | Media playback continuation | Device offline, audio/video cached via SW | Start playback, go offline | Playback continues without interruption | No stalls, media events fire as expected |
| O-06 | Cache update race | Device online, new version deployed, then go offline quickly | Reload, then disable network before SW finishes update | Old cached version served, no mix of old/new assets | Consistent version, no 404 on assets |
| O-07 | Accessibility of offline indicators | Device offline, ARIA live region for status | Observe screen reader output | Live region announces “offline, queued changes” | Text matches spec, no silence |
| O-08 | Error handling UI | Device offline, fetch fails with 500 simulated | Trigger action that calls API | Inline error message appears, not console only | Message visible, focus trapped if modal |
| O-09 | Security leakage check | Device offline, fetch with auth header fails | Open devtools console | No auth token logged in error output | Console clean of sensitive data |
| O-10 | Infinite retry prevention | Device offline, retry logic with exponential backoff | Initiate failing request | Retry attempts stop after max attempts | No more than N retries observed |
| O-11 | Deep link resilience | Device offline, user receives push notification with deep link | Tap notification | App shell loads, cached route shown | No blank page, fallback shown if route uncached |
| O-12 | Concurrent offline/online toggles | Device toggles airplane mode rapidly | Switch offline/online 5 × within 10 s | App remains stable, no crashes | No unhandled promise rejections, UI responsive |
| O-13 | IndexedDB quota exceeded | Device offline, large blob queued repeatedly | Fill form with 5 MB attachment 20 times | Older entries evicted per DB policy, no crash | App stays usable, logs quota warnings |
| O-14 | Service worker unregistration | Device offline, user clears site data | Remove SW via devtools, reload | App falls back to network‑only mode, shows offline message | No JavaScript errors, UI indicates offline |
| O-15 | Cross‑origin resource fetch | Device offline, attempt to load third‑party script | Insert | Request fails gracefully, fallback UI loads | No console error that breaks main thread |
Each row isolates a single variable, making it easy to automate or manually verify.
Manual Testing Approach Step‑by‑Step
- Prepare the environment
- Enable Chrome DevTools → Application → Service Workers → “Offline” checkbox, or use the Network tab → Throttling → “Offline”.
- Verify that
navigator.onLinereturnsfalse.
- Baseline load
- Reload the page. Confirm the app shell appears and that no request to the server shows up in the Network panel (all served from (ServiceWorker) or (disk cache)).
- Navigate through cached routes
- Use the UI to reach every page that should be available offline. Note any blank pages, spinner persistence, or console errors.
- Interact with offline‑capable features
- Fill forms, trigger media playback, open dropdowns that rely on fetched data.
- Observe whether the UI updates optimistically and whether data is persisted to IndexedDB (check Application → IndexedDB).
- Simulate reconnection
- Disable the offline flag, wait for background sync or manually trigger a retry (e.g., pull‑to‑refresh).
- Confirm that queued actions are sent, server responses are handled, and the UI reflects the final state.
- Accessibility audit
- Turn on a screen reader (NVDA, VoiceOver, or ChromeVox).
- Walk through the offline flow and listen for live region announcements, focus management, and proper ARIA states.
- Security check
- Open the Console, filter for “error” warnings. Ensure no authentication tokens, API keys, or personal data appear in stack traces or log messages.
- Repeat with variations
- Toggle airplane mode rapidly, close and reopen the tab, clear caches, and test each scenario from the matrix.
Manual testing is valuable for exploratory work, especially when you want to see how the UI feels under flaky conditions. However, it is hard to repeat at scale, which is why we complement it with automation.
Automated Testing Strategies
Using Service Worker Mocks
Unit tests can instantiate a service worker in a testing environment (e.g., using workbox-window or a custom mock) and feed it fabricated fetch events.
// test/sw.fetch.test.js
import { register } from 'workbox-window';
import { createFetchEvent } from './testHelpers';
describe('SW fetch handler – offline', () => {
let sw;
beforeAll(() => {
sw = register('/sw.js', { scope: '/' });
});
test('returns offline fallback for unknown route', async () => {
const event = createFetchEvent({
request: new Request('/profile', { method: 'GET' }),
});
await sw.active.postMessage({ type: 'MOCK_FETCH', event });
const response = await event.waitUntil(
caches.match(event.request)
);
expect(response).toBeDefined();
expect(await response.text()).toContain('Offline');
});
});
The mock lets you assert that the SW returns a cached offline.html without needing to toggle the network.
Network Throttling Tools
Chrome DevTools provides a programmatic interface via the Chrome DevTools Protocol (CDP). Tools like puppeteer or playwright can set the network condition to “offline” before each test.
// playwright.config.js
module.exports = {
use: {
headless: false,
// launch with offline flag
launchOptions: {
args: ['--disable-background-networking'],
},
},
};
// test file
import { test, expect } from '@playwright/test';
test('app loads offline', async ({ page }) => {
await page.context().setOffline(true);
await page.goto('/');
await expect(page.locator('h1')).toHaveText(/Welcome/);
});
Puppeteer offers the same API: await page.setOffline(true);.
End‑to‑End Frameworks with Offline Plugins
Cypress does not natively support offline mode, but you can combine it with cypress-network-idle or manually alter navigator.onLine.
// cypress/integration/offline.spec.js
describe('Offline behavior', () => {
beforeEach(() => {
// Overwrite the property
cy.window().then((win) => {
Object.defineProperty(win.navigator, 'onLine', {
get: () => false,
});
});
});
it('shows offline banner', () => {
cy.visit('/');
cy.get('[data-test="offline-banner"]').should('be.visible');
});
it('stores form data in IndexedDB', () => {
cy.get('[data-test="name-input"]').type('Ada');
cy.get('[data-test="submit"]').click();
cy.window().then((win) => {
return win.indexedDB.open('app-db', 1).then((db) => {
const tx = db.transaction('outbox', 'readonly');
const store = tx.objectStore('outbox');
return store.getAll();
}).then((rows) => {
expect(rows).to.have.lengthOf(1);
expect(rows[0].payload.name).to.eq('Ada');
});
});
});
});
Playwright offers a more straightforward approach with setOffline.
// playwright/offline.test.js
import { test, expect } from '@playwright/test';
test.describe('Offline mode', () => {
test.beforeEach(async ({ page }) => {
await page.context().setOffline(true);
});
test('retries queued requests on reconnect', async ({ page }) => {
await page.goto('/');
await page.fill('[data-test="email"]', 'test@example.com');
await page.click('[data-test="login"]');
// Wait for IndexedDB entry
await page.waitForFunction(() =>
window.indexedDB.open('app-db', 1).then((db) => {
const tx = db.transaction('outbox', 'readonly');
return tx.objectStore('outbox').getAll();
}).then((rows) => rows.length > 0)
);
// Go online
await page.context().setOffline(false);
// Expect network request
await page.waitForResponse((resp) =>
resp.url().endsWith('/login') && resp.status() === 200
);
// UI should show success
await expect(page.locator('[data-test="login-success"]')).toBeVisible();
});
});
Unit/Integration Tests for Fetch Handlers
If your app isolates network calls in a service layer, you can mock fetch with libraries like msw (Mock Service Worker) or fetch-mock.
// src/services/api.js
export async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Network error');
return res.json();
}
// src/services/api.test.js
import { getUser } from './api';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ id: req.params.id, name: 'Test' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('returns data when network ok', async () => {
const user = await getUser(42);
expect(user.name).toBe('Test');
});
test('throws when offline', async () => {
server.use(
rest.get('*', (req, res, ctx) => {
// Simulate network failure by not responding
return res.networkError('Failed to fetch');
})
);
await expect(getUser(1)).rejects.toThrow(/Network/);
});
These tests guarantee that the error path is handled correctly before the SW even sees the request.
Edge Cases that Surface Only in Production
Even with thorough lab testing, certain conditions only appear when real users interact with the app under unpredictable circumstances.
- Partial network: The device shows “connected” but the captive portal blocks external requests.
navigator.onLinestays true, yet all fetches fail. Your app must detect this via response status or timeout and fall back to offline behavior. - Background tab throttling: Browsers pause timers in background tabs, delaying IndexedDB writes or sync triggers. A user may switch tabs while a form is submitted offline; the data may not be flushed until they return, causing a perceived delay. Use the Page Visibility API to prioritize critical writes.
- Battery saver modes: On Android/iOS, battery‑saving can stop service workers from executing background sync. Test with the Battery API or device‑specific settings to ensure your sync logic gracefully degrades.
- Storage pressure: Low‑end devices may evict IndexedDB entries to free space. Simulate this by filling storage with large blobs via
navigator.storage.persist()checks and verifying that your app handlesQuotaExceededError. - Service worker updates during a race: If a user goes offline while the SW is installing a new version, the old SW may be killed before the new one takes over, leaving the app without any cached fallback. Listen for
statechangeonregistration.installingandwaitingand show an update prompt only when the network is reliable. - Cross‑origin iframe offline: Third‑party widgets (e.g., payment iframes) may not have their own SW, causing them to fail silently when the parent is offline. Consider disabling or replacing such widgets with a static fallback when
navigator.onLineis false. - User‑initiated data clear: Users can clear site data from settings, wiping both cache and IndexedDB. Your app should detect an empty DB on start and show a clear “start fresh” UI rather than trying to read non‑existent records.
These scenarios rarely appear in scripted test suites but can be exercised with tools that manipulate the browser’s internal state (e.g., Chrome’s --disable-background-networking, --disk-cache-size, or custom DevTools overrides).
Accessibility and Security Considerations in Offline Mode
Accessibility
- Live regions: When the app transitions to offline, update an
aria-live="polite"region with a concise message (“You are offline. Changes will be saved when you reconnect”). Avoid verbose announcements that could overwhelm screen‑reader users. - Focus management: If a modal error appears due to a failed fetch, trap focus inside the modal and restore it when dismissed.
- Contrast: Offline banners often use low‑contrast colors to stay subtle; verify they meet WCAG AA (4.5:1) for text against background.
- Touch target size: Buttons that queue actions (e.g., “Save for later”) must be at least 44 × 44 dp.
- Reduced motion: If you animate the offline indicator, respect
prefers-reduced-motion.
Security
- Do not cache sensitive responses: Ensure that
Cache-Control: no-storeis set on any endpoint returning passwords, tokens, or PII. - Opaque responses: Cross‑origin requests that fail offline produce opaque responses; avoid reading their body.
- Error messages: Never include stack traces or raw HTTP headers in UI messages shown offline. Log them internally only.
- Content Security Policy (CSP): Your SW should be served with
script-src 'self'andobject-src 'none'to prevent injection of malicious code via imported scripts. - Subresource Integrity (SRI): If you cache third‑party libraries, verify their integrity hashes; otherwise a compromised CDN could serve malicious code while offline.
Checklist for Offline Mode Readiness
| ✅ Item | Description |
|---|---|
| Service worker registered and controlling the page | Verify via navigator.serviceWorker.controller |
offline.html or app shell cached under precache | Ensure navigation to unknown routes shows fallback |
| All critical UI components functional without network | Forms, media, navigation, alerts |
| Optimistic UI writes persisted to IndexedDB before offline | Check DB after each user action |
| Background sync or retry mechanism re‑sends queued requests | Confirm network calls after setOffline(false) |
| No console errors containing auth tokens or PII | Review Console while offline |
| Accessible offline status announced via ARIA live region | Test with screen reader |
| Touch targets meet 44 × 44 dp | Manual inspection or automated axe test |
| CSP and SRI headers present on SW and cached resources | Use curl -I or devtools Network pane |
Storage quota handled gracefully (QuotaExceededError) | Simulate full storage and verify UI stays usable |
| Battery saver / background sync restrictions respected | Test with device settings or emulator flags |
Network‑type detection (e.g., navigator.connection) used to adjust aggressiveness of prefetch | Optional but recommended for low‑bandwidth |
Mark each item as done before signing off a release.
How Autonomous Persona‑Driven Exploration (SUSA) Finds Hidden Offline Bugs
Traditional test suites follow predetermined paths; they rarely simulate the erratic behavior of real users who toggle airplane mode mid‑gesture, rapidly switch between apps, or use assistive technology in unconventional ways. SUSA’s autonomous agent changes that equation by combining three core techniques:
- Goal‑free exploration – The agent starts from the entry point and follows any clickable element, scrollable region, or deep link it discovers, without a pre‑written script.
- Persona profiles – Each run can be configured with a distinct behavior set (e.g., “impatient” clicks repeatedly before waiting for responses, “elderly” uses larger tap targets and slower gestures, “adversarial” attempts to trigger error states by rapid network toggling).
- Cross‑session memory – The agent records which screens it has visited, which actions led to dead ends, and which network conditions caused failures. On subsequent runs it prioritizes unexplored states and retries flaky scenarios with varied timing.
When testing offline mode, SUSA automatically:
- Enables offline via the Chrome DevTools Protocol at random intervals during navigation, mimicking a user who suddenly loses signal.
- Observes the resulting state – if a fetch throws, the agent checks whether the UI shows an error, whether data is queued, and whether any uncaught promise rejection appears.
- Tests persona‑specific timing – the “impatient” profile may issue a second tap before the first request completes, exposing race conditions where the SW incorrectly assumes the first request succeeded.
- Validates accessibility – after each state change, the agent runs an axe‑core snapshot and asserts that no new WCAG violations appear, paying special attention to live region announcements.
- Checks security leaks – it scans the console and network logs for any occurrence of authentication strings in error messages.
Because the agent does not rely on hardcoded assertions like “the button must be disabled,” it can discover bugs that a scripted test would never anticipate—for example, a scenario where a user opens a modal, goes offline, then closes the modal via the ESC key, leaving a stray focus trap that only appears when the modal’s exit animation is interrupted by a network loss.
In practice, teams using SUSA have reported a 30‑40 % increase in caught offline‑mode defects compared to their existing Cypress/Playwright suites, particularly in the areas of:
- Conditional caching where the SW decides to cache based on response headers that change when the request is intercepted by a captive portal.
- IndexedDB transaction ordering where rapid toggling of online/offline leads to a locked transaction that blocks subsequent writes.
- Third‑party widget fallback where a payment iframe fails silently and the parent form does not show an offline warning.
Integrating SUSA into your CI pipeline is straightforward:
# Install the agent
pip install susatest-agent
# Run a persona‑driven exploration with offline focus
susatest run \
--url https://app.example.com \
--personas impatient,elderly,adversarial \
--offline-interval 10s \
--max-depth 6 \
--output ./susareport.json
The resulting report includes a list of discovered flows, each tagged with PASS/FAIL, screenshots, and console excerpts, giving developers actionable evidence without writing a single test line.
Closing Takeaways
Offline mode is not a toggle‑switch feature; it is a set of contracts between your service worker, caching strategy, persistence layer, and UI. Breaking any of those contracts produces data loss, confusing UX, or security exposure.
A disciplined approach combines:
- A clear test matrix that covers happy, error, edge, accessibility, and security cases.
- Manual exploratory steps to validate the feel of the app under realistic network loss.
- Automated checks using service‑worker mocks, network throttling via Playwright/Puppeteer, and unit tests for fetch handlers.
- Production‑aware edge case simulations such as partial connectivity, background tab throttling, and storage pressure.
- Accessibility and security validation specific to the offline state (live regions, error sanitization, CSP, SRI).
- Autonomous persona‑driven exploration (exemplified by SUSA) to surface the unpredictable interactions that scripted tests miss.
By treating offline mode as a first‑class citizen in your test strategy—just like login flows or checkout—you ship web applications that remain useful, trustworthy, and resilient when the network disappears.
---
*Keep this guide bookmarked. Return to it whenever you add a new caching rule, adjust a service worker, or introduce a feature that writes to IndexedDB. The effort you invest now prevents the costly, frustrating bugs that only surface when users lose connectivity.*
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free