Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
browser-apis/browser-apis.md
1# Browser APIs: Geolocation, Permissions & More23## Table of Contents451. [Geolocation](#geolocation)62. [Permissions](#permissions)73. [Clipboard](#clipboard)84. [Notifications](#notifications)95. [Camera & Microphone](#camera--microphone)1011## Geolocation1213### Mock Location1415```typescript16test("shows nearby stores", async ({ context }) => {17// Grant permission and set location18await context.grantPermissions(["geolocation"]);19await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); // San Francisco2021const page = await context.newPage();22await page.goto("/store-finder");23await page.getByRole("button", { name: "Find Nearby" }).click();2425await expect(page.getByText("San Francisco")).toBeVisible();26});27```2829### Geolocation Fixture3031```typescript32// fixtures/geolocation.fixture.ts33import { test as base } from "@playwright/test";3435type Coordinates = { latitude: number; longitude: number; accuracy?: number };3637type GeoFixtures = {38setLocation: (coords: Coordinates) => Promise<void>;39};4041export const test = base.extend<GeoFixtures>({42setLocation: async ({ context }, use) => {43await context.grantPermissions(["geolocation"]);4445await use(async (coords) => {46await context.setGeolocation({47latitude: coords.latitude,48longitude: coords.longitude,49accuracy: coords.accuracy ?? 100,50});51});52},53});5455// Usage56test("delivery zone check", async ({ page, setLocation }) => {57await setLocation({ latitude: 40.7128, longitude: -74.006 }); // NYC5859await page.goto("/delivery");6061await expect(page.getByText("Delivery available")).toBeVisible();62});63```6465### Test Location Changes6667```typescript68test("tracks location updates", async ({ context }) => {69await context.grantPermissions(["geolocation"]);7071const page = await context.newPage();72await page.goto("/tracking");7374// Initial location75await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 });76await page.getByRole("button", { name: "Start Tracking" }).click();7778await expect(page.getByTestId("location")).toContainText("37.7749");7980// Move to new location81await context.setGeolocation({ latitude: 37.8044, longitude: -122.2712 });8283// Trigger location update84await page.evaluate(() => {85navigator.geolocation.getCurrentPosition(() => {});86});8788await expect(page.getByTestId("location")).toContainText("37.8044");89});90```9192### Test Geolocation Denial9394```typescript95test("handles location denied", async ({ browser }) => {96// Create context without geolocation permission97const context = await browser.newContext({98permissions: [], // No permissions99});100101const page = await context.newPage();102await page.goto("/store-finder");103await page.getByRole("button", { name: "Find Nearby" }).click();104105await expect(page.getByText("Location access denied")).toBeVisible();106await expect(page.getByLabel("Enter ZIP code")).toBeVisible();107108await context.close();109});110```111112## Permissions113114### Grant Permissions115116```typescript117test("notifications with permission", async ({ context }) => {118await context.grantPermissions(["notifications"]);119120const page = await context.newPage();121await page.goto("/alerts");122123// Notification API should work124const permission = await page.evaluate(() => Notification.permission);125expect(permission).toBe("granted");126});127```128129### Test Permission Denied130131```typescript132test("handles notification permission denied", async ({ browser }) => {133const context = await browser.newContext({134permissions: [], // Deny all135});136137const page = await context.newPage();138await page.goto("/notifications");139140await page.getByRole("button", { name: "Enable Notifications" }).click();141142await expect(page.getByText("Please enable notifications")).toBeVisible();143144await context.close();145});146```147148### Multiple Permissions149150```typescript151test("video call with permissions", async ({ context }) => {152await context.grantPermissions(["camera", "microphone", "notifications"]);153154const page = await context.newPage();155await page.goto("/video-call");156157// All permissions should be granted158const permissions = await page.evaluate(async () => ({159camera: await navigator.permissions.query({160name: "camera" as PermissionName,161}),162microphone: await navigator.permissions.query({163name: "microphone" as PermissionName,164}),165}));166167expect(permissions.camera.state).toBe("granted");168expect(permissions.microphone.state).toBe("granted");169});170```171172## Clipboard173174### Test Copy to Clipboard175176```typescript177test("copy button works", async ({ page, context }) => {178// Grant clipboard permissions179await context.grantPermissions(["clipboard-read", "clipboard-write"]);180181await page.goto("/share");182183await page.getByRole("button", { name: "Copy Link" }).click();184185// Read clipboard content186const clipboardContent = await page.evaluate(() =>187navigator.clipboard.readText(),188);189190expect(clipboardContent).toContain("https://example.com/share/");191});192```193194### Test Paste from Clipboard195196```typescript197test("paste from clipboard", async ({ page, context }) => {198await context.grantPermissions(["clipboard-read", "clipboard-write"]);199200await page.goto("/editor");201202// Write to clipboard203await page.evaluate(() => navigator.clipboard.writeText("Pasted content"));204205// Trigger paste206await page.getByLabel("Content").focus();207await page.keyboard.press("Control+V");208209await expect(page.getByLabel("Content")).toHaveValue("Pasted content");210});211```212213### Clipboard Fixture214215```typescript216// fixtures/clipboard.fixture.ts217import { test as base } from "@playwright/test";218219type ClipboardFixtures = {220clipboard: {221write: (text: string) => Promise<void>;222read: () => Promise<string>;223};224};225226export const test = base.extend<ClipboardFixtures>({227clipboard: async ({ page, context }, use) => {228await context.grantPermissions(["clipboard-read", "clipboard-write"]);229230await use({231write: async (text) => {232await page.evaluate((t) => navigator.clipboard.writeText(t), text);233},234read: async () => {235return page.evaluate(() => navigator.clipboard.readText());236},237});238},239});240```241242## Notifications243244### Mock Notification API245246```typescript247test("shows browser notification", async ({ page }) => {248const notifications: any[] = [];249250// Mock Notification constructor251await page.addInitScript(() => {252(window as any).__notifications = [];253(window as any).Notification = class {254constructor(title: string, options?: NotificationOptions) {255(window as any).__notifications.push({ title, ...options });256}257static permission = "granted";258static requestPermission = async () => "granted";259};260});261262await page.goto("/alerts");263await page.getByRole("button", { name: "Notify Me" }).click();264265// Check notification was created266const created = await page.evaluate(() => (window as any).__notifications);267expect(created).toHaveLength(1);268expect(created[0].title).toBe("New Alert");269});270```271272### Test Notification Click273274```typescript275test("notification click handler", async ({ page }) => {276await page.addInitScript(() => {277(window as any).Notification = class {278onclick: (() => void) | null = null;279constructor(title: string) {280// Simulate click after creation281setTimeout(() => this.onclick?.(), 100);282}283static permission = "granted";284static requestPermission = async () => "granted";285};286});287288await page.goto("/messages");289await page.evaluate(() => {290new Notification("New Message");291});292293// Should navigate to messages when notification clicked294await expect(page).toHaveURL(/\/messages/);295});296```297298## Camera & Microphone299300### Mock Media Devices301302```typescript303test("video preview works", async ({ page, context }) => {304await context.grantPermissions(["camera"]);305306// Mock getUserMedia307await page.addInitScript(() => {308navigator.mediaDevices.getUserMedia = async () => {309const canvas = document.createElement("canvas");310canvas.width = 640;311canvas.height = 480;312return canvas.captureStream();313};314});315316await page.goto("/video-settings");317await page.getByRole("button", { name: "Start Camera" }).click();318319await expect(page.getByTestId("video-preview")).toBeVisible();320});321```322323### Test Media Device Selection324325```typescript326test("switch camera", async ({ page }) => {327await page.addInitScript(() => {328navigator.mediaDevices.enumerateDevices = async () =>329[330{331deviceId: "cam1",332kind: "videoinput",333label: "Front Camera",334groupId: "1",335},336{337deviceId: "cam2",338kind: "videoinput",339label: "Back Camera",340groupId: "2",341},342] as MediaDeviceInfo[];343344navigator.mediaDevices.getUserMedia = async () => {345const canvas = document.createElement("canvas");346return canvas.captureStream();347};348});349350await page.goto("/camera");351352// Should show camera options353await expect(page.getByRole("combobox", { name: "Camera" })).toBeVisible();354await expect(page.getByText("Front Camera")).toBeVisible();355await expect(page.getByText("Back Camera")).toBeVisible();356});357```358359### Test Media Errors360361```typescript362test("handles camera access error", async ({ page }) => {363await page.addInitScript(() => {364navigator.mediaDevices.getUserMedia = async () => {365throw new DOMException("Permission denied", "NotAllowedError");366};367});368369await page.goto("/video-call");370await page.getByRole("button", { name: "Join Call" }).click();371372await expect(page.getByText("Camera access denied")).toBeVisible();373await expect(374page.getByRole("button", { name: "Join Audio Only" }),375).toBeVisible();376});377```378379## Anti-Patterns to Avoid380381| Anti-Pattern | Problem | Solution |382| ----------------------------- | --------------------------------- | ----------------------------------- |383| Not granting permissions | Tests fail with permission errors | Use `context.grantPermissions()` |384| Testing real geolocation | Flaky, environment-dependent | Mock with `setGeolocation()` |385| Not testing permission denial | Misses error handling | Test both granted and denied states |386| Using real camera/mic | CI has no devices | Mock `getUserMedia` |387388## Related References389390- **Fixtures**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for context fixtures391- **Mobile**: See [mobile-testing.md](../advanced/mobile-testing.md) for device emulation392