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()inbeforeEachto start with a clean state. - Set reasonable timeouts: 15 seconds covers most email delivery latencies. Increase for slower providers.
- Check the subject: use the
subjectfilter to avoid picking up unrelated emails.