QA How-To
Playwright geolocation emulation: Examples and Best Practices
Explore playwright geolocation emulation examples for fixed cities, movement, unavailable positions, request checks, regional matrices, privacy, and CI testing.
22 min read | 2,472 words
TL;DR
Use Playwright context APIs to set position and permission, then assert a product result such as store selection, availability, or fallback. Validate outgoing coordinates when request mapping is part of the risk.
Key Takeaways
- Use a routed HTTPS origin to make isolated geolocation examples realistic and deterministic.
- Prefer named coordinate objects and origin-scoped permissions for clear, safe test setup.
- Assert both initial and updated states when testing watchPosition and movement.
- Capture coordinate requests to detect swapped fields, incorrect precision, and broken contracts.
- Use Playwright projects for a small risk-based regional matrix, not every possible combination.
- Keep boundary mathematics below the browser layer and sample user-visible decisions in UI tests.
Useful playwright geolocation emulation examples cover business outcomes, not just navigator.geolocation. A mature suite tests a known position, a permission boundary, movement, unavailable location, regional projects, and backend requests while keeping every browser context isolated.
This cookbook provides runnable TypeScript patterns and explains when each belongs in UI, integration, or lower-level coverage. The examples use current Playwright Test APIs: test.use(), context geolocation, grantPermissions(), clearPermissions(), and setGeolocation().
TL;DR
test.use({
geolocation: { latitude: 41.8781, longitude: -87.6298 },
permissions: ['geolocation'],
});
| Example | Main risk covered | Key API |
|---|---|---|
| Fixed city | Wrong regional result | test.use({ geolocation, permissions }) |
| Origin-scoped access | Overbroad permission | context.grantPermissions() |
| Location change | Stale watcher or cache | context.setGeolocation() |
| Position unavailable | Missing recovery path | context.setGeolocation(null) |
| Region matrix | Configuration drift | Playwright projects |
| Coordinate request | Swapped or rounded payload | page.waitForRequest() |
Prefer representative coordinates and exact user-facing assertions. Use broad geographic data sets below the browser layer.
1. Build reusable playwright geolocation emulation examples
A small routed page makes geolocation examples runnable without an external service. It also preserves a secure HTTPS origin, which is the correct environment for a powerful web API.
// tests/support/serve-geo-page.ts
import type { Page } from '@playwright/test';
export const GEO_ORIGIN = 'https://geo.example.test';
export async function serveGeoPage(page: Page): Promise<void> {
await page.route(GEO_ORIGIN + '/', route => route.fulfill({
contentType: 'text/html',
body: `
<button id="locate">Locate me</button>
<output id="result">Not requested</output>
<script>
const output = document.querySelector('#result');
document.querySelector('#locate').addEventListener('click', () => {
navigator.geolocation.getCurrentPosition(
position => {
output.textContent = JSON.stringify({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
});
},
error => output.textContent = 'ERROR:' + error.code
);
});
</script>
`,
}));
}
A test imports the helper, grants permission to GEO_ORIGIN, sets a position, and navigates. Routing fulfills the document inside the browser, but the page still has the HTTPS origin.
Do not turn this helper into a universal product mock. It exists to verify browser controls and teach the API. Product tests should navigate to the actual application or a purpose-built local test app. When they mock downstream services, route handlers should validate requests and model only the dependency boundary under test.
Store coordinate fixtures as named objects, not [41.8781, -87.6298]. Named latitude and longitude properties remove ordering ambiguity and match Playwright's API.
Keep the harness small enough that a reader can see where browser behavior ends. It should not contain city lookup, distance calculation, permission policy, and UI rendering all at once. Those concerns belong to product code or separate controlled fakes. A narrow harness makes an unexpected browser-engine difference easier to isolate.
2. Run a fixed-city playwright geolocation emulation example
The first complete test proves that the page receives the configured position. It uses an exact JSON value because every coordinate is controlled.
import { test, expect } from '@playwright/test';
import { GEO_ORIGIN, serveGeoPage } from './support/serve-geo-page';
test('receives an emulated Chicago position', async ({ context, page }) => {
await serveGeoPage(page);
await context.grantPermissions(['geolocation'], { origin: GEO_ORIGIN });
await context.setGeolocation({
latitude: 41.8781,
longitude: -87.6298,
accuracy: 15,
});
await page.goto(GEO_ORIGIN + '/');
await page.getByRole('button', { name: 'Locate me' }).click();
await expect(page.getByRole('status')).toHaveText(JSON.stringify({
latitude: 41.8781,
longitude: -87.6298,
accuracy: 15,
}));
});
This is a browser-control test. A product scenario should assert something closer to Chicago Loop store or Delivery available, because users do not care that a debug JSON field contains coordinates.
Accuracy is a non-negative number and defaults to zero if omitted. Include it only when the application consumes it or the test specifically verifies the browser coordinate object. Do not assert an arbitrary accuracy in every business test.
The standard context fixture is per test. That means another test can use a different location without racing this one. Preserve that isolation rather than sharing a manually created context across the suite.
If a failure shows slightly different numeric serialization, assert numeric fields through page evaluation or parse the displayed JSON and compare numbers. Do not loosen a product assertion without understanding formatting. The example uses controlled values that serialize consistently, while a real UI should generally present a place, distance, or availability rather than raw floating-point text.
3. Configure file-level and describe-level locations
test.use() is concise when all tests in a file or describe block share a position. It configures the browser context before the test begins.
import { test, expect } from '@playwright/test';
test.describe('Seattle service area', () => {
test.use({
geolocation: { latitude: 47.6062, longitude: -122.3321 },
permissions: ['geolocation'],
});
test('offers same-day delivery', async ({ page }) => {
await page.goto('/delivery');
await page.getByRole('button', { name: 'Use my location' }).click();
await expect(page.getByTestId('service-level'))
.toHaveText('Same-day delivery available');
});
test('uses miles for nearby locations', async ({ page }) => {
await page.goto('/stores');
await page.getByRole('button', { name: 'Use my location' }).click();
await expect(page.getByTestId('distance')).toContainText('mi');
});
});
Use this pattern when location is a stable precondition, not a behavior that changes mid-test. Put test.use() outside the running test callback. If only one scenario needs the position, a dedicated describe block prevents unrelated tests from silently inheriting it.
Configuration-level permissions: ['geolocation'] is intentionally convenient. For a test that visits third-party sites or evaluates origin boundaries, use context.grantPermissions(['geolocation'], { origin }) instead.
Avoid naming the block only geolocation tests. A domain title such as Seattle service area communicates why coordinates matter and appears clearly in reports.
4. Scope permission to the application origin
An origin-scoped grant is a strong default for multi-origin flows. It makes clear which site may read location.
import { test, expect } from '@playwright/test';
test('grants location only to the store application', async ({ context, page }) => {
const appOrigin = 'https://stores.example.test';
await context.grantPermissions(['geolocation'], { origin: appOrigin });
await context.setGeolocation({ latitude: 30.2672, longitude: -97.7431 });
await page.route(appOrigin + '/', route => route.fulfill({
contentType: 'text/html',
body: `
<button>Find store</button><output>Waiting</output>
<script>
document.querySelector('button').addEventListener('click', () => {
navigator.geolocation.getCurrentPosition(position => {
document.querySelector('output').textContent =
position.coords.latitude.toFixed(4);
});
});
</script>
`,
}));
await page.goto(appOrigin + '/');
await page.getByRole('button', { name: 'Find store' }).click();
await expect(page.getByRole('status')).toHaveText('30.2672');
});
Match the exact scheme, host, and port. A grant for https://stores.example.test does not describe https://checkout.example.test. If a product intentionally delegates to another origin, grant and test that origin explicitly.
Permission state belongs to the context. await context.clearPermissions() clears overrides for later steps in the same context. Use it for a deliberate state transition, not routine cleanup of standard Playwright Test contexts, which are already isolated and closed after the test.
Permission prompts are browser UI and not JavaScript alert dialogs. The Playwright dialog examples apply to alert, confirm, prompt, and beforeunload, not location permission prompts.
5. Test live movement with watchPosition
An application that follows a courier, trip, or device may subscribe with watchPosition(). Start at one position, wait for the first user-visible state, update the context, then assert the new state.
import { test, expect } from '@playwright/test';
test('updates a trip when the emulated position changes', async ({
context,
page,
}) => {
const origin = 'https://trip.example.test';
await context.grantPermissions(['geolocation'], { origin });
await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
await page.route(origin + '/', route => route.fulfill({
contentType: 'text/html',
body: `
<output id="city">Waiting</output>
<script>
navigator.geolocation.watchPosition(position => {
const latitude = position.coords.latitude;
document.querySelector('#city').textContent =
latitude > 40 ? 'Portland checkpoint' : 'San Francisco checkpoint';
});
</script>
`,
}));
await page.goto(origin + '/');
await expect(page.getByRole('status'))
.toHaveText('San Francisco checkpoint');
await context.setGeolocation({ latitude: 45.5152, longitude: -122.6784 });
await expect(page.getByRole('status'))
.toHaveText('Portland checkpoint');
});
The tiny latitude rule exists only to keep the demo self-contained. A real app would use a geographic service or controlled domain logic. Assert both initial and updated states so the test proves a transition rather than one final render.
Do not feed a long GPS track through one end-to-end case. Route mathematics, smoothing, and point sequences belong in fast deterministic component tests. Browser coverage should prove subscription wiring, a few representative updates, and important recovery behavior.
Movement tests also need lifecycle coverage. A component should clear its watcher when it unmounts or no longer needs tracking. Verify that navigation does not keep sending stale updates when this has user, battery, privacy, or billing impact. The browser example can assert no additional application request after leaving the tracking page, while unit tests cover cleanup calls precisely.
6. Emulate position unavailable and test recovery
context.setGeolocation(null) emulates a position that cannot be provided. Keep permission granted so this scenario stays distinct from denial.
import { test, expect } from '@playwright/test';
test('falls back to postal-code entry', async ({ context, page }) => {
const origin = 'https://weather.example.test';
await context.grantPermissions(['geolocation'], { origin });
await context.setGeolocation(null);
await page.route(origin + '/', route => route.fulfill({
contentType: 'text/html',
body: `
<button>Use current location</button>
<p role="alert"></p>
<label hidden>Postal code <input></label>
<script>
document.querySelector('button').addEventListener('click', () => {
navigator.geolocation.getCurrentPosition(
() => {},
() => {
document.querySelector('[role=alert]').textContent =
'We could not determine your location.';
document.querySelector('label').hidden = false;
}
);
});
</script>
`,
}));
await page.goto(origin + '/');
await page.getByRole('button', { name: 'Use current location' }).click();
await expect(page.getByRole('alert'))
.toHaveText('We could not determine your location.');
await expect(page.getByLabel('Postal code')).toBeVisible();
});
The fallback is the product contract. Browser error text and timing are implementation details, so the page translates them into a stable message.
Create separate cases for unavailable position, user-denied permission, invalid returned data, reverse-geocoding failure, and no service at a valid position. Their user messages may be similar, but their operational causes and telemetry should differ.
Do not use an extreme coordinate to represent unavailable position. (0, 0) is a valid geographic coordinate. Use null when the position itself should be unavailable.
7. Verify the application sends correct coordinates
UI output can look correct even when the request swaps or rounds coordinates. Capture the application's request and assert the integration contract.
import { test, expect } from '@playwright/test';
test.use({
geolocation: { latitude: 39.7392, longitude: -104.9903 },
permissions: ['geolocation'],
});
test('sends named coordinates to store search', async ({ page }) => {
await page.route('**/api/stores**', route => route.fulfill({
json: { stores: [{ name: 'Central Denver' }] },
}));
await page.goto('/stores');
const requestPromise = page.waitForRequest(request =>
request.url().includes('/api/stores') && request.method() === 'GET'
);
await page.getByRole('button', { name: 'Use my location' }).click();
const request = await requestPromise;
const url = new URL(request.url());
expect(url.searchParams.get('latitude')).toBe('39.7392');
expect(url.searchParams.get('longitude')).toBe('-104.9903');
await expect(page.getByText('Central Denver')).toBeVisible();
});
Start the request wait before the action so a fast request cannot escape observation. Assert both the request contract and the rendered result when this is an integration test.
If the application intentionally rounds coordinates for privacy, assert the documented precision. Do not copy values from the request into the expected result. Keep the fixture and expected contract independently readable.
Route mocks must reject unexpected method, path, or payload when those matter. A mock that returns success to every request can hide integration defects.
Be explicit about coordinate precision. More decimal places imply a more precise location, which may exceed the product's privacy policy or backend contract. If the client rounds before sending, assert the documented rounded fields. If it sends full browser coordinates, protect traces and logs as potentially sensitive test artifacts, even when fixtures are synthetic.
8. Create a regional project matrix
Projects are ideal when the same test should run under named region configurations. They improve filtering and reporting and configure a fresh context for each project.
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'chicago-en-us',
use: {
geolocation: { latitude: 41.8781, longitude: -87.6298 },
permissions: ['geolocation'],
locale: 'en-US',
timezoneId: 'America/Chicago',
},
},
{
name: 'paris-fr-fr',
use: {
geolocation: { latitude: 48.8566, longitude: 2.3522 },
permissions: ['geolocation'],
locale: 'fr-FR',
timezoneId: 'Europe/Paris',
},
},
],
});
| Matrix dimension | Add when | Avoid when |
|---|---|---|
| City coordinate | Region changes business behavior | Only raw coordinate wiring matters |
| Locale | Language or formatting is in scope | You only need physical location |
| Timezone | Date cutoffs depend on browser time | Backend uses a fixed server zone only |
| Browser engine | Product supports engine-specific behavior | It duplicates low-risk coverage blindly |
| Device preset | Mobile experience or consent differs | Viewport has no scenario relevance |
Coordinates, locale, and timezone are independent. Combining them creates a coherent persona, but mixed combinations also matter for travelers, VPN users, and saved preferences. Build the smallest matrix that represents requirements.
Avoid multiplying every browser, device, city, language, and account role. Use risk-based pairings and move pure formatting permutations to lower layers.
9. Model service-area boundaries without flaky maps
Boundary coverage should use business-owned test polygons or stable stub data. A point just inside and one just outside can prove UI wiring, while a unit suite covers the geometry exhaustively.
const boundaryCases = [
{
name: 'inside service zone',
position: { latitude: 32.7767, longitude: -96.7970 },
expected: 'Delivery available',
},
{
name: 'outside service zone',
position: { latitude: 33.0500, longitude: -96.7970 },
expected: 'Outside delivery area',
},
] as const;
for (const item of boundaryCases) {
test(item.name, async ({ context, page }) => {
await context.setGeolocation(item.position);
await page.goto('/delivery');
await page.getByRole('button', { name: 'Check location' }).click();
await expect(page.getByRole('status')).toHaveText(item.expected);
});
}
Each case is a separate test, so one failure does not block the other. The test environment must control the service-zone data associated with those points. If production boundaries change daily, hard-coded points can become stale and create false failures.
Do not infer distance by screenshot comparison. Assert accessible labels, eligibility state, store identity, or an API contract. Pixel-perfect map tiles are a separate visual testing concern and often depend on third-party rendering.
Document boundary ownership. When a business team changes a zone, they should update the controlled fixture and expected scenario together. Keep coordinates free of real customer addresses unless privacy review explicitly permits them.
Add a third boundary case only when it represents a distinct rule, such as exactly-on-edge behavior or a location inside a geographic hole. A large table of near-identical UI tests is harder to maintain than a geometry test suite. The browser layer should sample decisions that prove coordinate capture, request mapping, and result presentation.
10. Apply playwright geolocation emulation best practices in CI
Reliable CI examples share deterministic inputs, isolated contexts, secure origins, and user-facing assertions. Name region fixtures, include coordinate fields explicitly, and avoid mutable globals. Capture traces on first retry or failure so you can see page actions, console errors, and requests.
When a test hangs, probe the boundaries in order:
- Confirm the page is on the expected HTTPS origin.
- Confirm the context grant targets that origin.
- Confirm latitude and longitude are valid and not swapped.
- Check whether the page called geolocation after a user action.
- Inspect the coordinate request and downstream response.
- Assert the final application state.
A timeout is not proof that geolocation is slow. The page may be waiting for a permission it never received, a callback may throw, or a mocked service route may not match. The Playwright timeout troubleshooting guide helps identify which timeout owns the failure.
Use the typed Playwright fixtures guide to provide named locations or domain page objects while retaining per-test isolation. For larger browser suites, keep only critical regional scenarios at the end-to-end layer and test coordinate transformations through fast contracts and units.
11. Review playwright geolocation emulation examples before merging
Confirm every example controls four inputs explicitly: context, position, permission, and origin. Coordinates must use named latitude and longitude fields within valid ranges. Permission scope should match the scenario, and the page should use a secure origin. A new test must not depend on another test's context state.
Then review the assertion. Raw coordinate output is acceptable for a browser harness, but a product test should normally verify store identity, eligibility, regional content, or a recovery path. If a mock participates, make it validate the expected request so swapped or malformed values cannot pass.
Finally, check maintainability and privacy. Prefer named synthetic locations, separate tests for separate mechanisms, and a small risk-based project matrix. Remove fixed waits and sensitive coordinate logging. Record traces under the repository's retention policy, and ensure boundary fixtures have a business owner. These checks keep a useful cookbook from becoming a collection of environment-specific demos.
Interview Questions and Answers
Q: Show the shortest positive geolocation configuration in Playwright Test.
Use test.use({ geolocation: { latitude, longitude }, permissions: ['geolocation'] }) or the same fields under configuration use. Both settings matter: the position defines what can be returned, and permission allows the page to read it.
Q: Why use a routed HTTPS origin in a self-contained example?
Geolocation is a powerful browser feature associated with secure contexts. Routing an HTTPS test origin avoids an external server while preserving realistic origin and permission behavior. It also allows an origin-scoped grant.
Q: How can a test prove that latitude and longitude were not swapped?
Capture the application request with page.waitForRequest() and assert named query or body fields against the controlled fixture. Also use a downstream stub that validates the request rather than returning the same city for every coordinate.
Q: What is the difference between permission denial and position unavailable?
Denial means the page is not authorized to access location. Unavailable means access may be granted but a position cannot be supplied, which Playwright can emulate with context.setGeolocation(null). Test them as separate failure mechanisms.
Q: When should geolocation examples use Playwright projects?
Use projects when the same tests must run under named regional configurations or when region settings should be filterable in reports. Avoid a combinatorial matrix that multiplies cities, browsers, devices, locales, and roles without distinct risk.
Q: How do you test a moving location?
Start the page's watcher, assert its initial state, call context.setGeolocation() with the next position, and assert the updated product state. Use only a few representative points in the browser test and put full route calculations below the UI layer.
Q: How do you keep parallel regional tests isolated?
Use the standard per-test browser context, avoid shared mutable location state, and namespace backend data. Context-scoped permissions and coordinates should be configured independently for every test or project.
Common Mistakes
- Supplying
longitude, latitudeas an unlabeled array and reversing the order. - Setting a position without granting permission.
- Granting permission globally when the scenario requires one trusted origin.
- Expecting
clearPermissions()and unavailable position to model the same failure. - Using
(0, 0)as a fake unavailable location even though it is valid. - Loading a nonsecure or mismatched origin for a permission-sensitive example.
- Asserting a debug coordinate but not the store, delivery, or regional outcome.
- Returning a successful mock response for every malformed coordinate request.
- Sharing one browser context across parallel city tests.
- Combining every city, locale, timezone, device, and browser into an unmanageable matrix.
- Hard-coding production service boundaries that change outside test ownership.
- Waiting with fixed sleeps after movement instead of asserting the updated state.
Conclusion
Strong playwright geolocation emulation examples control position, permission, origin, and downstream data while asserting what the user sees. Use test.use() for a stable starting region, grantPermissions() for explicit trust, setGeolocation() for movement, and setGeolocation(null) for unavailable-position recovery.
Implement the fixed-city and unavailable-position examples first. Add request validation, project matrices, movement, and service-area boundaries only where those mechanisms represent real product risk.
Interview Questions and Answers
What is the shortest positive Playwright geolocation example?
Configure `test.use({ geolocation: { latitude, longitude }, permissions: ["geolocation"] })`, navigate, trigger the application location workflow, and assert its user-facing result. Coordinates and permission are both required.
Why use a routed HTTPS origin for an isolated example?
Geolocation is a powerful web feature associated with secure contexts. A routed HTTPS origin remains self-contained while supporting realistic origin-scoped permission behavior.
How do you prove latitude and longitude were not swapped?
Capture the application request and assert named coordinate fields against a controlled fixture. Configure the stub to reject malformed input so the same city is not returned for every request.
What is the difference between denial and unavailable position?
Denial means the page lacks authorization to read location. Unavailable means permission may exist but no position can be supplied, which Playwright models with `setGeolocation(null)`. They should be separate tests.
When should geolocation examples use projects?
Use projects for named, reportable regional environments that share tests. Avoid combinatorial projects when locale, browser, city, device, and account role do not each create a distinct risk.
How do you test movement?
Assert the initial watcher-driven state, update the context position, and assert the next user-visible state. Keep full route interpolation and smoothing coverage at lower testing layers.
How do you keep regional tests isolated in parallel?
Use Playwright Test's per-test context, avoid mutable global locations, and namespace backend data. Configure each test or project independently so permission and position cannot leak.
Frequently Asked Questions
Can a Playwright geolocation example run without a web server?
Yes. Route a synthetic HTTPS origin with `page.route()` and fulfill the HTML in the test. This preserves a realistic secure origin while avoiding an external service.
How do I test watchPosition with Playwright?
Load a page that starts a watcher, assert the initial location-driven state, call `context.setGeolocation()` with a new point, and assert the updated state. Use only representative checkpoints in the browser layer.
How can I check that the app sent coordinates correctly?
Start `page.waitForRequest()` before the action, capture the matching request, and assert named latitude and longitude fields. Make the route mock validate its input instead of returning success to every request.
Should geolocation tests use Playwright projects?
Projects are useful for named regional configurations that run the same tests. Keep the matrix risk-based so cities, locales, timezones, devices, and browsers do not multiply without distinct value.
How do I test an unavailable position?
Grant geolocation permission, call `context.setGeolocation(null)`, trigger the location workflow, and assert a stable manual-entry or retry fallback. Do not use `(0, 0)`, which is a valid coordinate.
How should I test delivery-zone boundaries?
Use business-owned points just inside and outside stable test-zone data. Put broad geometry cases in unit tests and use browser tests to prove coordinate capture, request mapping, and decision presentation.
Are test coordinates sensitive?
Precise coordinates can be sensitive, especially if copied from real users. Prefer synthetic points, control trace retention, and assert only the precision the product contract requires.
Related Guides
- Playwright device emulation: Examples and Best Practices
- Playwright drag and drop: Examples and Best Practices
- Playwright mock date and time: Examples and Best Practices
- Playwright popup and new tab handling: Examples and Best Practices
- Playwright tag and grep filters: Examples and Best Practices
- Playwright APIRequestContext: Examples and Best Practices