.agents/skills/playwright-best-practices/browser-apis/browser-apis.md
test('shows nearby stores', async ({context}) => {
// Grant permission and set location
await context.grantPermissions(['geolocation'])
await context.setGeolocation({latitude: 37.7749, longitude: -122.4194}) // San Francisco
const page = await context.newPage()
await page.goto('/store-finder')
await page.getByRole('button', {name: 'Find Nearby'}).click()
await expect(page.getByText('San Francisco')).toBeVisible()
})
// fixtures/geolocation.fixture.ts
import {test as base} from '@playwright/test'
type Coordinates = {latitude: number; longitude: number; accuracy?: number}
type GeoFixtures = {
setLocation: (coords: Coordinates) => Promise<void>
}
export const test = base.extend<GeoFixtures>({
setLocation: async ({context}, use) => {
await context.grantPermissions(['geolocation'])
await use(async (coords) => {
await context.setGeolocation({
latitude: coords.latitude,
longitude: coords.longitude,
accuracy: coords.accuracy ?? 100,
})
})
},
})
// Usage
test('delivery zone check', async ({page, setLocation}) => {
await setLocation({latitude: 40.7128, longitude: -74.006}) // NYC
await page.goto('/delivery')
await expect(page.getByText('Delivery available')).toBeVisible()
})
test('tracks location updates', async ({context}) => {
await context.grantPermissions(['geolocation'])
const page = await context.newPage()
await page.goto('/tracking')
// Initial location
await context.setGeolocation({latitude: 37.7749, longitude: -122.4194})
await page.getByRole('button', {name: 'Start Tracking'}).click()
await expect(page.getByTestId('location')).toContainText('37.7749')
// Move to new location
await context.setGeolocation({latitude: 37.8044, longitude: -122.2712})
// Trigger location update
await page.evaluate(() => {
navigator.geolocation.getCurrentPosition(() => {})
})
await expect(page.getByTestId('location')).toContainText('37.8044')
})
test('handles location denied', async ({browser}) => {
// Create context without geolocation permission
const context = await browser.newContext({
permissions: [], // No permissions
})
const page = await context.newPage()
await page.goto('/store-finder')
await page.getByRole('button', {name: 'Find Nearby'}).click()
await expect(page.getByText('Location access denied')).toBeVisible()
await expect(page.getByLabel('Enter ZIP code')).toBeVisible()
await context.close()
})
test('notifications with permission', async ({context}) => {
await context.grantPermissions(['notifications'])
const page = await context.newPage()
await page.goto('/alerts')
// Notification API should work
const permission = await page.evaluate(() => Notification.permission)
expect(permission).toBe('granted')
})
test('handles notification permission denied', async ({browser}) => {
const context = await browser.newContext({
permissions: [], // Deny all
})
const page = await context.newPage()
await page.goto('/notifications')
await page.getByRole('button', {name: 'Enable Notifications'}).click()
await expect(page.getByText('Please enable notifications')).toBeVisible()
await context.close()
})
test('video call with permissions', async ({context}) => {
await context.grantPermissions(['camera', 'microphone', 'notifications'])
const page = await context.newPage()
await page.goto('/video-call')
// All permissions should be granted
const permissions = await page.evaluate(async () => ({
camera: await navigator.permissions.query({
name: 'camera' as PermissionName,
}),
microphone: await navigator.permissions.query({
name: 'microphone' as PermissionName,
}),
}))
expect(permissions.camera.state).toBe('granted')
expect(permissions.microphone.state).toBe('granted')
})
test('copy button works', async ({page, context}) => {
// Grant clipboard permissions
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
await page.goto('/share')
await page.getByRole('button', {name: 'Copy Link'}).click()
// Read clipboard content
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText())
expect(clipboardContent).toContain('https://example.com/share/')
})
test('paste from clipboard', async ({page, context}) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
await page.goto('/editor')
// Write to clipboard
await page.evaluate(() => navigator.clipboard.writeText('Pasted content'))
// Trigger paste
await page.getByLabel('Content').focus()
await page.keyboard.press('Control+V')
await expect(page.getByLabel('Content')).toHaveValue('Pasted content')
})
// fixtures/clipboard.fixture.ts
import {test as base} from '@playwright/test'
type ClipboardFixtures = {
clipboard: {
write: (text: string) => Promise<void>
read: () => Promise<string>
}
}
export const test = base.extend<ClipboardFixtures>({
clipboard: async ({page, context}, use) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
await use({
write: async (text) => {
await page.evaluate((t) => navigator.clipboard.writeText(t), text)
},
read: async () => {
return page.evaluate(() => navigator.clipboard.readText())
},
})
},
})
test('shows browser notification', async ({page}) => {
const notifications: any[] = []
// Mock Notification constructor
await page.addInitScript(() => {
;(window as any).__notifications = []
;(window as any).Notification = class {
constructor(title: string, options?: NotificationOptions) {
;(window as any).__notifications.push({title, ...options})
}
static permission = 'granted'
static requestPermission = async () => 'granted'
}
})
await page.goto('/alerts')
await page.getByRole('button', {name: 'Notify Me'}).click()
// Check notification was created
const created = await page.evaluate(() => (window as any).__notifications)
expect(created).toHaveLength(1)
expect(created[0].title).toBe('New Alert')
})
test('notification click handler', async ({page}) => {
await page.addInitScript(() => {
;(window as any).Notification = class {
onclick: (() => void) | null = null
constructor(title: string) {
// Simulate click after creation
setTimeout(() => this.onclick?.(), 100)
}
static permission = 'granted'
static requestPermission = async () => 'granted'
}
})
await page.goto('/messages')
await page.evaluate(() => {
new Notification('New Message')
})
// Should navigate to messages when notification clicked
await expect(page).toHaveURL(/\/messages/)
})
test('video preview works', async ({page, context}) => {
await context.grantPermissions(['camera'])
// Mock getUserMedia
await page.addInitScript(() => {
navigator.mediaDevices.getUserMedia = async () => {
const canvas = document.createElement('canvas')
canvas.width = 640
canvas.height = 480
return canvas.captureStream()
}
})
await page.goto('/video-settings')
await page.getByRole('button', {name: 'Start Camera'}).click()
await expect(page.getByTestId('video-preview')).toBeVisible()
})
test('switch camera', async ({page}) => {
await page.addInitScript(() => {
navigator.mediaDevices.enumerateDevices = async () =>
[
{
deviceId: 'cam1',
kind: 'videoinput',
label: 'Front Camera',
groupId: '1',
},
{
deviceId: 'cam2',
kind: 'videoinput',
label: 'Back Camera',
groupId: '2',
},
] as MediaDeviceInfo[]
navigator.mediaDevices.getUserMedia = async () => {
const canvas = document.createElement('canvas')
return canvas.captureStream()
}
})
await page.goto('/camera')
// Should show camera options
await expect(page.getByRole('combobox', {name: 'Camera'})).toBeVisible()
await expect(page.getByText('Front Camera')).toBeVisible()
await expect(page.getByText('Back Camera')).toBeVisible()
})
test('handles camera access error', async ({page}) => {
await page.addInitScript(() => {
navigator.mediaDevices.getUserMedia = async () => {
throw new DOMException('Permission denied', 'NotAllowedError')
}
})
await page.goto('/video-call')
await page.getByRole('button', {name: 'Join Call'}).click()
await expect(page.getByText('Camera access denied')).toBeVisible()
await expect(page.getByRole('button', {name: 'Join Audio Only'})).toBeVisible()
})
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Not granting permissions | Tests fail with permission errors | Use context.grantPermissions() |
| Testing real geolocation | Flaky, environment-dependent | Mock with setGeolocation() |
| Not testing permission denial | Misses error handling | Test both granted and denied states |
| Using real camera/mic | CI has no devices | Mock getUserMedia |