Skip to main content

Playwright

ZeroDrop integrates with Playwright for end-to-end email testing in CI pipelines. No Docker, no SMTP server, no mocking.

Installation

npm install zerodrop-client
npm install --save-dev @playwright/test

Basic setup

import { test, expect } from '@playwright/test';
import { ZeroDrop } from 'zerodrop-client';

const mail = new ZeroDrop();

test('email verification flow', async ({ page }) => {
  // Generate a unique inbox for this test
  const inbox = mail.generateInbox();

  // Use the inbox in your signup form
  await page.goto('/signup');
  await page.fill('[data-testid="email"]', inbox);
  await page.click('[data-testid="submit"]');

  // Wait for the verification email
  const email = await mail.waitForLatest(inbox, { timeout: 30000 });

  // OTP and magic link are auto-extracted
  console.log(email.otp);       // "123456"
  console.log(email.magicLink); // "https://yourapp.com/verify?token=..."

  // Click the verification link
  await page.goto(email.magicLink!);
  await expect(page).toHaveURL('/dashboard');
});

OTP verification

ZeroDrop extracts OTPs automatically at the edge — no regex needed in your tests.
test('OTP login flow', async ({ page }) => {
  const inbox = mail.generateInbox();

  await page.goto('/login');
  await page.fill('[data-testid="email"]', inbox);
  await page.click('[data-testid="submit"]');

  // Wait for OTP email
  const email = await mail.waitForLatest(inbox, { timeout: 30000 });
  expect(email.otp).not.toBeNull();

  // Enter OTP directly — no body parsing needed
  await page.fill('[data-testid="otp"]', email.otp!);
  await page.click('[data-testid="verify"]');

  await expect(page).toHaveURL('/dashboard');
});

Password reset flow

test('password reset flow', async ({ page }) => {
  const inbox = mail.generateInbox();

  // Request password reset
  await page.goto('/forgot-password');
  await page.fill('[data-testid="email"]', inbox);
  await page.click('[data-testid="submit"]');

  // Wait for reset email
  const email = await mail.waitForLatest(inbox, { timeout: 30000 });

  // Navigate to reset link
  await page.goto(email.magicLink!);
  await page.fill('[data-testid="password"]', 'NewPassword123!');
  await page.click('[data-testid="submit"]');

  await expect(page).toHaveURL('/login');
});

Parallel tests

Every inbox is isolated — parallel tests never collide.
test.describe.parallel('Email flows', () => {
  test('user A can verify email', async ({ page }) => {
    const inbox = mail.generateInbox(); // unique per test
    // ...
  });

  test('user B can verify email', async ({ page }) => {
    const inbox = mail.generateInbox(); // different inbox
    // ...
  });
});

Multiple emails in one test

test('signup then password reset', async ({ page }) => {
  const inbox = mail.generateInbox();
  let lastEmailId: string;

  // First email — verification
  const verifyEmail = await mail.waitForLatest(inbox, { timeout: 30000 });
  lastEmailId = verifyEmail.id;
  await page.goto(verifyEmail.magicLink!);

  // Trigger password reset
  await page.goto('/forgot-password');
  await page.fill('[data-testid="email"]', inbox);
  await page.click('[data-testid="submit"]');

  // Poll until a NEW email arrives
  let resetEmail = null;
  const deadline = Date.now() + 30000;
  while (Date.now() < deadline) {
    const latest = await mail.fetchLatest(inbox);
    if (latest && latest.id !== lastEmailId) {
      resetEmail = latest;
      break;
    }
    await new Promise(r => setTimeout(r, 2000));
  }

  expect(resetEmail).not.toBeNull();
  await page.goto(resetEmail!.magicLink!);
});

Timeout options

// Default: 10 seconds
const email = await mail.waitForLatest(inbox);

// Custom timeout
const email = await mail.waitForLatest(inbox, { timeout: 30000 });

// Force polling mode (disable SSE)
const email = await mail.waitForLatest(inbox, { sse: false });

playwright.config.ts

import { defineConfig } from '@playwright/test';

export default defineConfig({
  timeout: 60000, // allow time for email delivery
  use: {
    baseURL: 'http://localhost:3000',
  },
});