examples
Cypress testing with disposable email accounts
Test email sign-up, password verification and more with Cypress testing and MailSlurp. Capture OTP magic links and test auth flows end-to-end.
A signup test is incomplete while the account still waits for email verification. Cypress can fill in the form, but it also needs the code delivered to that new address before it can check that the intended user is signed in.
MailSlurp gives the test a private inbox and APIs for receiving its messages. Create a different inbox for each scenario, submit its address in the signup form and use the received code in the same browser test. This keeps parallel workers from reading each other's emails.
The example below uses mailslurp-client in Cypress's Node process. Browser commands call those operations through cy.task; your API key stays out of browser code. It does not require the separate cypress-mailslurp plugin or its cy.mailslurp() command.
Prepare your signup application
Run this against an application you control. The spec assumes /signup has an input named email, a Create account button, a following input named code and a Verify code button. After verification, an element with data-testid="signed-in-email" must show the signed-in account's address. Replace those selectors and the expected email subject with your application's actual interface.
This example checks email ownership during signup. If your application also requires a password or a second authentication factor, include those steps and assert that neither can be skipped.
Install Cypress and the MailSlurp client
In your test project, install the packages:
npm install --save-dev cypress mailslurp-client
Set MAILSLURP_API_KEY through your shell's private environment or CI secret store, and set CYPRESS_BASE_URL to your test application's origin. Do not commit a key to the configuration or prefix it with CYPRESS_, which is intended for Cypress configuration/environment values.
Create cypress.config.js with these Node tasks. The inbox has a ten-minute expiry as a fallback; explicit teardown below deletes it after the test. If your project already has setupNodeEvents, add these tasks to the existing function.
const { defineConfig } = require("cypress");
const { MailSlurp } = require("mailslurp-client");
module.exports = defineConfig({
e2e: {
supportFile: false,
baseUrl: process.env.CYPRESS_BASE_URL,
setupNodeEvents(on, config) {
const apiKey = process.env.MAILSLURP_API_KEY;
if (!apiKey) throw new Error("Set MAILSLURP_API_KEY");
const mail = new MailSlurp({ apiKey });
async function receive({ inboxId, address, since, subject }) {
const email = await mail.waitController.waitForMatchingFirstEmail({
inboxId, since: new Date(since), timeout: 60_000, unreadOnly: false,
matchOptions: {
matches: [{ field: "SUBJECT", should: "CONTAIN", value: subject }],
},
});
if (!email.to.includes(address)) throw new Error("Unexpected email recipient");
return email;
}
on("task", {
createTestInbox: () => mail.createInboxWithOptions({ expiresIn: 600_000 }),
async receiveOtp(input) {
const email = await receive(input);
const result = await mail.emailController.getEmailCodes({
emailId: email.id,
extractCodesOptions: {
method: "PATTERN", allowFallback: false,
minLength: 6, maxLength: 6, maxCandidates: 5,
},
});
const candidates = new Set(result.candidates.map(item => item.code));
if (!result.found || typeof result.code !== "string" ||
!/^[0-9]{6}$/.test(result.code) || candidates.size !== 1 ||
!candidates.has(result.code)) {
throw new Error("Expected one six-digit verification code");
}
return result.code;
},
async deleteTestInbox(inboxId) {
await mail.inboxController.deleteInbox({ inboxId });
return null;
},
});
return config;
},
},
});
The wait matches the subject inside this test's inbox and excludes messages older than the signup request. Recipient validation catches an unexpected response before extraction. The code must be one unambiguous six-digit string, so 004219 keeps its leading zeros and a message with two possible codes fails clearly.
Complete the signup in one test
Save the following as cypress/e2e/signup-email.cy.js. Setup creates the inbox before the test runs. The request timestamp is captured immediately before the form's send action, and the task's 90-second budget contains the 60-second email wait plus extraction.
describe("signup email code", () => {
let inbox;
beforeEach(() => {
inbox = undefined;
cy.task("createTestInbox", null, { log: false }).then(value => { inbox = value; });
});
afterEach(() => {
if (inbox) cy.task("deleteTestInbox", inbox.id, { log: false });
});
it("verifies the intended account", () => {
cy.visit("/signup");
cy.get('input[name="email"]').type(inbox.emailAddress, { log: false });
cy.then(() => {
const since = new Date().toISOString();
cy.contains("button", "Create account").click();
return cy.task("receiveOtp", {
inboxId: inbox.id, address: inbox.emailAddress,
since, subject: "Verify your email",
}, { timeout: 90_000, log: false });
}).then(code => {
cy.get('input[name="code"]').type(code, { log: false });
cy.contains("button", "Verify code").click();
cy.get('[data-testid="signed-in-email"]').should("have.text", inbox.emailAddress);
});
});
});
Cypress queues commands. Return the task's result through the command chain; do not use await cy.task(...) or read an inbox before its creation command finishes. The final assertion checks the account identity, rather than treating any success banner as proof that the right user signed in.
Run the spec with the private environment values already set:
npx cypress run --spec cypress/e2e/signup-email.cy.js
The application's test user is a separate resource from its MailSlurp inbox. Remove it through your application's supported test-data process after the scenario, including when a later assertion fails. Keep API keys, usable codes and message bodies out of shared CI artifacts.
Check isolation and failures before expanding the suite
Run two distinct signup scenarios concurrently and verify each final account address. Then suppress the application's send in a controlled test: the bounded email wait should fail instead of accepting another scenario's message. Test an invalid code separately and assert that the account remains unverified.
If a message is missing, first compare the submitted recipient, sender outcome, subject filter and request timestamp. Increasing every timeout will not fix a mismatched subject. The wait troubleshooting guide explains the checks in more detail.
The afterEach hook deletes this test's inbox after normal assertion failures. An interrupted runner still needs periodic cleanup of retained test resources; inbox expiry stops new receipt but does not delete retained data. Choose an expiry that comfortably covers the whole scenario.
For a password reset or magic link, continue with the Cypress email-testing guide. For an existing suite that uses the plugin, follow the plugin setup and keep the same isolation, current-message matching and account assertions.