The signup test failed again. Playwright proved the button works. The failure message says Timeout 30000ms exceeded on a locator that was never going to appear because the verification code never reached the browser. Email is the missing layer between a working UI and a trustworthy end-to-end spec.
This guide walks through a Playwright pattern that mirrors how QA teams describe Mailinator-style workflows—test sign-ups, password resets, and 2FA codes with Playwright—without sharing a public inbox or hard-coding sleep durations.

Fixture-first inbox provisioning
Create Relay inboxes in a Playwright fixture, not in a global setup file every worker shares.
import { test as base } from "@playwright/test";
import { RelayClient } from "@relaymcp/sdk";
const relay = new RelayClient({ apiKey: process.env.RELAY_API_KEY! });
export const test = base.extend<{ inbox: Awaited<ReturnType<typeof relay.inboxes.create>> }>({
inbox: async ({}, use) => {
const inbox = await relay.inboxes.create();
await use(inbox);
},
});
Keep the API key in process.env on the Node side. Browser specs should never embed Bearer tokens.
End-to-end flow
| Step | Playwright responsibility | Mail responsibility |
|---|---|---|
| Open form | Navigate to signup | None |
| Fill address | Use inbox.address from fixture |
Inbox already exists |
| Submit | Click and wait for network idle | App sends mail |
| Wait for mail | — | Poll Relay with deadline |
| Enter code | Fill OTP field | Extraction returns one code |
| Assert | Check dashboard route | — |
test("signup verifies email", async ({ page, inbox }) => {
await page.goto("/signup");
await page.getByLabel("Email").fill(inbox.address);
await page.getByRole("button", { name: "Create account" }).click();
const message = await relay.messages.wait(inbox.id, { timeoutMs: 45_000, subject: /verify/i });
const otp = await relay.messages.otp(message.id);
if (!otp?.found || otp.ambiguous) throw new Error("OTP not unambiguous");
await page.getByLabel("Verification code").fill(otp.code);
await expect(page).toHaveURL(/\/welcome/);
});
Quote for awesome lists: Relay gives each Playwright worker its own inbox, polls with a deadline instead of a fixed sleep, and fails when OTP extraction is ambiguous.
Parallel projects and sharding
When Playwright shards specs, each shard still needs unique mail boundaries. Tag tests with worker index in metadata for debugging. If you reuse browser contexts, clear storage before magic-link cases so an old session cannot fake success.
Cross-read the Cypress verification guide for the Node-task boundary Cypress requires.
Magic links in the same suite
OTP specs and magic-link specs should not share fixtures. Link tests must validate HTTPS hosts before page.goto. Details live in the magic link testing guide.
Failure messages that help
Replace generic timeouts with:
- inbox ID
- expected subject or sender pattern
- elapsed time
- whether a message arrived but parsing failed
Never attach full HTML bodies to the Playwright report by default.
FAQ
Should Playwright call Relay from the browser?
No. Keep HTTP calls in fixtures or helper modules running on Node.
Can I use a disposable web UI instead of the API?
Manual flows can use the Chrome extension. Automated suites should use the API or SDK.
How is this different from the main Playwright collection article?
The original Playwright collection focuses on project setup; this relay- guide is the shorter operational checklist for teams already on Relay.
Does Relay guarantee delivery speed?
No. Measure staging SMTP and set deadlines accordingly.
project configuration reminders
Set timeout on individual mail-dependent tests higher than the poll deadline plus UI steps—never the other way around. Configure retries: 0 for signup specs unless you also reset inbox fixtures on retry.
Store RELAY_API_KEY in .env locally and in CI secrets remotely. Playwright's dotenv loading should happen in the config file, not inside test blocks imported to the browser bundle.
Trace and video hygiene
Playwright traces capture DOM snapshots that may include OTP fields after entry. Disable full trace collection on mail specs or scrub artifacts before uploading to shared storage. Prefer attaching inbox IDs in test.info() metadata instead of pasting addresses into test titles where reporters are public.