Playwright Examples

Using mail-catcher in Playwright E2E tests.

mail-catcher pairs well with Playwright for testing email-driven flows like account verification, password resets, and invitation links.

Setup

Create a helper to query mail-catcher from your tests:

// e2e/helpers/inbox.ts

const API_URL = process.env.API_URL!;
const TOKEN = process.env.TOKEN!;

interface Email {
  messageId: string;
  inbox: string;
  sender: string;
  subject: string;
  body: string;
  htmlBody: string;
  receivedAt: number;
}

interface EmailResponse {
  emails: Email[];
  nextCursor?: string;
  hasMore: boolean;
}

export async function waitForEmail(
  inbox: string,
  options: { timeout?: number; subject?: string } = {}
): Promise<Email> {
  const { timeout = 15, subject } = options;
  const params = new URLSearchParams({
    inbox,
    wait: "true",
    timeout: String(timeout),
  });
  if (subject) params.set("subject", subject);

  const response = await fetch(`${API_URL}/v1/emails?${params}`, {
    headers: { Authorization: `Bearer ${TOKEN}` },
  });

  const data: EmailResponse = await response.json();

  if (data.emails.length === 0) {
    throw new Error(`No email arrived in inbox "${inbox}" within ${timeout}s`);
  }

  return data.emails[0];
}

export async function clearInbox(inbox: string): Promise<void> {
  await fetch(`${API_URL}/v1/emails?inbox=${inbox}`, {
    method: "DELETE",
    headers: { Authorization: `Bearer ${TOKEN}` },
  });
}

Test: Account verification flow

// e2e/tests/signup.spec.ts
import { test, expect } from "@playwright/test";
import { waitForEmail, clearInbox } from "../helpers/inbox";

const TEST_INBOX = `signup-${Date.now()}`;
const TEST_EMAIL = `${TEST_INBOX}@receive.yourdomain.com`;

test.beforeEach(async () => {
  await clearInbox(TEST_INBOX);
});

test("user can sign up and verify email", async ({ page }) => {
  await page.goto("/signup");
  await page.fill('[name="email"]', TEST_EMAIL);
  await page.fill('[name="password"]', "SecurePass123!");
  await page.click('button[type="submit"]');

  await expect(page.locator(".success-message")).toBeVisible();

  const email = await waitForEmail(TEST_INBOX, {
    subject: "Verify",
    timeout: 15,
  });

  const match = email.htmlBody.match(/href="([^"]+)"/);
  expect(match).toBeTruthy();
  const verificationUrl = match![1];

  await page.goto(verificationUrl);
  await expect(page.locator(".verified")).toBeVisible();
});

Test: Password reset flow

// e2e/tests/password-reset.spec.ts
import { test, expect } from "@playwright/test";
import { waitForEmail, clearInbox } from "../helpers/inbox";

const TEST_INBOX = `reset-${Date.now()}`;
const TEST_EMAIL = `${TEST_INBOX}@receive.yourdomain.com`;

test("user can reset password via email", async ({ page }) => {
  await clearInbox(TEST_INBOX);

  await page.goto("/forgot-password");
  await page.fill('[name="email"]', TEST_EMAIL);
  await page.click('button[type="submit"]');

  const email = await waitForEmail(TEST_INBOX, {
    subject: "Reset",
    timeout: 15,
  });

  const resetUrl = email.htmlBody.match(/href="([^"]*reset[^"]*)"/)?.[1];
  expect(resetUrl).toBeTruthy();

  await page.goto(resetUrl!);
  await page.fill('[name="password"]', "NewSecurePass456!");
  await page.fill('[name="confirmPassword"]', "NewSecurePass456!");
  await page.click('button[type="submit"]');

  await expect(page.locator(".password-updated")).toBeVisible();
});

Test: Invitation email

// e2e/tests/invite.spec.ts
import { test, expect } from "@playwright/test";
import { waitForEmail, clearInbox } from "../helpers/inbox";

const INVITEE_INBOX = `invite-${Date.now()}`;
const INVITEE_EMAIL = `${INVITEE_INBOX}@receive.yourdomain.com`;

test("admin can invite a user via email", async ({ page }) => {
  await clearInbox(INVITEE_INBOX);

  await page.goto("/admin/users");
  await page.click("text=Invite User");
  await page.fill('[name="email"]', INVITEE_EMAIL);
  await page.click("text=Send Invite");

  const email = await waitForEmail(INVITEE_INBOX, {
    subject: "invited",
    timeout: 15,
  });

  expect(email.sender).toContain("noreply@");

  const inviteUrl = email.htmlBody.match(/href="([^"]*invite[^"]*)"/)?.[1];
  expect(inviteUrl).toBeTruthy();
});

Tips

  • Use unique inbox names: include a timestamp or test ID (e.g., signup-1710000000) to avoid collisions between parallel test runs.
  • Clear before each test: call clearInbox() in beforeEach to start with a clean state.
  • Set reasonable timeouts: 15 seconds covers most email delivery latencies. Increase for slower providers.
  • Check the subject: use the subject filter to avoid picking up unrelated emails.

Search Documentation

Search through the docs