Guide · Playwright
Test sign-up emails in Playwright,
without a shared inbox.
Verification codes, magic links and password resets are the flows most likely to break and the hardest to test. Give every test its own disposable address, wait for the email, and read the code or link from it.
Three steps
- 1
Install the package
One small dependency, no IMAP client or mail server to run.
npm install --save-dev poofbox
- 2
Add your token
Dashboard → MCP access → New token. Save it as POOFBOX_TOKEN locally and in CI.
POOFBOX_TOKEN=pbm_…
- 3
Wait for the email
waitForEmail() returns the moment it lands, with the code and link picked out.
await inbox.waitForEmail()
signup.spec.ts
import { test, expect } from '@playwright/test';
import { Poofbox } from 'poofbox';
const pb = new Poofbox(); // reads POOFBOX_TOKEN
test('sign up and verify the email address', async ({ page }) => {
const inbox = await pb.createInbox();
await page.goto('/signup');
await page.getByLabel('Email').fill(inbox.address);
await page.getByLabel('Password').fill('correct horse battery staple');
await page.getByRole('button', { name: 'Create account' }).click();
const email = await inbox.waitForEmail({ subject: 'verify' });
await page.getByLabel('Verification code').fill(email.code!);
await page.getByRole('button', { name: 'Verify' }).click();
await expect(page.getByText('Welcome')).toBeVisible();
await inbox.delete();
});Magic links and fixtures
The same two calls cover login links and password resets. A fixture gives every test its own inbox and cleans up after it.
Magic link login
magic-link.spec.ts
test('log in with a magic link', async ({ page }) => {
const inbox = await pb.createInbox();
await page.goto('/login');
await page.getByLabel('Email').fill(inbox.address);
await page.getByRole('button', { name: 'Email me a link' }).click();
const { link } = await inbox.waitForEmail({ subject: 'sign in' });
await page.goto(link!);
await expect(page.getByText('Signed in')).toBeVisible();
await inbox.delete();
});A fresh inbox for every test
fixtures.ts
import { test as base } from '@playwright/test';
import { Poofbox, type Inbox } from 'poofbox';
const pb = new Poofbox();
export const test = base.extend<{ inbox: Inbox }>({
inbox: async ({}, use) => {
const inbox = await pb.createInbox();
await use(inbox);
await inbox.delete();
},
});What waitForEmail() returns
No HTML parsing in your tests: the code and the link come back as fields, already ranked.
code
The most likely one-time code, e.g. "482913".
link
The best action link: verify, confirm, magic link or reset.
codes · links
Every candidate, ranked, if you need another one.
subject · from · text
For assertions on the email itself.
attachments
Names, types and signed download URLs.
receivedAt · id
When it arrived, and an id for readEmail().
Run it in CI
Add the token as a repository secret and pass it to the test step. Use an MCP token, never your license key: tokens can be revoked one by one.
.github/workflows/e2e.yml
- name: Playwright tests
run: npx playwright test
env:
POOFBOX_TOKEN: ${{ secrets.POOFBOX_TOKEN }}Tips
Filter, don’t sleep
Pass subject or from, so a welcome email is never mistaken for the verification one.
No race to lose
Mail that lands after the inbox was created counts, even if it arrives before you start waiting.
One inbox per test
Keeps parallel workers apart. Personal: 10 active inboxes and 5 parallel waits; Pro: 50 and 20.
Clean up
Delete inboxes when done to free the slot. A deleted address is never given to anyone else.
Long-lived test users
Accounts you keep, like a test user that gets password resets, belong on the permanent poofy.uk domain.
Agents driving the browser
With Claude Code, Cursor or the Playwright MCP server, connect Poofbox over MCP and let the agent call the tools itself.
Personal €9.99 or Pro €34.99 a year
Plus VAT. Comparing tools? See Poofbox vs Mailosaur and vs MailSlurp.