Playwright email testing

Playwright drives the browser; the email step needs a mailbox your test can read. This integration gives you a small polling helper plus a complete spec that signs a user up, waits for the verification email, extracts the code and finishes the flow — the pattern transfers directly to OTP and password-reset tests.

page.fill emailpage.click submitwaitForMessage(addr)extractCode(preview)page.fill codeassert success

The inbox helper

Save as wait-for-email.mjs next to your specs. It polls the free inbox API — no credentials for named inboxes — and returns the first matching message:

// wait-for-email.mjs — poll the Tempx inbox API until a matching mail lands
const TEMPX = "https://tempx.uk/api/inbox";

export async function waitForMessage(address, opts = {}) {
  const timeoutMs = opts.timeoutMs ?? 45000;
  const intervalMs = opts.intervalMs ?? 2000;
  const match = opts.match ?? (() => true);
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const res = await fetch(TEMPX + "?address=" + encodeURIComponent(address));
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, 5000));
      continue;
    }
    if (!res.ok) throw new Error("Tempx API returned " + res.status);
    const { messages } = await res.json();
    const hit = messages.find(match);
    if (hit) return hit;
    await new Promise((r) => setTimeout(r, intervalMs));
  }
  throw new Error("No matching email for " + address + " within " + timeoutMs + "ms");
}

export function extractCode(text) {
  const m = /\b(\d{4,8})\b/.exec(text || "");
  return m ? m[1] : null;
}

A complete spec: signup with email verification

// signup.spec.mjs
import { test, expect } from "@playwright/test";
import { waitForMessage, extractCode } from "./wait-for-email.mjs";

// unique inbox per run — parallel workers never share mail
const addr = "pw-" + (process.env.TEST_RUN_ID || "local") + "-signup@tempx.uk";

test("signup sends a verification email with a working code", async ({ page }) => {
  await page.goto("https://staging.example.com/signup");
  await page.getByLabel("Email").fill(addr);
  await page.getByLabel("Password").fill("correct-horse-battery");
  await page.getByRole("button", { name: "Create account" }).click();

  const msg = await waitForMessage(addr, {
    timeoutMs: 45000,
    match: (m) => /verify|confirm/i.test(m.subject),
  });

  expect(msg.from).toContain("noreply@example.com");
  const code = extractCode(msg.preview);
  expect(code).toHaveLength(6);

  await page.getByLabel("Verification code").fill(code);
  await page.getByRole("button", { name: "Verify" }).click();
  await expect(page.getByText("Welcome")).toBeVisible();
});

Notes that save an afternoon

Run this spec against your staging

The only setup is picking an address prefix — the inbox exists the moment your app sends to it.

Open an inbox

Related