Skip to content
Facteur

Cypress

Waiting for mail without cy.wait

Cypress runs your test inside the browser, which raises two problems for reading email: the API key must not be there, and the network call answers to the origin of the page under test. One Node task solves both.

The config, then the test

The task lives in the Cypress process, where the key can stay. The test only ever sees a command.

cypress.config.jsts
// cypress.config.js — the key stays here, out of the browser.
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    setupNodeEvents(on) {
      on('task', {
        async waitForMail({ sentTo }) {
          const response = await fetch('https://api.facteur.eu/v1/messages/search', {
            method: 'POST',
            headers: {
              authorization: 'Bearer ' + process.env.FACTEUR_API_KEY,
              'content-type': 'application/json',
            },
            body: JSON.stringify({ inbox: 'j3k9x2mq', sentTo, wait: 30000 }),
          });
          const { messages } = await response.json();
          return messages[0] ?? null;
        },
      });
    },
  },
});

// the test — the attempt number keeps a retry apart from the run before it.
it('the code arrives by email', () => {
  const tag = 'signup-a' + (Cypress.currentRetry ?? 0) + '-' + Date.now();

  cy.visit('/signup');
  cy.get('[name=email]').type(tag + '@j3k9x2mq.inbox.facteur.eu');
  cy.contains('button', 'Create my account').click();

  cy.task('waitForMail', { sentTo: tag }, { timeout: 40000 }).should((message) => {
    expect(message, 'no mail arrived').to.not.be.null;
    expect(message.otp).to.match(/^[0-9]{6}$/);
  });
});
  • The key stays on the Node side

    Your spec is served to a page: a key written into it would be a key in a page’s JavaScript. The task keeps it on the other side.

  • cy.task yields a value, so the chain continues

    The message comes back into the usual command queue, and .should() behaves as it does on a cy.get().

  • The attempt number is in the address

    Without it, a retried test reads the mail from its own first attempt and passes for the wrong reason, exactly when something is already flaky.

Why there is no cy.wait

The waiting happens on our side

The request stays open until the message lands, with a maximum delay you set. A fixed cy.wait is either too short on a loaded machine or wasted time on every run.

The code is already extracted

The message arrives with its one-time code in a field of its own.

One mailbox is enough for the suite

Each test receives at its own address inside the same mailbox, with nothing to create. On a plan that includes a single mailbox, that is the difference between a suite that runs and one that hits a ceiling.

What about a Cypress plugin?

Planned, not published: the version above is the one that works today, in about thirty lines you keep.

See the state of each integration

Write this test today

A test mailbox and a read-only key are enough. The free plan asks for no card.