Skip to content

Playwright recipe

The goal: a real end-to-end test that signs a user up, waits for the verification email, clicks the link, and asserts they reach the dashboard.

1. Add a tiny helper

Put this in tests/helpers/mailfade.ts:

import { APIRequestContext, expect } from "@playwright/test";

const API = process.env.MAILFADE_API_URL ?? "https://api.mailfade.dev";
const KEY = process.env.MAILFADE_KEY;

const headers = KEY ? { Authorization: `Bearer ${KEY}` } : undefined;

export function freshInbox(prefix = "pw") {
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@mailfade.dev`;
}

export async function waitForEmail(
  request: APIRequestContext,
  inbox: string,
  match: { subject?: RegExp; from?: RegExp } = {},
  timeoutMs = 30_000,
) {
  let firstEmailId: string | undefined;
  await expect.poll(async () => {
    const r = await request.get(`${API}/inbox/${encodeURIComponent(inbox)}`, { headers });
    const body = await r.json();
    const hit = (body.emails ?? []).find((e: any) => {
      if (match.subject && !match.subject.test(e.subject ?? "")) return false;
      if (match.from && !match.from.test(e.sender ?? "")) return false;
      return true;
    });
    if (hit) { firstEmailId = hit.id; return true; }
    return false;
  }, { timeout: timeoutMs, message: `no matching email arrived at ${inbox}` }).toBe(true);

  const r = await request.get(`${API}/message/${firstEmailId}`, { headers });
  return r.json();
}

2. Use it in a test

import { test, expect, request as pwRequest } from "@playwright/test";
import { freshInbox, waitForEmail } from "./helpers/mailfade";

test("user signs up and verifies their email", async ({ page }) => {
  const inbox = freshInbox("signup");
  const api = await pwRequest.newContext();

  await page.goto("/signup");
  await page.getByLabel("Email").fill(inbox);
  await page.getByLabel("Password").fill("hunter2hunter2");
  await page.getByRole("button", { name: "Create account" }).click();

  const email = await waitForEmail(api, inbox, {
    subject: /confirm your account/i,
    from:    /@acme\.com$/,
  });

  const link = (email.text ?? "").match(/https?:\/\/\S+/)?.[0];
  expect(link, "verification link missing from email").toBeTruthy();

  await page.goto(link!);
  await expect(page.getByText("Welcome")).toBeVisible();
});

3. CI

Add MAILFADE_KEY to your GitHub Actions / GitLab CI secrets, then:

env:
  MAILFADE_KEY: ${{ secrets.MAILFADE_KEY }}

On the free tier, the test runs without a key — but you’ll hit the 100/day per-IP cap fast if your CI is shared (e.g. GitHub-hosted runners). A Dev key solves that.

Common patterns

Password-reset flow. Same shape — freshInbox("reset"), trigger the reset, poll for the email, extract the token from the URL, hit your reset endpoint.

Multiple emails in one test. Pass since: Date.now() to ignore older messages: include it in the helper by adding a ?since= query string.

Asserting on HTML body. With a paid key, email.html is present — feed it through cheerio or playwright’s setContent for proper DOM assertions on rendered templates.