MailFlatDocs
Documentation/Test frameworks & CI/Playwright

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.
Shell
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.
Shell
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.
TypeScript
// 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.
TypeScript
// 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.
TypeScript
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.