Using MailFlat with Playwright
Give every Playwright test its own real inbox, read the verification code your app actually sent, and finish the signup flow end to end.
How it fits together
Playwright already drives the browser; MailFlat gives it a mailbox. A fixture opens an inbox before the test, hands the address over, and deletes it afterwards — so parallel workers never read each other's mail and a failed run cleans itself up.
Skip the setup
The steps below are what a working project looks like. If you want that project rather than the explanation, generate it — one runnable smoke test, one signup test to edit, and a gitignored .env for your key.
npm create mailflat@latest -- --template playwright
Zero dependencies of its own; the generated project pulls the SDK.
Install
The client has no dependencies of its own and runs on the Node that Playwright already uses.
npm i -D @mailflat/sdk
A fixture that hands each test an inbox
Everything after the use call is teardown, so it runs even when the test fails.
// e2e/fixtures.ts
import { test as base } from "@playwright/test";
import { MailFlat, type Inbox } from "@mailflat/sdk";
const mf = new MailFlat({ apiKey: process.env.MAILFLAT_API_KEY! });
export const test = base.extend<{ inbox: Inbox }>({
inbox: async ({}, use) => {
const inbox = await mf.create({
prefix: `e2e-${Date.now().toString(36)}`,
retentionHours: 2,
});
await use(inbox);
await inbox.delete();
},
});
export { expect } from "@playwright/test";
The test
A real address, a real email, a real code. No test-mode backdoor in the app under test.
// e2e/signup.spec.ts
import { test, expect } from "./fixtures";
test("user can sign up with a real code", async ({ page, inbox }) => {
await page.goto("/signup");
await page.fill("#email", inbox.address);
await page.click("#submit");
await page.fill("#code", await inbox.waitForOtp({ timeout: 60_000 }));
await expect(page).toHaveURL(/dashboard/);
});
Magic links instead of codes
Same idea, one step shorter: take the first link out of the message and navigate to it.
const msg = await inbox.waitForMessage({ timeout: 60_000 });
await page.goto(msg.raw.links[0]);
await expect(page).toHaveURL(/dashboard/);
Worth knowing
Playwright runs workers in parallel by default. One inbox per test is what keeps that safe.
Set MAILFLAT_API_KEY in your CI secrets, never in playwright.config.ts.
See also
TypeScript
The same package as JavaScript, with types shipped in the box: Message, CreateInboxOptions, WaitOptions and typed errors.
Waiting and timeoutsMail is asynchronous, so every email test waits for something. The difference between a solid suite and a flaky one is how that wait is written.
Testing & CIGive every CI run its own real inbox: the four API calls, parallel shards, teardown, and how to keep an end-to-end suite fast and honest.