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.