SurfSense/.cursor/skills/playwright-testing/browser-apis/browser-apis.md
2026-05-10 04:19:55 +05:30

10 KiB

Browser APIs: Geolocation, Permissions & More

Table of Contents

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

Geolocation

Mock Location

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

// 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

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

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

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

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

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

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

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

// 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

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

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

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

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

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-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