skip to content

In Playwright, why does navigator.geolocation still fail after you set the context's geolocation option?

level: middleimportance: should knowfreq 41%

answer

  1. Two settings, not one
  2. Coordinates do not imply consent
  3. A second context option unlocks the read
  4. Runtime setter moves every page at once
  5. Null position drives the error branch

basics

~10 s

Because coordinates and permission are two separate context settings. Playwright's geolocation option only supplies a position; the context must also hold the geolocation permission before the page is allowed to read it.

solid answer

~40 s

Playwright's `geolocation` context option feeds a fake position into the browser, but reading it is still gated by the browser's permission model, which defaults to `prompt` and is never answered in an automated run. You need both settings on the context: `geolocation: { latitude, longitude }` and `permissions: ['geolocation']` — as a pair in `browser.newContext()` or `test.use()`. After that, `context.setGeolocation()` moves the position mid-test for **every page in the context**, and passing `null` emulates position unavailable. The coordinates are validated, so `longitude: 200` rejects with a precondition error rather than silently clamping. Popups opened from the page inherit both the position and the permission, since they belong to the same context.

code

typescript · 19 lines
typescript
import { test, expect } from '@playwright/test';

test.use({
  geolocation: { latitude: 41.890221, longitude: 12.492348 },
  permissions: ['geolocation'],
});

test('nearby hotels follow the emulated position', async ({ page, context }) => {
  await page.goto('/search');
  await expect(page.getByTestId('near-you-city')).toHaveText('Rome');

  await context.setGeolocation({ latitude: 48.858455, longitude: 2.294474 });
  await page.getByRole('button', { name: 'Search again' }).click();
  await expect(page.getByTestId('near-you-city')).toHaveText('Paris');

  await context.setGeolocation(null);
  await page.getByRole('button', { name: 'Search again' }).click();
  await expect(page.getByRole('alert')).toContainText('enter a city');
});

go deeper

for a junior

Remember that faking a location takes two context settings, not one: the coordinates and the geolocation permission. Set both together when you create the context or in a test.use block.

for a middle

Explain why they are separate — the coordinates feed the browser a position while the permission decides whether the page may read it — and know that the runtime setter moves the whole context at once.

for a senior

Diagnose the failure end to end: missing permission, an origin mismatch, an app that checks permission state before offering the feature, or an error callback the page swallows silently.

for a principal

Be clear about what emulated position buys you. It proves the location code path and its fallbacks run, and it does not stand in for real device accuracy or the consent experience a user actually sees.

## Two settings, not one Playwright models the browser's geolocation in two independent pieces, and a test that sets only the first looks broken: - **`geolocation`** — a `{ latitude, longitude, accuracy? }` object that becomes the position the browser hands back. It is a browser-context option, so `browser.newContext({ geolocation })` or `test.use({ geolocation })`. - **`permissions`** — the list of browser permissions the context holds. Geolocation is not granted by default; the browser's permission state starts at `prompt`, and nothing in a headless run answers a prompt, so `navigator.geolocation.getCurrentPosition()` never reaches your success callback. Supplying coordinates does not imply consent. That asymmetry is deliberate: it lets you test the denied path as well as the granted one, simply by leaving the permission out. | Context settings | What the page experiences | |---|---| | Coordinates only | The read is never allowed; the success callback never fires | | Permission only | Access is allowed, but there is no emulated position to hand back | | Both together | `getCurrentPosition()` resolves with the coordinates you supplied | ## The working shape ```ts const context = await browser.newContext({ geolocation: { latitude: 41.890221, longitude: 12.492348 }, permissions: ['geolocation'], }); ``` or, in the runner, the same two keys in one `test.use()` block. On a hotel-booking site this is what makes "hotels near you" testable at all: the search page calls `navigator.geolocation.getCurrentPosition()`, gets Rome, and queries the tenant's inventory for Rome — no real device and no real GPS involved. ## Moving the position during a test `context.setGeolocation({ latitude, longitude })` changes the emulated position after the context exists. Three properties matter: 1. **It is context-wide.** There is no per-page geolocation; every page and popup in the context moves at once. 2. **`watchPosition` observers fire.** A page that registered `navigator.geolocation.watchPosition()` receives a new reading for each `setGeolocation()` call, which is how you test a live "recalculating nearby hotels" view without a moving device. 3. **`null` means unavailable.** `context.setGeolocation(null)` emulates position unavailable, which is the cheapest way to drive the app's error branch — the "we couldn't find you, enter a city" fallback. ## Validation and isolation Playwright validates the coordinates rather than passing them through to the browser: - `longitude: 200` rejects with `geolocation.longitude: precondition -180 <= LONGITUDE <= 180 failed.` - A missing `latitude` rejects with `geolocation.latitude: expected float, got undefined`. Both surface as a rejected promise at the call site, so a bad fixture fails loudly instead of producing a plausible-looking wrong location. Isolation follows the context boundary, like the rest of the emulated environment: - Two contexts can sit on different positions simultaneously and neither observes the other's. - A popup opened with `window.open()` inherits the parent context's coordinates and permission, because it is a page in the same context. - Closing and recreating the context resets both the position and the permission. ## The shape of the position object The object is small and strict, and knowing its edges saves a debugging session: - `latitude` and `longitude` are required floats, bounded to -90..90 and -180..180. - `accuracy` is optional and defaults to `0`, which is what the page sees in `position.coords.accuracy`. If the booking site hides the "near you" panel below an accuracy threshold, that default is the reason it never appears — set `accuracy` explicitly. - The object you pass at context creation is not mutated by a later `setGeolocation()` call, so a shared fixture object stays reusable across tests. - Coordinates are numbers, not a place name; resolving "Rome" to a latitude and longitude is your fixture's job, not Playwright's. ## Diagnosing the failure When "hotels near you" never renders in a Playwright run, work through this order: - **Is the permission on the context?** Ninety per cent of the time the answer is a missing `permissions: ['geolocation']` next to the coordinates. - **Is the origin what you think it is?** Permissions are per-origin, so a grant scoped to one origin does nothing after the test navigates to another. - **Is the page reading position at all?** If the app calls `navigator.permissions.query()` first and hides the button when the state is not granted, no amount of coordinate emulation helps until the permission is there. - **Is the app silently swallowing the error callback?** An empty error handler makes a denied read indistinguishable from a hanging one; add a temporary `page.on('console')` or assert the fallback UI to tell them apart. ## What this does and does not prove Emulated geolocation proves that the app's location-dependent code path runs with a known position and that the UI reacts to a change or an unavailable reading. It does not exercise real GPS accuracy, permission prompts as a human sees them, or a device that drifts — those live outside what a browser context can emulate.

  • How would you test the branch where the guest refuses to share their location?
    Leave `permissions` off the context, or scope the grant to a different origin, so the read is never allowed. The app's error callback fires and you assert the fallback UI. Separately, `context.setGeolocation(null)` covers the granted-but-position-unavailable case, which is a different branch.
  • Can two pages in one Playwright context sit at different emulated positions?
    No. Geolocation is a property of the browser context, so `context.setGeolocation()` moves every page and popup in it at once. Two simultaneous positions require two contexts, which in the runner usually means two tests or two projects.

saying these in an interview costs you the question

  • Thinks the geolocation option alone grants access to the position
  • Expects a permission prompt to appear and be clickable in a run
  • Believes geolocation can be set per page rather than per context
  • Assumes out-of-range coordinates are clamped instead of rejected
  • Confuses an unavailable position with a denied permission