How to Test Email Sending with Playwright
The basic recipe — sign up, poll an inbox, extract a code — is one page long. This is the version that survives contact with a real suite: fixtures, parallel workers, CI, retries and debugging.
Start from the fixture, not the spec
A fixture gives every test a fresh inbox without repeating yourself. It wraps the polling helper so specs read as intent:
// fixtures.mjs
import { test as base } from "@playwright/test";
import { waitForMessage } from "./wait-for-email.mjs";
export const test = base.extend({
inbox: async ({}, use) => {
const address = "pw-" + Math.random().toString(36).slice(2, 8) + "@tempx.uk";
await use({
address,
waitFor: (opts) => waitForMessage(address, opts),
});
},
});
export { expect } from "@playwright/test";
A spec becomes:
import { test, expect } from "./fixtures.mjs";
test("welcome mail arrives after signup", async ({ page, inbox }) => {
await page.goto("/signup");
await page.getByLabel("Email").fill(inbox.address);
await page.getByRole("button", { name: "Create account" }).click();
const msg = await inbox.waitFor({
timeoutMs: 45000,
match: (m) => /welcome/i.test(m.subject),
});
expect(msg.from).toContain("noreply@example.com");
});
Parallel workers are free — if addresses are unique
Playwright runs workers in parallel, and each test in the fixture gets its own random address. Because Tempx inboxes exist implicitly (mail sent to any name creates the mailbox), there is no setup API to race and no teardown. Two rules keep it hermetic: never reuse an address across tests, and never assert on all inbox contents — match the message, don't count it.
CI: no secrets, one env var
Named-inbox reads need no credentials, so the CI config is boring — which is the point:
# .github/workflows/e2e.yml (excerpt)
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
env:
TEST_RUN_ID: ${{ github.run_id }} # unique per run
BASE_URL: https://staging.example.com
Timeouts and retries without flakes
- One clock for mail. Give
waitForMessagea 45-second deadline and poll every 2 seconds — and let Playwright's ownexpecthandle the UI side. Two polling systems fighting each other is a classic flake source. - Retries need fresh addresses. Playwright retries re-run the whole test; the fixture generates a new inbox per attempt, so a retry can't be poisoned by the first attempt's mail.
- Rate limits. 240 reads/minute per client. Ten workers at 2-second polls is fine; a 64-worker fan-out should share one poller per process or back off on 429s.
When an email assertion fails at 2 a.m.
- Did the app send? Open the address in the Tempx app — if the mail is there, the test's matching failed; if not, it's the app.
- What actually arrived? The trace plus the inbox's message preview usually settle it; for header-level questions use the header analyzer.
- Wrong content? The MIME decoder shows the exact text and HTML parts that were sent.
The copy-paste starter spec lives in the Playwright integration; Cypress and Selenium have their own.
Create a temporary email address
Free, instant, no sign-up — messages auto-delete.
Go to your inbox