Skip to content

Receive email in a Cypress test

Cypress runs your test in the browser. That raises a problem for reading an email: a key written in a spec is served to a page, and anyone who opens the developer tools of the browser reads it. A Node task solves it, because the task runs in the Cypress process and the key never reaches the page.

This page calls the API directly. No Facteur plugin for Cypress is published yet, so there is no package to install.

  1. A read-only API key, kept in the secrets of your continuous integration as FACTEUR_API_KEY. The quickstart shows how to create it.
  2. The identifier of your inbox: the eight characters before .inbox in its address. It is j3k9x2mq in the examples.
cypress.config.js
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
setupNodeEvents(on) {
on('task', {
async waitForEmail({ sentTo }) {
const search = new URLSearchParams({ inbox: 'j3k9x2mq', sentTo, wait: '30000' })
const response = await fetch(`https://api.facteur.eu/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()
return messages[0] ?? null
},
})
},
},
})
it('the code arrives by email', () => {
const tag = `signup-a${Cypress.currentRetry ?? 0}-${Date.now()}`
// The sign-up journey of your own application.
cy.visit('/signup')
cy.get('[name=email]').type(`${tag}@j3k9x2mq.inbox.facteur.eu`)
cy.contains('button', 'Create my account').click()
cy.task('waitForEmail', { sentTo: tag }, { timeout: 40000 }).should((message) => {
expect(message, 'an email arrived').to.not.be.null
expect(message.otp).to.match(/^[0-9]{6}$/)
})
})

The wait happens on the side of Facteur: the request stays open until the message arrives, for the time you set. A fixed pause is too short on a loaded machine, and time lost on every other run.

The timeout of cy.task must stay above the wait sent to the API: forty seconds for a wait of thirty. Otherwise Cypress gives up on the task before the answer arrives, and the failure blames the wrong thing.

In this test the search starts after the click, because Cypress runs its commands one after another. Nothing is lost: the search looks once before it waits, so a message that arrived during the click comes back on that first look.

Cypress.currentRetry is 0 on the first attempt. Without it in the address, a retried test reads the email of its first attempt and passes for the wrong reason, precisely when something is already unstable.

Any address that ends with the domain of the inbox arrives in it, with nothing to create. Each test receives at its own address in the same inbox, so a plan with a single inbox is enough for a whole suite.