How to Test a Web App in Offline Network Mode With Cypress
Use Cypress and Chrome DevTools Protocol to test a web app offline, assert its fallback, restore connectivity, and avoid leaking browser state between tests.
To test a web app in browser-wide offline mode with Cypress, use the Chrome DevTools Protocol (CDP): enable the Network domain, emulate offline conditions, assert both the browser’s offline status and your app’s visible fallback, then restore connectivity in cleanup. Use cy.intercept() with forceNetworkError instead when the behavior under test is failure of one particular request rather than the browser’s overall online state.
The Cypress offline recipe uses Cypress.automation('remote:debugger:protocol', ...) for this workflow. Because those calls return promises, wait for them inside Cypress command chains. The detailed recipe is older, so verify it against your pinned Cypress and browser versions before relying on it across a browser matrix.
Browser-wide offline test with Cypress
The following example assumes the app is available at /users, shows an element with data-testid="network-status", and has a button with data-testid="load-users". Adapt those selectors and the expected error text to your app. The test enables CDP network control, verifies the browser and app state, triggers a fetch, checks the error, restores connectivity, and checks recovery.
// cypress/e2e/offline.cy.js
describe('offline behavior', () => {
const emulateOffline = (offline) => {
return cy.then(() =>
Cypress.automation('remote:debugger:protocol', {
command: 'Network.emulateNetworkConditions',
params: {
offline,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
},
})
);
};
const disableNetworkDomain = () => {
return cy.then(() =>
Cypress.automation('remote:debugger:protocol', {
command: 'Network.disable',
})
);
};
// Reset before and after each test. afterEach runs after a failed assertion too.
beforeEach(() => {
cy.then(() =>
Cypress.automation('remote:debugger:protocol', {
command: 'Network.enable',
})
);
emulateOffline(false);
});
afterEach(() => {
// Restore first so Cypress can continue communicating with the browser.
emulateOffline(false);
disableNetworkDomain();
});
it('shows an offline error and recovers when connectivity returns', () => {
cy.visit('/users');
cy.get('[data-testid="network-status"]').should('contain', 'Online');
emulateOffline(true);
cy.window().its('navigator.onLine').should('equal', false);
cy.get('[data-testid="network-status"]').should('contain', 'Offline');
cy.get('[data-testid="load-users"]').click();
cy.contains('Could not load users while offline').should('be.visible');
emulateOffline(false);
cy.window().its('navigator.onLine').should('equal', true);
cy.get('[data-testid="load-users"]').click();
cy.get('[data-testid="users-list"]').should('be.visible');
});
});
Network.emulateNetworkConditions takes an offline boolean plus latency, downloadThroughput, and uploadThroughput. The example uses zero latency and unlimited throughput values as shown in Cypress’s recipe. To simulate a slow connection instead, set offline: false, choose a latency in milliseconds, and set download and upload throughput in bytes per second; confirm parameter behavior with the CDP and Cypress versions you use.
Why the hooks matter
If the browser remains offline, Cypress may be unable to communicate with it and the run can fail in confusing ways. Restore online conditions in both setup and teardown so an earlier failed test cannot contaminate the next one. If your runner or browser does not permit the cleanup command after a failure, keep the reset in the next test’s setup as a second safeguard and isolate this spec in the relevant browser job.
Choose between browser offline mode and a failed request
| Technique | What it simulates | Useful assertions | Important detail |
|---|---|---|---|
| CDP offline emulation | Browser-wide offline state | navigator.onLine, online/offline UI events, app fallback and recovery |
Requires a compatible browser and reliable cleanup. |
cy.intercept() with forceNetworkError: true |
Failure of a matching browser HTTP request | Visible app result and an intercepted request’s error |
Does not make the browser globally offline. |
Use browser-wide emulation when the app listens for the browser’s online/offline status or events. Use request interception when you need to exercise a specific fetch or API failure and do not need navigator.onLine to change. Cypress describes forceNetworkError as destroying the connection for the intercepted request; an assertion can inspect the request error.
Fail one request with cy.intercept
Register the route before visiting if the app sends the request during startup. In this example the app requests /api/users after the button is clicked:
describe('users API failure', () => {
it('shows a useful message when the users request fails', () => {
cy.intercept('GET', '/api/users', { forceNetworkError: true }).as('users');
cy.visit('/users');
cy.get('[data-testid="load-users"]').click();
cy.wait('@users').should('have.property', 'error');
cy.contains('Could not load users').should('be.visible');
});
});
For startup requests, keep the cy.intercept() before cy.visit() and wait on the alias after the page has had a chance to issue it. A browser-cached resource might never reach the network layer, so the intercept will not see it. If the request must be observed, use a test setup that avoids serving that request from browser cache.
Assert the user-visible behavior
Test the outcome a person using the app depends on: an offline banner, a retry option, cached content, an error message, or successful recovery. Checking only a transport-level detail can be brittle across browser networking paths. Combine the useful observable for the selected technique with the UI outcome:
- For CDP offline mode, check
navigator.onLineand the app’s offline state, then trigger a network-dependent action. - For a forced request failure, wait on the intercept alias and check its
errorproperty, then assert the app’s error or fallback. - If recovery is part of the feature, restore connectivity and verify the user can retry or reload the needed data.
Browser and version compatibility
The detailed Cypress offline recipe uses Chrome DevTools Protocol and says its example cannot run in Firefox; it names Electron, Chrome, and Edge as compatible at the time it was published. Cypress 16’s native interception documentation describes Chrome, Chromium, and Edge using native network interception, while Firefox, WebKit, and Electron use a legacy network path. That interception guidance does not guarantee that the older CDP offline recipe behaves identically in every version or browser.
Check the Cypress version and browser version pinned in your project. Run this spec only in browser jobs where the automation command is supported, and gate it by browser if necessary. Do not infer offline emulation support solely from whether cy.intercept() works in that browser.
Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Cypress.automation rejects or the protocol command is unavailable |
The browser or Cypress version does not support the CDP command used by this recipe. | Verify the pinned versions and browser; run the test only in a compatible browser job or test a single request with cy.intercept(). |
| Cypress loses the browser or the run hangs after going offline | Offline state was left enabled, including after a failed assertion. | Restore offline: false in afterEach and at the start of each test; disable the Network domain after restoring. |
navigator.onLine stays true |
Offline emulation did not apply to the active browser target, or the command has not completed. | Chain and await the automation promise through cy.then(); verify browser support and the active target. |
| The app’s offline banner does not appear | The app may not subscribe to browser online/offline events, or the expected selector/text does not match. | Check the app’s event handling and selectors. Assert a real user-facing fallback rather than assuming the browser status automatically creates UI. |
| The intercept alias never completes | The app did not send a matching request, the route was registered too late, or the resource came from browser cache. | Match the actual method and URL, register before visit or action, and ensure the request reaches the network layer. |
cy.request() succeeds while the browser is offline |
cy.request() executes from Cypress’s Node process, not as browser-originated application traffic. |
Trigger the app’s browser request and observe it with CDP or cy.intercept(); do not use cy.request() as proof of browser offline behavior. |
Performance, reliability, and cost
Offline emulation itself avoids depending on a remote API being down at just the right time, which makes it useful for repeatable client-side fallback checks. The result still depends on the browser and Cypress path under test, so pin versions and run the case in the same browser family your product supports. Keep assertions focused on behavior users see, and restore network state even when a test fails.
Intercepting one request is narrower and can make a focused error-path test easier to diagnose. It does not cover browser-wide offline indicators or online/offline event handling. A test suite pays the usual cost of browser startup and test execution; no authoritative source in the research dossier provides a Cypress-specific runtime or cost benchmark, so measure the added time in your own CI environment.
Or skip the browser setup
If the task is to capture how a page looks, rather than verify your app’s offline behavior, ScreenshotNeo is a website screenshot API and MCP server. It does not replace Cypress for offline behavior tests. One GET request returns an image or PDF, and the API accepts the destination URL as a parameter:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request options. Cookie banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Only clean shots are billed, and response headers report the page verdict and billing status.
Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.
FAQ
Does offline mode make every request fail?
It emulates browser-wide offline conditions. Use an intercept when the test should fail only a matching request.
Can I use this to test server-side connectivity?
No. This tests browser behavior. Cypress’s cy.request() runs from the Node process, so it does not demonstrate how app-originated browser traffic behaves offline.
Should I assert an exact browser error message?
Usually assert the app’s own visible fallback. Browser transport details can vary across Cypress networking paths.


