Cypress email testing
Cypress commands run in the browser, but cy.task hands work to the Node process — which is exactly where inbox polling belongs. Register one task, and every spec can wait for real email with a single, readable command.
Register the task
// cypress.config.mjs
import { defineConfig } from "cypress";
const TEMPX = "https://tempx.uk/api/inbox";
export default defineConfig({
e2e: {
baseUrl: "https://staging.example.com",
async setupNodeEvents(on) {
on("task", {
// resolves with the matching message, or null on timeout
async tempxWait({ address, subjectRe, timeoutMs = 45000 }) {
const deadline = Date.now() + timeoutMs;
const re = subjectRe ? new RegExp(subjectRe, "i") : null;
while (Date.now() < deadline) {
const res = await fetch(TEMPX + "?address=" + encodeURIComponent(address));
if (res.ok) {
const { messages } = await res.json();
const hit = messages.find((m) => !re || re.test(m.subject));
if (hit) return hit;
}
await new Promise((r) => setTimeout(r, 2000));
}
return null;
},
});
},
},
});
Use it in a spec
// signup.cy.mjs
describe("signup", () => {
it("emails a verification code and completes the flow", () => {
const addr = "cy-" + Date.now().toString(36) + "@tempx.uk";
cy.visit("/signup");
cy.get('input[name="email"]').type(addr);
cy.get('input[name="password"]').type("correct-horse-battery");
cy.contains("button", "Create account").click();
cy.task("tempxWait", { address: addr, subjectRe: "verify" })
.should("not.be.null")
.then((msg) => {
expect(msg.from).to.contain("noreply@example.com");
const code = (msg.preview.match(/\b(\d{6})\b/) || [])[1];
expect(code, "6-digit code in preview").to.be.a("string");
cy.get('input[name="code"]').type(code);
cy.contains("button", "Verify").click();
cy.contains("Welcome").should("be.visible");
});
});
});
Why this stays stable
- One address per spec. Generated from a timestamp, so parallel runs and retries never share an inbox.
- Polling inside one task. The Node loop keeps the command log clean — one
tempxWaitrow instead of dozens of request rows; Cypress retries re-run the task, and the helper returns quickly once the mail is there. - Timeout semantics. The task resolves
nullon timeout so the assertion failure names the real problem (expected message, got null) rather than a fetch stack trace. - Rate limit headroom. 240 reads/minute against a 2-second poll — dozens of parallel specs stay under the cap.
Point it at your staging app
Register the task once and every email-dependent spec becomes deterministic.
Open an inbox