PRACTICAL GUIDE / Playwright geolocation permission testing

Why a mocked location still fails in Playwright

Pair browser permission with mocked coordinates, diagnose origin and policy failures, and keep geolocation tests isolated across CI workers.

By The Testing AcademyUpdated August 4, 202621 min read
All field guides
In this guide7 sections
  1. Understand the two gates before changing the test
  2. Prove the browser state and the user result together
  3. Change location only when the application asks again
  4. Diagnose permission, origin, policy, and product failures separately
  5. Keep the setup narrow when the suite reaches CI
  6. Build a matrix around decisions, not city names
  7. Know when browser geolocation emulation is the wrong tool

What you will learn

  • Understand the two gates before changing the test
  • Prove the browser state and the user result together
  • Change location only when the application asks again
  • Diagnose permission, origin, policy, and product failures separately

The map centers on your office even though the test supplied coordinates for another city. On the next run, the browser reports that location access is unavailable. Both failures usually begin with the same mistake: treating location data and permission as one browser setting when they are two separate controls.

Understand the two gates before changing the test

A web page does not receive latitude and longitude merely because the browser context has a mocked position. The page must also be allowed to use the Geolocation API. Playwright reflects that browser model. browserContext.setGeolocation() controls the position exposed by the context, while browserContext.grantPermissions() controls whether a page may read it. Set only one and the application cannot complete the same path a consenting user would take.

There is a third gate outside those two Playwright calls. Browsers expose geolocation only in a secure context. Production normally satisfies that requirement with HTTPS, and browsers generally treat loopback development addresses as trustworthy for local work. A staging site accidentally served over plain HTTP can therefore fail even when the context contains valid coordinates and the permission override is correct. That is a deployment defect, not a reason to add a wait.

Origin matters as well. grantPermissions(['geolocation'], { origin }) applies the override to the specified origin. An origin is the scheme, host, and port together. Permission granted to https://shop.example.test does not automatically cover https://www.shop.example.test, an embedded map on another origin, or the same host on a different port. If login redirects the page, inspect the final URL before blaming the location fixture.

The Geolocation API itself is asynchronous. An application calls getCurrentPosition() for one result or watchPosition() for continuing updates. Playwright supplies the browser-side position, but it does not make the application request that position again. Changing the context after a page has cached an earlier result will not magically rerun product code. The next user action, watcher callback, or reload still belongs to the application contract.

This separation gives a test useful diagnostic power. A permission failure, an unavailable position, and a valid position that produces the wrong store are different defects. Do not collapse them into one assertion such as “the map looks right.” Check the browser preconditions, then check the product result that matters to the user.

The coordinate object also has defined bounds. Latitude must be between -90 and 90, longitude between -180 and 180, and accuracy cannot be negative. Reversing latitude and longitude is an especially common mistake because mapping libraries and GeoJSON often present coordinate pairs in longitude-first order, while engineers tend to say latitude first. Name the fields explicitly rather than passing around an unlabelled number pair.

Accuracy deserves deliberate treatment. Playwright defaults it to zero when it is omitted. That does not mean a real device can identify a person with perfect accuracy. It means the emulated value is deterministic. If product behavior changes when accuracy is coarse, add a case with a non-zero value and assert the correct product branch. Do not turn the default into a claim about GPS hardware.

Finally, permission support differs among browser engines and can change across versions. Playwright's own BrowserContext documentation warns about that boundary. Keep the geolocation cases on the engines where the permission override is supported by the version you run, and make any browser-specific exclusion explicit. A silently skipped assertion is worse than a documented coverage limit.

Prove the browser state and the user result together

Start with one recognizable business scenario. A store finder should choose the nearest branch for a known coordinate. The test below sets a position on the test's context, grants permission to the exact application origin, verifies what the browser returns, and then verifies the selected store. The browser assertion catches fixture mistakes. The store assertion catches product mistakes. Either can fail independently.

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

const appOrigin = process.env.APP_ORIGIN ?? 'http://127.0.0.1:4173';
const london = {
  latitude: 51.5074,
  longitude: -0.1278,
  accuracy: 25,
};

test.use({ geolocation: london });

test.beforeEach(async ({ context }) => {
  await context.grantPermissions(['geolocation'], { origin: appOrigin });
});

test('selects the branch nearest the permitted location', async ({ page }) => {
  await page.goto(`${appOrigin}/stores`);
  await page.getByRole('button', { name: 'Use my location' }).click();

  const browserPosition = await page.evaluate(() =>
    new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        position => resolve({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
        }),
        reject,
        { maximumAge: 0, timeout: 5_000 },
      );
    }),
  );

  expect(browserPosition.latitude).toBeCloseTo(london.latitude, 4);
  expect(browserPosition.longitude).toBeCloseTo(london.longitude, 4);
  await expect(page.getByTestId('nearest-store')).toContainText('Central London');
});

The final assertion must describe your product's observable contract. A marker existing on a map proves little if the user depends on a branch name, delivery eligibility, tax region, or pickup inventory. Pick the state downstream code actually uses. In a store finder, that may be the selected store identifier plus the displayed address. In a delivery flow, it may be the serviceability result returned by the backend. The right oracle is the one a real regression can break.

The direct page.evaluate() probe is intentional here. It checks the browser input to the application without replacing application code. It does not dispatch a synthetic event, stub navigator.geolocation, or call an internal store-selection function. If the coordinate check passes while the nearest-store assertion fails, the test has isolated the defect to product logic or data rather than the permission fixture.

Keep the expected branch stable. Public store data can change, and a coordinate near a boundary can legitimately select a different location after a business update. Use seeded test data or a location comfortably inside one service region. If the nearest-store algorithm belongs to a remote service, control that service's test data and retain its response with the failure. Mocking the browser location while querying mutable production data creates a deterministic input with a non-deterministic oracle.

Do not assert raw floating-point equality for coordinates unless your application deliberately rounds them. Browsers and application serialization can preserve or transform decimals in ways that are irrelevant to the user outcome. toBeCloseTo proves the position is the intended one without making formatting part of the contract. For a geofence boundary test, use points clearly on either side and assert the classification, not the number of decimal places rendered in a debug panel.

A second positive case should cover a materially different decision. For example, test that a remote coordinate offers shipping instead of pickup, or that a regional legal notice appears. Repeating five city names through the same UI path adds maintenance without exploring another failure mode. Each coordinate should earn its place by crossing a product boundary.

Change location only when the application asks again

Travel, courier, and check-in applications often need more than an initial position. A weak test changes the context and immediately expects the page to move. That expectation assumes product code is actively watching location. If the application only calls getCurrentPosition() when the user presses a refresh button, the old screen is correct until that button is pressed.

Make that request boundary visible in the test. The following case proves that the first request uses London and a later user-triggered request uses Paris. It does not claim that setGeolocation() itself causes a UI update.

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

const appOrigin = process.env.APP_ORIGIN ?? 'http://127.0.0.1:4173';
const london = { latitude: 51.5074, longitude: -0.1278, accuracy: 30 };
const paris = { latitude: 48.8584, longitude: 2.2945, accuracy: 30 };

test.use({ geolocation: london });

test('refreshes a location after the context position changes', async ({ page, context }) => {
  await context.grantPermissions(['geolocation'], { origin: appOrigin });
  await page.goto(`${appOrigin}/check-in`);

  const refresh = page.getByRole('button', { name: 'Refresh location' });
  await refresh.click();
  await expect(page.getByTestId('current-city')).toHaveText('London');

  await context.setGeolocation(paris);
  await refresh.click();

  await expect(page.getByTestId('current-city')).toHaveText('Paris');
  await expect(page.getByRole('button', { name: 'Confirm check-in' })).toBeEnabled();
});

If this case still shows London, inspect whether the application set a non-zero maximumAge when calling getCurrentPosition(). That option permits the browser to return a cached position up to the accepted age. The right fix depends on the product. A courier tracking screen may need a fresh reading. A weather page may intentionally accept a cached location to save time and power. A test should not override a documented product choice just to make its second assertion convenient.

An application using watchPosition() has another lifecycle. It must retain the watch identifier and eventually call clearWatch(). A component that starts a new watcher on every render can produce duplicate updates. A component that clears the watcher too early can remain stuck on the first city. Test those behaviors through the product state, and use application instrumentation or a focused component test if you need to count watcher registrations. Do not infer registration count from the number of map animations.

The near-miss here is stale application state. Its symptom resembles a failed geolocation override because the screen still shows the first place. The browser probe tells them apart. If a fresh call to navigator.geolocation.getCurrentPosition() returns Paris while the screen shows London, permission and emulation are working. The defect is a cached selector, missed watcher update, ignored callback, or product rule. If the browser probe also returns London, inspect the context and test ordering.

Avoid reusing one page for a large matrix of cities unless the product explicitly supports live movement. A fresh context for each static region is easier to understand and closer to a new user session. Use a moving-location case only for behavior that depends on movement. That split also prevents one failed transition from contaminating every later assertion in a parameterized loop.

Position unavailable is a different branch again. Playwright documents that passing null to setGeolocation() emulates an unavailable position. Keep permission granted so the application cannot confuse lack of data with lack of consent.

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

const appOrigin = process.env.APP_ORIGIN ?? 'http://127.0.0.1:4173';

test('offers manual search when position is unavailable', async ({ page, context }) => {
  await context.grantPermissions(['geolocation'], { origin: appOrigin });
  await context.setGeolocation(null);
  await page.goto(`${appOrigin}/stores`);

  await page.getByRole('button', { name: 'Use my location' }).click();

  await expect(page.getByRole('alert')).toContainText('We could not get your location');
  await expect(page.getByLabel('Search by postcode')).toBeVisible();
  await expect(page.getByTestId('nearest-store')).toBeHidden();
});

That oracle can fail in several meaningful ways. The application might leave an old store selected, omit the manual fallback, or show a permission-denied message for an availability error. Each is a user-facing defect. Asserting only that the geolocation callback rejected would miss all three.

Diagnose permission, origin, policy, and product failures separately

When a CI failure says only that a branch name was absent, collect the browser facts in the same run. The Permissions API can report granted, denied, or prompt for geolocation where the browser supports that query. A direct position request can then record either coordinates or the numeric error code. Treat the message as diagnostic text because wording can differ by engine and operating system.

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

const appOrigin = process.env.APP_ORIGIN ?? 'http://127.0.0.1:4173';
const london = { latitude: 51.5074, longitude: -0.1278, accuracy: 25 };

test.use({ geolocation: london });

test('records the geolocation preconditions', async ({ context, page }, testInfo) => {
  await context.grantPermissions(['geolocation'], { origin: appOrigin });
  await page.goto(`${appOrigin}/stores`);

  const probe = await page.evaluate(async () => {
    const permission = await navigator.permissions.query({ name: 'geolocation' });

    const result = await new Promise<
      | { kind: 'position'; latitude: number; longitude: number; accuracy: number }
      | { kind: 'error'; code: number; message: string }
    >(resolve => {
      navigator.geolocation.getCurrentPosition(
        position => resolve({
          kind: 'position',
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
          accuracy: position.coords.accuracy,
        }),
        error => resolve({ kind: 'error', code: error.code, message: error.message }),
        { maximumAge: 0, timeout: 5_000 },
      );
    });

    return {
      origin: window.location.origin,
      secureContext: window.isSecureContext,
      permission: permission.state,
      result,
    };
  });

  await testInfo.attach('geolocation-probe', {
    body: Buffer.from(JSON.stringify(probe, null, 2)),
    contentType: 'application/json',
  });

  expect(probe.origin).toBe(appOrigin);
  expect(probe.secureContext).toBe(true);
  expect(probe.permission).toBe('granted');
  expect(probe.result.kind).toBe('position');
});

The setup lines are what make this diagnostic honest. A probe that asserts granted while never granting anything is not a diagnostic, it is a test that is red on healthy input: run it without the grantPermissions call and Chromium returns prompt, then rejects the position request with error code 1 and the message User denied Geolocation. That is the correct browser behaviour for a page nobody consented to, and no amount of waiting changes it. The grant and the emulated coordinates have to be present for granted and position to be the expected values.

With the setup in place, every assertion still has a real failure path. Serving the page from an insecure origin makes the secure-context assertion fail. Granting permission to a different host or port leaves the permission state at prompt and fails the third assertion. Removing the emulated coordinates while keeping the grant leaves permission at granted but changes result.kind to error, so the two halves fail independently and tell you which one broke. The origin assertion catches the redirect case, where the grant was correct for the origin the test named and wrong for the origin the page ended up on. None of that is a hard-coded fixture checking itself.

Read the result in order. First compare probe.origin with the origin configured by the test. Then check secureContext. Next check permission. Only after those pass should you inspect coordinates and product behavior. That sequence prevents a team from adjusting map waits when the browser never allowed the call.

An iframe adds another policy layer. Geolocation is controlled by Permissions Policy, and third-party frame access must be allowed by the embedding document as well as by browser permission. A context-level grant cannot repair a production response header or iframe allow attribute that blocks the feature. If the location UI lives in a frame, retain the top-level URL, frame URL, and relevant response headers. A failure confined to the frame points toward policy or origin configuration rather than the coordinate object.

A prompt state is not the same as a deterministic denied state. Omitting a Playwright permission override can leave the result dependent on browser mode, engine, and version. Do not build a cross-browser “user clicked Deny” test from that assumption. If the exact native prompt choice is a release requirement, use only a browser automation capability that officially supports that interaction, document the engine limit, and keep application-level handling covered separately.

Trace Viewer helps with the product half of the investigation. Open the action around “Use my location” and compare the before and after DOM snapshots. Check whether the button was found, whether the alert or selected-store element appeared, and whether a redirect changed the origin. The trace does not turn browser chrome into application DOM, so absence of a permission prompt in a snapshot is not proof that permission was granted. The attached probe supplies that missing fact.

Console output can expose a different near-miss. An uncaught TypeError inside the success callback may leave the UI unchanged even though the browser returned a valid position. Capture page errors in the failing test or rely on the trace's console view. A valid probe plus a page error after the click is strong evidence that the fixture worked and product code crashed while consuming it.

Network evidence matters when coordinates are sent to a backend. Confirm the request occurred and inspect the serialized latitude and longitude. A swapped pair, rounded boundary value, or stale cached request can all produce the wrong region while the browser state remains correct. Mask any precise real-user location in retained artifacts; test coordinates should be synthetic and documented as such.

Keep the setup narrow when the suite reaches CI

Global geolocation configuration is convenient but broad. Every test in the project then receives coordinates, and a product regression that reads location too early may go unnoticed because permission or position was already available. Prefer a dedicated project or test-level test.use() block for location cases. The dependency is visible beside the tests that need it.

TypeScript
import { defineConfig, devices } from '@playwright/test';

const baseURL = process.env.APP_ORIGIN ?? 'http://127.0.0.1:4173';

export default defineConfig({
  testDir: './tests',
  retries: process.env.CI ? 1 : 0,
  reporter: [['html', { open: 'never' }], ['line']],
  use: {
    baseURL,
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      testIgnore: /geolocation\.spec\.ts/,
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'geolocation-chromium',
      testMatch: /geolocation\.spec\.ts/,
      use: {
        ...devices['Desktop Chrome'],
        geolocation: { latitude: 51.5074, longitude: -0.1278, accuracy: 25 },
      },
    },
  ],
});

Two details in that configuration are load-bearing, and both are easy to leave out. The first is testIgnore on the general project. testMatch on the dedicated project narrows what that project runs; it does not stop any other project from picking the same file up. Without the exclusion, the general project also collects geolocation.spec.ts and every location case executes twice per run, once under a project designed for it and once under a project that was never meant to see it. Confirm the shape with playwright test --list rather than assuming: the listing prints one line per project and file, so a duplicated spec is visible immediately, and the total drops from two lines to one once the exclusion is in place.

The second is that the dedicated project must actually differ from the general one. Two projects with byte-identical use blocks are the same project twice over with different labels, and the extra name suggests an isolation that does not exist. Here the difference is a default emulated position, so a location spec that forgets its own test.use({ geolocation }) still runs against a deterministic coordinate instead of whatever the unconfigured browser reports.

The location project still does not grant permission in configuration. Each spec grants it to the application origin, making a redirect mismatch visible. If a whole project genuinely shares one consent model, permissions: ['geolocation'] is a supported context option, but that convenience trades away some test clarity.

Default Playwright Test fixtures create a fresh browser context per test. Keep that isolation unless measured suite cost justifies a custom context. A worker-scoped context can retain permission overrides and current coordinates. If one is unavoidable, teardown must call clearPermissions() and restore or clear the position before another case uses the context. Closing the context is simpler and harder to get wrong.

Roll out the change in stages. First add the browser probe to one failing scenario without changing its existing user assertion. Once the team can distinguish fixture failure from product failure, replace global permission grants with test-level grants. Then separate static-location cases from movement cases. Finally, run the location project in CI and retain traces only on failure or retry. Tracing every passing map test adds storage and runtime without improving routine diagnosis.

Do not treat a passing retry as proof that permission setup is sound. Playwright retries rerun the test in a new worker after a failure in many configurations, which can erase the leaking context that caused the first attempt to fail. Review the first attempt's probe and trace. If the retry passes because it received clean browser state, the suite still has an isolation defect.

Build a matrix around decisions, not city names

A useful geolocation matrix separates consent, position availability, and product classification. The permitted-position case proves that a known coordinate reaches the application and produces a known regional decision. The permitted-but-unavailable case proves the manual fallback. A movement case is justified only when the product requests position more than once. Those rows exercise different mechanisms, even if they share the same page.

Do not turn the matrix into ten capitals that all select a store. Add coordinates only when they cross a business boundary: inside versus outside a delivery zone, one tax jurisdiction versus another, a supported country versus an unsupported one, or coarse accuracy versus a feature's declared threshold. Give each boundary its own expected domain result. If two coordinates drive identical code and assertions, keep the clearer one.

Location does not set locale, language, or time zone. A context positioned in Paris can still use an English locale and another time zone unless the test configures those options separately. That combination may be useful for a traveler, or it may be impossible for the scenario being tested. State each emulated dimension explicitly when regional formatting or opening hours affect the result, and avoid attributing a time-zone failure to the coordinate override.

Consent denial needs careful ownership. Playwright can grant and clear permission overrides, but clearing an override restores the browser's default permission behavior; it is not a universal simulation of a user pressing a native Deny button. Cover the application's mapping from a denied Geolocation API result at the smallest layer where that result can be controlled honestly. Keep a browser-level denial case only for engines and versions where the denial mechanism is documented and stable, and label that coverage limit in the test.

Embedded content deserves a separate matrix row when it is supported. A same-origin frame and an allowed third-party frame have different Permissions Policy requirements. Use a fixture response with the real policy header and iframe attribute, then assert behavior inside the frame. Do not reuse the top-level case and claim it covers embedding merely because both pages share a browser context.

Record the reason for every exclusion. If WebKit in the pinned Playwright release cannot support a permission override used by the scenario, annotate that exact reason and link it to a tracked upgrade or browser issue. A broad browserName !== 'chromium' skip with no contract gradually becomes permanent. Recheck the matrix during Playwright upgrades because permission support is one of the areas the official API warns can change.

The matrix has an execution cost. Maps load tiles, reverse-geocoding calls consume service capacity, and each browser context adds time. Stub external map presentation where it is not the subject, seed regional decisions, and keep one integrated path that proves the real boundaries connect. That balance gives more information than multiplying every coordinate by every browser and retrying the resulting noise.

Know when browser geolocation emulation is the wrong tool

Context geolocation is ideal for deterministic application paths that consume the Web Geolocation API. It is not a simulator for a phone's GPS receiver. It cannot prove satellite accuracy, indoor drift, battery behavior, operating-system location settings, or the wording and placement of native permission UI. Those checks require device testing, platform tooling, or a carefully scoped manual pass.

It also does not change an IP address. A backend that derives country from the request IP will ignore the browser's latitude and longitude unless the application sends them. If the test expects regional pricing based on IP, use an environment or proxy designed for that path. Mixing an emulated coordinate with an unchanged server-side location can create an impossible user state and a misleading failure.

Do not use the technique to bypass a product abstraction that is easier to test at a lower level. A pure function that maps coordinates to a delivery zone belongs in unit or API tests with a large boundary matrix. Keep one or two browser cases to prove that permission, position retrieval, serialization, and visible UI are connected. Pushing hundreds of coordinate pairs through a map page makes the suite slow while giving poor feedback when the classification rule changes.

Avoid it for native mobile permission dialogs wrapped around a web view. Playwright controls the browser context, not the host operating system's app permission. Appium or platform-specific automation is the correct layer when the host permission is the behavior under test. A web-only case can still cover the content after the web view receives location, but it must not claim native-dialog coverage.

There is a privacy trade-off even with test data. Traces, screenshots, URLs, and attached JSON can retain coordinates. Use clearly synthetic locations, avoid employees' homes or customer addresses, and decide how long artifacts remain available. The diagnostic probe above is useful because its contents are explicit; it is also a file that your retention policy must cover.

Do not emulate a location merely to make an unrelated test pass. If the application should work when users decline or cannot provide location, that fallback is part of the product. A suite in which every context silently has geolocation permission will never exercise it. Keep consent, unavailable-position, and manual-entry paths distinct, and let each one fail for the user outcome it owns.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official developer.mozilla.org reference

    developer.mozilla.org

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Why does Playwright geolocation fail after I set coordinates?

Coordinates alone do not let a page read location. Grant the geolocation permission to the page's actual origin, then set the latitude and longitude on the same browser context.

Should I put geolocation and permissions in playwright.config.ts?

Use project configuration only when every test in that project needs the same location. Test-level settings make the dependency visible and reduce the chance that an unrelated test passes because location access was granted globally.

Can a redirect break an origin-scoped geolocation permission?

A redirect to another origin can leave the permission override attached to the old origin. Compare the final page URL with the origin passed to grantPermissions before investigating the coordinates.

How do I stop geolocation permission leaking between tests?

Keep the default Playwright Test context fixture, which creates an isolated context for each test. If a custom fixture reuses a context, clear permission overrides and restore the intended location during teardown.

How can I test that the current position is unavailable?

Passing null to browserContext.setGeolocation emulates an unavailable position while leaving permission as a separate control. Assert the application's user-facing recovery state, not a browser-specific error message.