Receive email in a Playwright test
A sign-up journey ends in a mailbox, and the test has to read what arrives there. This page calls the API directly with a key. No Facteur plugin for Playwright is published yet, so there is no package to install, and the helper below needs none.
Before you start
Section titled “Before you start”- A read-only API key, kept in the secrets of your continuous integration as
FACTEUR_API_KEY. The quickstart shows how to create it. - The identifier of your inbox: the eight characters before
.inboxin its address.
The helper
Section titled “The helper”The helper starts a search and holds it open until the email arrives, twenty seconds at most:
const API = 'https://api.facteur.eu'const INBOX = 'j3k9x2mq'
export async function waitForMail(sentTo: string, wait = 20_000) { const search = new URLSearchParams({ inbox: INBOX, sentTo, wait: String(wait) }) const response = await fetch(`${API}/messages?${search}`, { headers: { authorization: `Bearer ${process.env.FACTEUR_API_KEY}` }, }) if (!response.ok) throw new Error(`Facteur answered ${response.status}`)
const { messages } = await response.json() if (!messages[0]) throw new Error(`Nothing arrived for ${sentTo}`)
return messages[0]}The test
Section titled “The test”import { expect, test } from '@playwright/test'import { waitForMail } from './facteur'
test('the confirmation code activates the account', async ({ page }) => { const tag = `run-${Date.now()}` const address = `signup+${tag}@j3k9x2mq.inbox.facteur.eu`
// Started before the click: the request stays open and answers when the email arrives. const arriving = waitForMail(`signup+${tag}`)
// The sign-up journey of your own application. await page.goto('/signup') await page.getByLabel('Email').fill(address) await page.getByRole('button', { name: 'Create my account' }).click()
const message = await arriving expect(message.status).toBe('parsed') await page.getByLabel('Confirmation code').fill(message.otp) await page.getByRole('button', { name: 'Confirm' }).click()
await expect(page.getByText('Account verified')).toBeVisible()})Start the wait before the action
Section titled “Start the wait before the action”This is the one line whose order matters. The request leaves before the click, stays open, and answers when the email arrives. Started after the click, it would chase a message that may already have been processed, and you would see the flaw the day your continuous integration slows down.
One address per test, with nothing to create
Section titled “One address per test, with nothing to create”Any address that ends with the domain of your inbox arrives in it. You call nothing to create signup+run-1734@j3k9x2mq.inbox.facteur.eu: you write it. That is what lets sixteen parallel tests share one inbox without reading each other’s mail. An organization can keep twenty waits open at once. Beyond that, the answer is 429 with the code TOO_MANY_WAITS.
Put the name of the test and the attempt number in the tag. Without the attempt number, a retried test reads the email of its first attempt and passes for the wrong reason, precisely when something is already unstable.
The code is already out of the message
Section titled “The code is already out of the message”message.otp carries the one-time code, found when the message was received. You write no regular expression, and nothing breaks the day the template of the email changes. message.links holds the links in order of appearance, each as plain text: pass one to page.goto().
Read message.status first. A message exists for a moment before it is read, with no subject and no code. A search returns parsed messages only by default, so the case comes up only if you ask for status=any.