Testing geolocation and permissions: a step-by-step guide
Modern web applications increasingly rely on sensitive device capabilities—geolocation, camera access, microphone permissions, and notification settings. When testing these features with Playwright, you need to simulate the browser's permission model accurately, including both granted and denied states. This guide walks you through the practical techniques we've used to validate permission flows across e-commerce checkout flows, ride-hailing platforms, and location-based SaaS applications.
Why permission testing matters in production
Permissions are a critical trust boundary between your application and the user's device. A poorly handled denied permission can break core functionality or confuse users with incorrect fallback UI. We've seen production incidents where iOS users denied geolocation once, and the app silently failed to show nearby stores—with no error message. Testing these scenarios at automation time catches these edge cases before they reach users.
Playwright gives you fine-grained control over the permission context through the grantPermissions() method and context-level configuration. Unlike manual testing, automated permission tests run consistently and cover both happy paths and rejection flows.
Grant permissions — context.grantPermissions(['geolocation'])
The simplest permission scenario is granting access. In Playwright, you grant permissions when you create a browser context, and those permissions persist for the lifetime of that context.
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const context = await browser.newContext({
permissions: ['geolocation']
});
const page = await context.newPage();
await page.goto('https://example.com/location-finder');
// At this point, if your app calls navigator.geolocation.getCurrentPosition(),
// Playwright will allow it without a browser prompt
This approach works well for applications that request geolocation on page load or during an interaction. The permission is granted immediately; no user prompt appears. In a real browser, the user would see a system dialog asking to allow or deny access. In your test, you're simulating the "allow" button being clicked.
A common real-world scenario: you're testing a store locator feature. Your e-commerce site needs the user's location to show nearby fulfillment centers.
import { test, expect } from '@playwright/test';
test('show nearby stores when geolocation is granted', async ({ browser }) => {
const context = await browser.newContext({
permissions: ['geolocation']
});
const page = await context.newPage();
await page.goto('https://example.com/stores');
// User clicks "Find stores near me"
await page.click('[data-testid="find-nearby-button"]');
// With geolocation granted, the store list appears
const storeList = page.locator('[data-testid="store-list"]');
await expect(storeList).toBeVisible();
const storeCount = await storeList.locator('li').count();
expect(storeCount).toBeGreaterThan(0);
await context.close();
});
Key insight: granting permissions in the context constructor is the preferred approach because it's immutable and predictable. The permission state doesn't change partway through your test.
Set location — geolocation option in context
Granting geolocation permission alone isn't enough; you also need to set the actual latitude, longitude, and accuracy. Playwright's context accepts a geolocation object that simulates the device's physical location.
const context = await browser.newContext({
permissions: ['geolocation'],
geolocation: { latitude: 40.7128, longitude: -74.0060, accuracy: 100 }
});
The accuracy parameter (in meters) models GPS precision. A value of 100 meters is typical for urban settings; outdoor/rural GPS can vary widely. This matters because location-based algorithms may adjust behavior based on accuracy—for example, showing restaurants only if accuracy is better than 50 meters.
Let's walk through a ride-hailing scenario where you test that drivers appear when the user is in a service area:
test('show available drivers when user is in service area', async ({ browser }) => {
// San Francisco (in service area)
const context = await browser.newContext({
permissions: ['geolocation'],
geolocation: { latitude: 37.7749, longitude: -122.4194, accuracy: 50 }
});
const page = await context.newPage();
await page.goto('https://ridehail-app.com/request');
// Wait for driver list to load
const driverCard = page.locator('[data-testid="driver-card"]').first();
await expect(driverCard).toBeVisible({ timeout: 5000 });
await context.close();
});
test('show service unavailable when user is outside service area', async ({ browser }) => {
// Remote location (outside service area)
const context = await browser.newContext({
permissions: ['geolocation'],
geolocation: { latitude: 39.5, longitude: -119.8, accuracy: 100 }
});
const page = await context.newPage();
await page.goto('https://ridehail-app.com/request');
const unavailableMsg = page.locator('[data-testid="service-unavailable"]');
await expect(unavailableMsg).toBeVisible();
await context.close();
});
Notice we create separate contexts for each location test. This keeps tests isolated and prevents cross-test pollution. Each context is a clean slate with its own geolocation state.
You can also call setGeolocation() on an existing context to change location mid-test, though this is less common:
await context.setGeolocation({ latitude: 51.5074, longitude: -0.1278, accuracy: 50 });
await page.reload(); // Some apps need a reload to pick up the new location
Denied permissions — Test fallback UI
Permission denial is where many applications fail in production. Users tap "Don't Allow" out of habit, and your app crashes or shows a blank screen. Testing denial flows catches these bugs early.
To deny a permission, simply create a context without granting it:
test('show fallback UI when geolocation is denied', async ({ browser }) => {
// Create a context without geolocation permission
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com/store-locator');
// User clicks "Find stores near me"
await page.click('[data-testid="find-nearby-button"]');
// Since geolocation is denied, the app should show a fallback
const fallbackForm = page.locator('[data-testid="manual-location-form"]');
await expect(fallbackForm).toBeVisible({ timeout: 2000 });
// User enters zip code manually
await page.fill('[data-testid="zip-input"]', '10001');
await page.click('[data-testid="search-button"]');
// Stores should still appear
const storeList = page.locator('[data-testid="store-list"]');
await expect(storeList).toBeVisible();
await context.close();
});
A production e-commerce flow we tested: when geolocation fails (either denied or unavailable), the site should prompt for a zip code. This fallback must work seamlessly. We've caught bugs where the fallback form had missing labels, incorrect styling, or wasn't accessible via keyboard. Automation makes it easy to test both the happy path (permission granted) and the fallback (permission denied) in the same test suite.
You can be even more granular by granting some permissions while denying others in the same context:
const context = await browser.newContext({
permissions: ['notifications'], // Allowed
// geolocation not listed, so it's implicitly denied
});
Notifications — Grant or deny notification permission
Notification permission follows the same pattern as geolocation. Grant it in the context, or omit it to deny:
test('show notification opt-in when permission is not granted', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com/home');
// App checks notification permission and shows banner
const notificationBanner = page.locator('[data-testid="notification-banner"]');
await expect(notificationBanner).toBeVisible();
await context.close();
});
test('send notification when permission is granted', async ({ browser }) => {
const context = await browser.newContext({
permissions: ['notifications']
});
const page = await context.newPage();
// Listen for console messages (some apps log when notifications are sent)
let notificationFired = false;
page.on('console', msg => {
if (msg.text().includes('notification sent')) {
notificationFired = true;
}
});
await page.goto('https://example.com/home');
await page.click('[data-testid="enable-notifications"]');
// Wait for the notification to be scheduled
await page.waitForTimeout(500);
expect(notificationFired).toBe(true);
await context.close();
});
Testing notifications is trickier than geolocation because browsers don't expose a direct API to intercept actual notification display. Your best approach is to test the permission state and the code path that sends notifications, rather than the browser's notification UI itself.
In a SaaS app we tested, users could opt into desktop notifications for important alerts. We confirmed that when permission was granted, the app called the Notification API with the correct title and body. We also verified that when permission was denied, the app showed an in-app banner instead.
Common mistakes
- Forgetting to set both permission and geolocation: Granting 'geolocation' permission without setting the
geolocationobject means the permission is allowed, butnavigator.geolocation.getCurrentPosition()may hang or return an empty result. Always pair them. - Creating a new context per page instead of per test: If you create a context, navigate, then create another context on the same page, the old context is orphaned and leaks resources. Use one context per test, or reuse the same context throughout the test.
- Not testing permission denial: Teams often test the happy path (permission granted) and assume denial flows work. We've caught countless bugs where the app crashes when geolocation is denied. Always test both states.
- Using hardcoded real locations in tests: Don't hardcode your office or home coordinates. Use stable, well-known locations (e.g., city centers) so tests remain reproducible and don't leak personal information in logs.
- Ignoring the accuracy parameter: If your app has distance-based logic (e.g., "only show results within 1 km"), adjust accuracy to test edge cases. A 100-meter accuracy near the boundary can cause flaky tests.
Key takeaways
- Grant permissions via the context constructor using the
permissionsarray. This is immutable and predictable. - Set the actual location (latitude, longitude, accuracy) using the
geolocationcontext option. Permissions and location are separate concerns. - Test permission denial by creating a context without that permission. Verify your app's fallback UI works and doesn't crash.
- For notifications and other permission types, use the same pattern: list in
permissionsto grant, omit to deny. - Keep one context per test to avoid resource leaks and ensure isolation. Use stable, well-known coordinates rather than personal locations.
- Test both happy path (permission granted) and fallback path (permission denied) for any feature that depends on user permissions.






Comments
Loading comments…