Source-walked reference

Playwright setOffline and navigator.onLine: what the docs don't say

Direct answer (verified 2026-05-13)

Yes, await context.setOffline(true) does flip navigator.onLine to false in Chromium and WebKit. Internally it dispatches the CDP command Network.emulateNetworkConditions (Chromium) or Network.setEmulateOfflineState (WebKit). The official BrowserContext.setOffline reference does not mention either command or navigator.onLine by name, which is why the question keeps coming up. The traps below are also in the source, also not in the docs.

M
Matthew Diakonov
8 min read

Source paths in this page are from the Playwright repository at github.com/microsoft/playwright. Line numbers were checked against main on the verification date above; the file structure has been stable for several minor releases.

What the docs say

The entire entry for BrowserContext.setOffline on playwright.dev is the signature, the boolean parameter, and one sentence:

await browserContext.setOffline(offline);
// offline (boolean) Whether to emulate network being offline for the browser context.

That is everything. Nothing about navigator.onLine, nothing about the window offline event, nothing about whether the three browser engines behave the same way. People search for this question because the docs do not answer it, not because the answer is hidden.

The good news: the answer is in the open-source implementation, two files deep. The rest of this page is that walk.

What Chromium actually does

The call chain on Chromium starts at the BrowserContext, hops through CRPage, and lands in CRNetworkManager. In packages/playwright-core/src/server/chromium/crNetworkManager.ts, lines 229 through 250:

async setOffline(offline: boolean) {
  if (offline === this._offline)
    return;
  this._offline = offline;
  await this._forEachSession(info => this._setOfflineForSession(info));
}

private async _setOfflineForSession(info: SessionInfo, initial?: boolean) {
  if (initial && !this._offline)
    return;
  if (info.workerFrame)
    return;
  await info.session.send('Network.emulateNetworkConditions', {
    offline: this._offline,
    latency: 0,
    downloadThroughput: -1,
    uploadThroughput: -1
  });
}

The load-bearing line is the protocol send. Playwright is asking Chromium to flip an internal flag via the same CDP command Network.emulateNetworkConditions that Chrome DevTools' throttling panel uses. The Chromium implementation of that command flips navigator.onLine to false, dispatches a window offline event, and starts failing outgoing HTTP and HTTPS requests. The latency and throughput fields are present because the same CDP method handles network throttling; setting throughput to -1 disables the throttle so only the offline flag is in play.

So in Chromium, the chain is: your test code, the Playwright client, the Playwright server, one CDP command, one browser-engine flag. The boolean you read with page.evaluate(() => navigator.onLine) reflects that flag, and it returns false.

The protocol chain, end to end

Test codePlaywright clientPlaywright serverBrowser (CDP/WK protocol)await context.setOffline(true)BrowserContext.setOffline RPCcontext._options.offline = trueNetwork.emulateNetworkConditions (Chromium)Network.setEmulateOfflineState (WebKit)navigator.onLine = false; fire 'offline' eventackPromise resolves

What WebKit does differently

WebKit does not speak CDP. It speaks its own Web Inspector protocol, which is similar in shape but uses different method names. Playwright has a parallel implementation in packages/playwright-core/src/server/webkit/wkPage.ts, lines 1068 through 1070:

async updateOffline() {
  await this._updateState('Network.setEmulateOfflineState', {
    offline: !!this._browserContext._options.offline,
  });
}

Same idea, different method. Playwright is asking WebKit to flip the equivalent internal flag via Network.setEmulateOfflineState. That command, on the WebKit side, also flips navigator.onLine and fires the offline event. The reason it shows up in support questions more often than the Chromium version is the timing bug below.

Per-engine behaviour matrix

Same Playwright API, three engines, three slightly different shapes. This is the part the docs flatten into "emulates network being offline".

EngineProtocol commandnavigator.onLinePre-nav setOffline
ChromiumNetwork.emulateNetworkConditionsflips to falseworks
WebKitNetwork.setEmulateOfflineStateflips to false after navfails (issue #34402)
Firefoxjuggler offline RPCflips to false after navfails (issue #34402)

Issue numbers refer to the microsoft/playwright issue tracker; #34402 was closed as not planned, so the workaround is permanent: navigate first, then setOffline.

The four traps the docs also skip

Each of these is a closed or stale issue in the Playwright repo. None of them appear on the BrowserContext API page. All four can produce a green run that ships a broken offline experience, so they are worth knowing.

Trap 1 (issue #34402)

setOffline before the first navigation breaks on WebKit and Firefox

The reporter set up context.setOffline(true) before any page.goto. The test passes in Chromium, fails in WebKit and Firefox, and for an extra surprise it corrupts state for subsequent tests in WebKit. Maintainers closed it as not planned. Fix: navigate first, flip offline second.

Trap 2 (issue #2311)

Service workers keep serving cached responses

Filed May 2020. A Workbox-based service worker continued to answer fetch events from cache after setOffline(true), so the test could not verify the app's offline behaviour. navigator.onLine flips, but the SW does not care about it; it answers from the cache it already holds. Fix: start the test with an empty cache, or assert at the UI layer rather than the network layer.

Trap 3 (issue #4910)

WebSockets stay open and keep flowing

Reported on Playwright 1.7.1 across Chromium and Firefox: HTTP requests fail under setOffline(true), but a socket opened before the flip continues to send and receive. Closed without a behavioural change. Fix: close the socket from the app side before going offline, or assert on a UI-side disconnect indicator instead of on the socket itself.

Trap 4 (engine throttling deprecation)

Network.emulateNetworkConditions only works in Chromium-based browsers

If you reach for the raw CDP method (via context.newCDPSession(page)) instead of setOffline, you get the same offline flip but you also opt into the throttling fields. That code path does not exist on WebKit or Firefox; calling it there will throw. Stick to setOffline unless you specifically need latency or throughput emulation, and even then, gate the CDP path on browserName === 'chromium'.

The six-step verification recipe

Drop this into any test where you want to confirm navigator.onLine actually flipped and the offline event actually fired. It works across all three engines if you keep the order.

Verifying setOffline end-to-end

1

Navigate first

Start with await page.goto('/'). If you flip offline before the first navigation in WebKit or Firefox, you hit issue #34402 and the test fails inconsistently.

2

Install the listener before going offline

page.evaluate adds an addEventListener('offline', ...) that resolves a promise on fire. Wire this up while the page is still online, otherwise you miss the event.

3

Flip offline

await context.setOffline(true). Internally this sends Network.emulateNetworkConditions (Chromium) or Network.setEmulateOfflineState (WebKit) to the browser.

4

Assert the boolean

await page.evaluate(() => navigator.onLine) should be false. If it is true, you are on a pre-navigation timing bug; reorder steps 1 and 3.

5

Assert the event fired

Await the promise the listener wired up. If the 'offline' event never resolves, the engine probably acked the protocol command but skipped the JS dispatch; file a Playwright issue with the engine and version.

6

Restore

await context.setOffline(false). Skipping this in a beforeEach leaves a poisoned context for the next test on the same worker; the symptom is mysterious cross-test network failures.

As a runnable spec:

import { test, expect } from '@playwright/test';

test('setOffline flips navigator.onLine and fires the offline event', async ({ page, context }) => {
  // 1. Navigate first (avoids issue #34402 on WebKit and Firefox).
  await page.goto('/');

  // 2. Install the offline listener while still online.
  const offlineFired = page.evaluate(() => new Promise<string>((resolve) => {
    addEventListener('offline', () => resolve('fired'), { once: true });
  }));

  // 3. Flip offline. Internally:
  //    Chromium -> Network.emulateNetworkConditions { offline: true, ... }
  //    WebKit   -> Network.setEmulateOfflineState   { offline: true }
  await context.setOffline(true);

  // 4. The boolean reflects the engine flag.
  expect(await page.evaluate(() => navigator.onLine)).toBe(false);

  // 5. The JS event fired in the page context.
  expect(await offlineFired).toBe('fired');

  // 6. Restore so the next test on this worker is not poisoned.
  await context.setOffline(false);
  expect(await page.evaluate(() => navigator.onLine)).toBe(true);
});

If the boolean assertion in step 4 fails, you are almost certainly on the pre-navigation timing bug; check that page.goto ran before setOffline. If the event assertion in step 5 fails but the boolean passes, the engine acked the protocol command but skipped the JS event; that is worth a new Playwright issue with the engine and version.

When setOffline is the wrong tool

Three cases where the right answer is not setOffline at all.

  • You want to fail one request, not all. Use page.route with route.abort('internetdisconnected'). setOffline is context-wide, so it cannot scope.
  • You need slow network, not no network. Drop to a raw CDP session and call Network.emulateNetworkConditions with real latency and throughput values. Chromium only.
  • You need to test reconnect behaviour. setOffline followed by setOffline(false) gives you the cleanest transition, but pair it with an explicit addEventListener('online', ...) assertion so you know the app saw the recovery, not just that the request layer recovered.

Want to walk a flaky offline test together?

If you have a Playwright suite where setOffline does not produce the navigator.onLine you expect, bring it to the call. The fix is almost always one of the four traps on this page; the trick is matching it to the right one.

Frequently asked questions about setOffline and navigator.onLine

Does Playwright's context.setOffline(true) actually set navigator.onLine to false?

Yes in Chromium and WebKit. The Chromium implementation in packages/playwright-core/src/server/chromium/crNetworkManager.ts (lines 229-250) dispatches the CDP command Network.emulateNetworkConditions with { offline: true, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }. That command flips navigator.onLine at the browser engine level and fires the window 'offline' event. The WebKit implementation in packages/playwright-core/src/server/webkit/wkPage.ts (lines 1068-1070) dispatches Network.setEmulateOfflineState, which has the same effect inside WebKit. The official BrowserContext docs do not mention navigator.onLine, the offline event, or either protocol command, which is why this question shows up in search at all.

Why does the official Playwright docs page only say one sentence about it?

Because the docs page is a method reference, not a behaviour spec. The whole entry for setOffline is the signature plus 'Whether to emulate network being offline for the browser context.' Everything else, what protocol command runs, which browser-internal flags flip, what happens to navigator.onLine and the offline event, is left to the source. That is workable when the engine behaviour is consistent across Chromium, Firefox, and WebKit. It is not, which is why teams hit surprises in cross-browser test runs and end up Googling this exact query.

Why does it not work when I call setOffline before page.goto in WebKit or Firefox?

Known bug, GitHub issue #34402, closed as not planned. The reporter ran the same offline test across all three engines and watched WebKit and Firefox fail on the 'works offline when immediately setting browser to offline' case, while Chromium passed. The maintainers chose not to address it. The practical workaround is to navigate first, then call setOffline, which lines up with what most real test code does anyway. If you need an offline state at the very start of a navigation, you will need page.route to abort the requests instead and accept that navigator.onLine will not flip.

Why does my service worker still answer requests after setOffline(true)?

Known and tracked in GitHub issue #2311, filed in May 2020 and still labelled P3-collecting-feedback. The original reporter used a Workbox cache-first strategy; after context.setOffline(true), the SW kept serving cached responses, so the offline state was invisible to the app. The reason is that setOffline blocks network traffic between the browser and the outside world; it does not stop the service worker, which has already cached the responses inside the browser. If your test needs to assert offline behaviour AND your app uses a service worker, you need either an empty cache, a route handler that aborts SW-bound traffic, or a separate context for the SW-aware test.

What about WebSocket connections, do they stop too?

No. GitHub issue #4910, filed against Playwright 1.7.1 by a reporter who confirmed the behaviour across Chrome and Firefox: HTTP requests fail, open web socket connections continue to send and receive data. The issue is now closed. If you have a long-lived socket and need to test offline drop, you either need to close the socket from the app side before going offline, or assert at a higher layer (the UI should show a disconnect state) rather than relying on the socket itself to error.

Is there a difference between setOffline and the page.context().setOffline() invocation?

They are the same method, accessed from two paths. context.setOffline lives on BrowserContext; page.context().setOffline() is the same call via the page handle. Both go through CRPage.updateOffline (Chromium) or wkPage.updateOffline (WebKit), which forwards to the network manager. There is no per-page offline mode in Playwright; if you flip offline on a context with multiple pages, every page on that context goes offline together. If you need per-page isolation, use one context per page.

Can I verify navigator.onLine flipped from inside the test?

Yes, and it is the cleanest sanity check. await page.evaluate(() => navigator.onLine) returns false right after await context.setOffline(true) in Chromium and (post-navigation) WebKit. The same evaluate also lets you wire up listeners: await page.evaluate(() => new Promise(r => addEventListener('offline', () => r('fired')))) before the setOffline call lets you prove the offline event reached the page. If the boolean does not flip in your run, you have probably hit the pre-navigation timing bug above; navigate first, then setOffline, then re-check.

What does Assrt do with offline behaviour in its generated tests?

Assrt generates real Playwright code; nothing proprietary. When you describe an offline scenario, the generated file calls context.setOffline(true) the same way you would write it by hand, and you can read and edit the resulting .spec.ts file. The MCP server source is at github.com/assrt-ai/assrt-mcp; the agent that produces the test code reads the same Playwright API surface this page walks. If a generated test produces a surprising result around navigator.onLine, the fix is the same as for hand-written code: navigate first, then setOffline, and assert with page.evaluate against navigator.onLine.

A short closing

The Playwright BrowserContext API is one of the most polished surfaces in any test framework, but the offline section is genuinely underdocumented. The good news is that the behaviour is fully knowable from the source: a few dozen lines in two files tell you exactly which protocol command flips your boolean, which engines have a timing bug, and which adjacent layers (service workers, WebSockets, raw CDP) do not behave the way the metaphor suggests. The next time the docs feel thin, read the source; the codebase rewards it.

How did this page land for you?

React to reveal totals

Comments ()

Leave a comment to see what others are saying.

Public and anonymous. No signup.