MoeMail
Back to Blog

Cypress Email Testing: Verify Emails in E2E Tests

Cypress is great at driving the browser, but the verification email lives outside it. This guide adds Cypress email testing with a custom command that provisions a disposable inbox and reads it through an API — part of our email testing for developers guide, alongside the Playwright and Selenium versions.

The approach

Create one inbox per test, drive the flow, then poll the inbox API until the message arrives. Wrap it in custom commands so specs read cleanly.

Custom commands

// cypress/support/commands.js
Cypress.Commands.add('createInbox', () => {
  return cy.request({
    method: 'POST',
    url: API + '/emails/generate',
    headers: { 'X-API-Key': Cypress.env('MAIL_KEY') },
  }).its('body')
})

Cypress.Commands.add('waitForEmail', (id, tries = 20) => {
  const poll = (n) => cy.request({
    url: API + '/emails/' + id + '/messages',
    headers: { 'X-API-Key': Cypress.env('MAIL_KEY') },
  }).then((res) => {
    if (res.body.messages.length) return res.body.messages[0]
    if (n === 0) throw new Error('no email arrived')
    cy.wait(1500)
    return poll(n - 1)
  })
  return poll(tries)
})

The test

it('verifies the signup email', () => {
  cy.createInbox().then((inbox) => {
    cy.visit('/signup')
    cy.get('#email').type(inbox.address)
    cy.contains('Sign up').click()
    cy.waitForEmail(inbox.id).then((msg) => {
      const code = msg.text.match(/[0-9]{6}/)[0]
      cy.get('#code').type(code)
    })
    cy.contains('Welcome').should('be.visible')
  })
})

The recursive poll keeps Cypress's command queue happy without arbitrary cy.wait on a fixed timer. The same command also tests password-reset emails — just match the link instead of a code.

Tips

Give each test its own address, prefer a webhook when you can, and keep CI timeouts tight. Create a mailbox and read the OpenAPI docs to wire it up.