context.setOffline(true) flips navigator.onLine on the page. Here is everything it leaves online.
The official Playwright API does flip window.navigator.onLine on every supported browser. It also leaves your service worker, your open WebSockets, your keep-alive sockets, and a couple of edge cases quietly online. This page is the per-browser truth table, the literal Playwright line that the official @playwright/mcp server emits when something asks it to go offline, and the one addInitScript patch that actually closes the gaps.
“response.addCode(`await page.context().setOffline(${offline});`);”
playwright-core/lib/tools/backend/network.js:123
That is the entire Playwright API surface for going offline. The official @playwright/mcp server has a tool called browser_network_state_set that wraps it. Assrt drives that tool, so when an Assrt scenario says "go offline", the line above lands in your trace. You can copy it into a *.spec.ts and own the test forever.
What setOffline does and does not flip
Six things you might assume go offline together. They do not. Read the right column as "what your test has to handle so the assumption does not bite you".
| Feature | Playwright behavior | What actually happens |
|---|---|---|
| navigator.onLine on the page | Flipped via Browser.setOnlineOverride (Juggler) on Firefox; Network.emulateNetworkConditions (CDP) on Chromium | true → false. Reads correctly from window.navigator.onLine. |
| offline event on window | Fires on state transition only. No event if you create the context with offline: true. | Use newContext({ offline: false }) then setOffline(true) after load to get the event. |
| fetch() in page context | New requests reject with net::ERR_INTERNET_DISCONNECTED | In-flight requests started pre-flip can still resolve. Always await the rejection. |
| Service worker fetch handler | Not flipped (microsoft/playwright#2311) | SW continues to run normally. Patch via addInitScript before SW registration. |
| Open WebSocket connections | Not closed (microsoft/playwright#4910) | ws.send() and ws.onmessage keep working. Force close via page.evaluate or proxy ctor. |
| Existing HTTP keep-alive sockets | Not torn down | A request reusing an open socket may succeed for a few seconds after the flip. |
The exact code @playwright/mcp emits
From node_modules/playwright-core/lib/tools/backend/network.js. The browser_network_state_set MCP tool calls browserContext.setOffline() and emits the resulting Playwright line as a code snippet, so any agent driving the MCP server gets a copy-pasteable test artifact for free.
From scenario to real Playwright code
Three layers, no DSL between you and the line. A natural-language case enters the agent. The agent calls the official MCP tool. The tool calls Playwright. You walk away with the line and the run trace.
What flows through Assrt when a scenario says 'go offline'
The four places setOffline silently fails
All four are tracked in microsoft/playwright. None are bugs in your test code. They are holes in what the CDP Network.emulateNetworkConditions offline flag actually covers.
Service workers do not see the flip
Open since 2020 (microsoft/playwright#2311). The CDP offline flag is set per-target. The page target is offline, the SW target is not, so the SW keeps serving cached pages and even passing through new fetches. Your offline banner test passes against the SW response, not the offline state.
Open WebSockets stay alive
microsoft/playwright#4910. setOffline does not close existing WebSocket connections; ws.send() and ws.onmessage continue to fire. Reconnect logic that listens on the close event never triggers. You have to force the close yourself from a page.evaluate.
Keep-alive HTTP sockets are not torn down
If a request just completed and the underlying socket is sitting in the connection pool, a subsequent request can reuse it after setOffline(true). Most apps do not notice because the next request usually exceeds the keep-alive window, but a flaky test under load will hit this.
No event when you start offline
newContext({ offline: true }) creates the context already offline, but no offline event fires when the page loads, because there was no online-to-offline transition. Tests that key on the event handler running need newContext({ offline: false }) followed by page.context().setOffline(true) after navigation.
The addInitScript patch
page.addInitScript runs your code in every new document and worker before any page script executes. Use it to redefine navigator.onLine as a property you control, then drive that flag from your test. Now your service worker, your dedicated workers, and your page all read the same value, and the offline event fires deterministically.
Before vs after the patch
// What you write
await context.setOffline(true);
// Page reads navigator.onLine → false
// offline event fires on window
//
// But:
// - service worker still serves cached + live fetches
// - open WebSocket keeps streaming
// - keep-alive sockets stay openThe patch is yours to keep. Because Assrt outputs the Playwright line directly, you can paste both blocks into the same test() and own the offline coverage end to end. A YAML-based test runner cannot give you this because there is no place to inject addInitScript in a YAML file.
By the numbers
Watch a single Assrt run drive setOffline
The agent never wrote page.context().setOffline(true) itself. It called browser_network_state_set and the official Playwright MCP tool emitted that line into the response. Assrt persisted it. You can grep /tmp/assrt/results/latest.json for the snippet and check it into your repo as a real Playwright test.
Test your offline UI without writing any of this
npx @m13v/assrt test https://your-app.com -- the agent figures out where the offline banner lives, calls browser_network_state_set, and drops the real Playwright line in the trace. Open-source, self-hosted, no $7.5K/mo cloud.
Run it →Related Playwright APIs you usually pair this with
Questions developers actually ask about setOffline
Does context.setOffline(true) actually set navigator.onLine to false?
Yes, on the page. Playwright's Chromium driver sends Network.emulateNetworkConditions { offline: true } via CDP (playwright-core/lib/server/chromium/crNetworkManager.js:131), and Firefox sends Browser.setOnlineOverride via Juggler (ffBrowser.js:293). Both flip window.navigator.onLine and dispatch the offline event on window. The catch is that this only covers the page context. Service workers, dedicated workers running in their own thread, and pre-existing TCP connections do not all see the same flip.
Why does my service worker still respond to fetch when the context is offline?
Because Network.emulateNetworkConditions in CDP applies per-target, and Playwright historically only set the offline flag on the page session, not the service worker session. This is the long-standing microsoft/playwright#2311. If your offline UI depends on the SW returning a cached page, run-time setOffline will not exercise that path. Either start the context with offline: true at newContext time so the SW boots offline, or stub the SW's fetch handler with addInitScript before the SW registers.
What about WebSocket connections that were already open?
setOffline does not close them. microsoft/playwright#4910 has been open since 2020 documenting that an existing WebSocket continues to send and receive messages after setOffline(true). If your reconnect logic listens for the close event, it never fires. The workaround is to call ws.close() yourself from a page.evaluate, or to inject a beforeunload-style hook via addInitScript that proxies WebSocket and forces close on offline.
Does the offline event fire when I call setOffline mid-session?
Yes, but only on a state transition. If you call setOffline(true) twice in a row, the second call is a no-op and no event fires. If you create a context with offline: true and then load the page, no offline event fires either, because there was no online-to-offline transition. For tests that hook window.addEventListener('offline', ...), pass offline: false to newContext, then page.context().setOffline(true) after the page loads.
Will fetch() inside the page actually fail after setOffline(true)?
Yes for new fetches. Playwright sets the CDP Network.emulateNetworkConditions offline flag, which makes the network stack reject new requests with net::ERR_INTERNET_DISCONNECTED on Chromium. In-flight requests started before the flip can still complete. If your test depends on a request failing, await its rejection, do not assume it failed just because you called setOffline.
How does Assrt generate this code?
Assrt drives the official @playwright/mcp server. Its browser_network_state_set tool (playwright-core/lib/tools/backend/network.js:107) accepts state: 'offline' or 'online', calls await browserContext.setOffline(...), and returns the literal line `await page.context().setOffline(true);` as code. Assrt records that line in the run trace, so the artifact you keep is real Playwright code you can paste into a *.spec.ts. There is no Assrt-specific DSL between you and the API call.
Can I write the offline test by hand if I do not want an agent?
Absolutely. The line is literally await page.context().setOffline(true). Wrap it in a test('shows offline banner'...) block, navigate, click whatever triggers the network, and assert your offline UI. The reason to drive this through @playwright/mcp + an agent is that the agent re-resolves selectors from the accessibility tree on every run, so a redesign of the offline banner does not require touching the test file. The setOffline call itself is identical in both worlds.
What about iOS Safari and the setOffline gap on WebKit?
Playwright's WebKit driver supports setOffline, but Safari on real iOS devices does not honor offline emulation the same way; it relies on the OS-level connectivity status. If you need to verify behavior on a real iOS device, use Network Link Conditioner on the device itself, then use Playwright on desktop WebKit for the per-build automated checks. The two cover different failure modes.
Stop maintaining offline tests by hand. Get the same Playwright line out of an agent.
Try Assrt freeHow did this page land for you?
Comments
Public and anonymous. No signup.
Loading…