Back to Sanity

Browser APIs: Geolocation, Permissions & More

.agents/skills/playwright-best-practices/browser-apis/browser-apis.md

5.31.110.3 KB
Original Source

Browser APIs: Geolocation, Permissions & More

Table of Contents

  1. Geolocation
  2. Permissions
  3. Clipboard
  4. Notifications
  5. Camera & Microphone

Geolocation

Mock Location

typescript
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()
})

Geolocation Fixture

typescript
// 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 Location Changes

typescript
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 Geolocation Denial

typescript
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()
})

Permissions

Grant Permissions

typescript
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 Permission Denied

typescript
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()
})

Multiple Permissions

typescript
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')
})

Clipboard

Test Copy to Clipboard

typescript
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

typescript
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')
})

Clipboard Fixture

typescript
// 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())
      },
    })
  },
})

Notifications

Mock Notification API

typescript
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

typescript
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/)
})

Camera & Microphone

Mock Media Devices

typescript
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 Media Device Selection

typescript
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 Media Errors

typescript
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-Patterns to Avoid

Anti-PatternProblemSolution
Not granting permissionsTests fail with permission errorsUse context.grantPermissions()
Testing real geolocationFlaky, environment-dependentMock with setGeolocation()
Not testing permission denialMisses error handlingTest both granted and denied states
Using real camera/micCI has no devicesMock getUserMedia