Guide
Cypress Email Testing with a Real Inbox
Use Nylas CLI as the email layer for Cypress tests. Create an isolated agent inbox, poll it from cy.task, read the delivered message as JSON, and continue the browser flow without Gmail API setup.
Written by Qasim Muhammad Staff SRE
Reviewed by Qasim Muhammad
Why use a real inbox for Cypress email tests?
Cypress email testing usually gets blocked by the inbox layer, not by Cypress itself. Password reset, email verification, invite, and OTP flows all leave the browser and wait for a message. Mocking that step proves the UI path, but it does not prove that the email was sent, routed, rendered, and readable by a user.
Nylas CLI gives Cypress a provider-neutral inbox reader. The test runner can call nylas email list and nylas email read from Node, parse JSON, and keep browser assertions inside the Cypress spec.
1. Create a managed test inbox
Start with a dedicated Nylas agent account for the test suite. nylas agent account create gives you a real inbox without paying for a Google Workspace seat or connecting a personal mailbox through OAuth.
nylas agent account create cypress@yourapp.nylas.email --json
# Confirm messages can be listed from the active grant
nylas email list --json --limit 5For parallel Cypress workers, create one inbox per worker or add a unique token to each subject line. A per-worker inbox is cleaner because the test does not need to filter through unrelated messages.
2. Add a Cypress task that polls email
Browser code cannot spawn local processes, so the CLI call belongs in the Cypress Node process. Add a cy.task handler in cypress.config.ts that runs the CLI and returns the first matching message.
import { defineConfig } from "cypress"
import { execFileSync } from "node:child_process"
function runNylas(args: string[]) {
return JSON.parse(execFileSync("nylas", args, { encoding: "utf8" }))
}
export default defineConfig({
e2e: {
setupNodeEvents(on) {
on("task", {
latestEmail({ grantId, subject }: { grantId: string; subject: string }) {
const messages = runNylas([
"email",
"search",
subject,
grantId,
"--json",
"--limit",
"5",
])
return messages[0] ?? null
},
readEmail({ messageId, grantId }: { messageId: string; grantId: string }) {
return runNylas(["email", "read", messageId, grantId, "--json"])
},
})
},
},
})3. Verify a password reset email
The test below submits a reset form, waits for a matching subject, reads the full message, extracts a link, and visits it. The same shape works for invite links and account confirmation links.
const grantId = Cypress.env("NYLAS_TEST_GRANT_ID")
it("resets a password from email", () => {
cy.visit("/forgot-password")
cy.get("input[name=email]").type("cypress@yourapp.nylas.email")
cy.contains("button", "Send reset link").click()
cy.task("latestEmail", {
grantId,
subject: "Reset your password",
}, { timeout: 60000 }).then((message: any) => {
expect(message, "reset email").to.exist
cy.task("readEmail", { grantId, messageId: message.id }).then((full: any) => {
const match = String(full.body).match(/https:\/\/[^"\s]+reset[^"\s]+/)
expect(match?.[0], "reset URL").to.include("/reset")
cy.visit(match![0])
})
})
})4. Test OTP codes when there is no link
OTP flows can use the same search/read task, or you can run nylas otp watch locally while building the test. The watch command continuously scans new messages and prints the latest code.
nylas otp watch cypress@yourapp.nylas.email --interval 5 --no-copyIn CI, keep OTP extraction inside cy.task so the test can fail with a normal Cypress assertion instead of depending on a long-running terminal watcher.
When should you still use a mock or sandbox?
Use a mock when the test only checks local UI state. Use a sandbox-only service when you need to inspect SMTP envelope behavior. Use a real inbox when the flow depends on deliverability, provider rendering, magic links, or user-facing message content.
If you are comparing hosted testing services, read the Mailtrap alternative guide next. It covers where Mailtrap and Mailosaur fit versus a Nylas-managed inbox.
What should a Cypress email test prove?
A useful Cypress email test proves the same path a user follows. The browser submits a form, the application sends a message, the inbox receives it, and the test continues from the link or code inside that message. If the inbox step is mocked, the test can still catch front-end regressions, but it cannot catch broken templates, missing variables, provider delivery failures, or a worker that never sent the message.
Start by choosing the smallest user story that needs a real inbox. Password reset is a common first target because it has a clear trigger, a single recipient, a predictable subject, and a link that returns to the app. Account verification, magic login, user invites, and payment confirmation flows follow the same shape. Each story should own a unique subject marker so the poller can identify the message from the current test run.
Keep lower-level email rendering tests separate from browser tests. Unit tests can assert that a template function includes the right variables. Integration tests can assert that your mailer queues a send request. Cypress should cover the end-to-end handoff: user action, delivered email, extracted link or code, and browser continuation. That split keeps the browser suite small enough to run on every pull request.
How should you isolate test inboxes?
Isolation is the difference between a stable Cypress email test and a flaky one. A shared inbox can work for a single local developer, but it becomes noisy when parallel CI jobs, retries, and branch builds use the same address. A dedicated Nylas Agent Account per suite is a good baseline. For parallel workers, create one address per worker or include the worker index in the subject marker.
The subject marker should be generated by the test, not hard-coded in the app. A simple pattern is `Reset your password [run-123 worker-2]`. The app receives the marker as part of the user input or a test-only metadata field, then includes it in the email subject. The Cypress task searches for that marker and ignores older messages. This is safer than searching only by recipient because inboxes often keep prior attempts.
When a test creates user accounts, make the email address unique too. For managed inboxes, provision addresses ahead of time and assign them to workers through environment variables. For app users, use plus addressing or a generated local part if your product accepts it. Store the created user ID in the test context so cleanup can remove the account without deleting the inbox itself.
How should Cypress poll email without flaking?
Email delivery is asynchronous, so the poller needs retry logic. Do not call `nylas email list` once and fail immediately. Poll every few seconds until the matching subject appears or the test timeout expires. The task should return a simple object to Cypress: message ID, subject, sender, timestamp, and any fields needed for the next assertion. Keep provider-specific response details inside the task layer.
Use a fixed upper bound for the wait. Sixty seconds is enough for most password reset and OTP flows in CI. If your application intentionally delays email through a queue, set the timeout to match that queue behavior and log the run ID in the failure message. A failure that says `no reset email for marker run-123 after 60s` is much easier to debug than a generic Cypress timeout.
Sort or filter messages by received time. Inboxes can contain old messages with the same subject, especially after retries. The task should ignore messages older than the test start time or prefer the newest matching message. If the CLI output includes timestamps, compare them in Node before returning the candidate to Cypress. If not, add a unique marker to the body and subject so old messages cannot match.
What should you assert inside the delivered email?
Assert the parts that would break the user journey. For a reset link, confirm the URL points at the expected host, contains a token or code, and lands on the reset page. For an invite, confirm the recipient name, workspace name, and action URL. For an OTP, confirm the code format and submit it through the same form a user sees. Avoid broad snapshot assertions on the entire email body; one footer change should not fail every browser test.
Read the full message only after the search result identifies the right email. Listing messages is cheaper and easier to retry. Reading the full message gives you HTML, plain text, and headers for deeper checks. If your app sends both HTML and text bodies, prefer extracting links from HTML and verifying a few plain-text markers separately. That catches template rendering bugs without making the test brittle.
Be careful with links that include escaped HTML entities or tracking wrappers. Parse the body as text first, then normalize `&` before visiting the URL. If your product adds click tracking, assert that the redirect eventually reaches the app route rather than assuming the first URL in the message is already the final destination. The browser flow should verify what a real recipient experiences.
How do you run Cypress email tests in CI?
CI needs the same inputs as local development: a Nylas API key, a grant ID or managed inbox address, and a way to install the CLI. Store secrets in your CI provider, not in `cypress.env.json`. Install the CLI during the job, run a quick `nylas email list --json --limit 1` smoke check, then start the Cypress suite. If the smoke check fails, stop before launching the browser.
Keep the real-inbox suite small. Run template unit tests and mailer integration tests on every commit, then run a focused Cypress email group for the journeys that matter most. A good starting set is one password reset test, one email verification test, and one invite test. Add OTP only if OTP is a core login path. This keeps cost and runtime predictable while still covering the highest-risk handoffs.
Treat inbox state as test data. Do not rely on manually clearing messages before a run. Use unique markers, ignore old messages, and record the message ID in test output. When a failure happens, the message ID lets a developer inspect exactly what was delivered. That turns email testing from a flaky black box into a normal debugging workflow.
How do you debug a failed Cypress email test?
Debug the failure by finding the first missing handoff. If the browser never submitted the form, the problem is in the UI flow. If the app submitted the form but no send job was created, the problem is in application logic. If the send job exists but no inbox message arrives, inspect the provider response and worker logs. If the message arrives but the test cannot parse it, the failure is in the polling or extraction helper.
Save enough evidence in the test output. The subject marker, test inbox address, message ID, received timestamp, and run URL make failures actionable. Avoid logging the full message body unless the test environment is safe for that data. Most failures can be diagnosed from identifiers and a few expected markers without exposing reset links or user data.
When a test flakes, compare the received timestamp with the test start time. Old messages with matching subjects are a common cause. The second cause is a timeout that is shorter than the application queue delay. The third is a parser that assumes one HTML shape and fails when a template changes. Fix the cause instead of only raising the timeout.
What security rules apply to email test flows?
Treat test inboxes as real mailboxes. They can receive reset links, invite links, OTP codes, and customer-like data. Store API keys and grant IDs in the CI secret store. Do not commit Cypress environment files with live credentials. Rotate the sender and reader credentials if a log ever exposes a token or reset URL.
Keep test users separated from production users. A test should never send reset links to a real customer address, and a production account should never depend on a test inbox. Use a dedicated domain, a managed agent address, or a clearly named test mailbox. That naming also helps support teams recognize automated traffic in logs.
Make reset and invite links short-lived. Browser tests often expose links in artifacts, screenshots, or trace files. Short token lifetimes reduce the damage if a test artifact is shared too widely. The test can still pass because it reads the email immediately after the app sends it.
How should a team roll out Cypress email testing?
Start with one journey and one helper. Password reset is usually enough to prove the pattern: create user, request reset, poll inbox, read message, extract link, visit link, and complete the form. Once that test is stable for a week, reuse the helper for account verification or team invites.
Document the ownership of the test inboxes. Someone should know which suite uses each inbox, where credentials live, and how to rotate them. If a test inbox starts receiving unexpected messages, the owner can decide whether to keep, quarantine, or replace it. Shared ownership usually leads to slow cleanup.
Review the suite after each new email feature. Ask whether the feature needs a real inbox test, a template test, or only a mailer integration test. Not every email deserves a browser run. The value comes from covering the handoffs that users and support teams care about most.
References for this workflow
- Cypress cy.task documentation -- running Node-side work from a Cypress spec
- Cypress Node events overview -- configuring
setupNodeEventsincypress.config.ts - Nylas Agent Accounts quickstart -- creating managed inboxes for automated workflows
Next steps
- E2E email testing with Playwright -- same inbox pattern for Playwright projects
- Extract OTP codes from email -- OTP-focused test flows
- Receive inbound email -- managed addresses and webhook delivery
- Command reference -- every email, agent, and OTP flag