How to Test Email Sending with Cypress

Cypress specs read best when the email step looks like any other command. This is how to wrap the inbox API into cy.waitForEmail — and how to keep it deterministic under retries and parallel runs.

One task, one command

The polling loop lives in a cy.task (Node side, from the integration guide). Wrap it in a custom command so specs never touch plumbing:

// cypress/support/commands.mjs
Cypress.Commands.add("waitForEmail", (address, subjectRe, timeoutMs = 45000) => {
  cy.task("tempxWait", { address, subjectRe, timeoutMs })
    .should("not.be.null")
    .as("lastEmail");
  return cy.get("@lastEmail");
});

And a spec now says what it means:

const addr = "cy-" + Date.now().toString(36) + "-login@tempx.uk";

cy.visit("/login");
cy.get('input[name="email"]').type(addr);
cy.contains("button", "Send code").click();

cy.waitForEmail(addr, "login code").then((msg) => {
  const code = (msg.preview.match(/code is (\d{6})/) || [])[1];
  cy.get('input[name="code"]').type(code);
  cy.contains("button", "Sign in").click();
  cy.contains("Welcome back").should("be.visible");
});

Retries: know which loop you are in

Cypress retries commands until their assertions pass — but cy.task is not retried internally; it resolves once. That is why the task itself polls until deadline and resolves null on timeout: the .should("not.be.null") turns a missing email into a readable failure, and test-level retries start clean because each test mints a fresh address.

Parallel runs

Debugging a failed email step

Open the address in the Tempx app: mail present means the matching or extraction failed (the MIME decoder shows exactly what text the mail carried); no mail means the send side never fired. For header-level surprises, paste the source into the header analyzer.

The starter cy.task config is in the Cypress integration; Playwright and Selenium have equivalents.

Create a temporary email address

Free, instant, no sign-up — messages auto-delete.

Go to your inbox