How does a Cypress spec verify the suspension email the admin console sent?
answer
- The browser cannot open a mailbox
- Interception only sees the page's traffic
- Node is the only door outward
- Bound the poll inside the task timeout
- Return parsed data, never the client
basics
~20 sNot from the browser. The spec has no mailbox client, and cy.intercept() only observes traffic the browser makes, so the check goes through cy.task(), where Node can poll a mail API, or cy.request() against a test-only inbox endpoint.
solid answer
~40 sThe spec is page JavaScript: no IMAP client, no queue consumer, no socket of its own. `cy.intercept()` does not help either, because it only observes requests the browser makes and a message sent server-to-server never passes through the page. Two doors exist. `cy.task()` runs a handler in the Node process that loaded your Cypress config, where a real mail-API client can look for the message and return its parsed subject and body as plain data. `cy.request()` issues HTTP from that same Node process, so a test-only inbox endpoint works with no CORS involved. Either way the check must be bounded: a task has to finish inside `taskTimeout`, 60 seconds by default, so poll with its own shorter deadline and return only serialisable values — never the client itself.
code
javascript · 17 lines// cypress/e2e/tenant-suspension.cy.js
it('emails the owner when a staging environment is suspended', () => {
const since = Date.now()
cy.visit('/tenants/acme/environments/staging')
cy.get('[data-cy=suspend-environment]').click()
cy.contains('[data-cy=env-status]', 'Suspended').should('be.visible')
// the page never sees this send, so ask the Node half to look for it
cy.task(
'findSuspensionEmail',
{ to: '[email protected]', since, deadlineMs: 20000 },
{ timeout: 30000 }
)
.its('subject')
.should('contain', 'staging environment suspended')
})go deeper
Know that a Cypress spec runs in the browser and can only reach beyond it through cy.task() or cy.request(); opening a mailbox from the spec itself is not something the tool offers.
Explain why cy.intercept() cannot observe a server-to-server send, and which of the two doors carries the check instead.
Show the whole shape: a bounded poll in Node, a command timeout wider than that deadline, a search scoped to this test, and plain serialisable data coming back.
Decide whether the notification belongs in the browser suite at all, or whether the contract is better proved where the message is produced, with the UI test covering what the operator sees.
## What the spec itself cannot see The suspension email is produced by the admin console's backend and handed to a mail provider. None of that happens in the browser. A Cypress spec runs inside the application's own page, so what it can observe is what the page can observe: the DOM, the page's own network activity, its storage, its globals. A mailbox, a message queue, an object store and a database are all outside that circle, and no Cypress setting widens it. This is the honest shape of the limit. It is not that the check is hard; it is that the browser half of Cypress is structurally the wrong place to make it, and every workaround that tries to make it there is a workaround around the wrong thing. ## Why cy.intercept() does not help here `cy.intercept()` is the reflex answer and it is wrong for this case. It sits on the traffic the browser generates. The request that matters — the console's backend calling the mail provider — never leaves the server, so there is no request for a route to match. You can, of course, assert on the request the **page** made: the `POST /environments/staging/suspend` that triggered the flow. That is a legitimate assertion, but it proves the button called the API, not that an email went out. If your suite only ever asserts on that, be honest in the test name about what is covered. ## The two doors, and what each costs | door | where it runs | good when | cost | |---|---|---|---| | `cy.task()` | Node, in the process that loaded the Cypress config | the check needs a client the browser cannot host, or credentials that must stay out of the browser | a handler to write and maintain in `setupNodeEvents`; failures surface in the terminal, not the page | | `cy.request()` | Node, as a plain HTTP call | the application or a test mail service already exposes an inbox endpoint | you depend on that endpoint existing and staying stable | Both run outside the browser, which is exactly the point: no CORS, no same-origin rule, no page to keep alive while you wait. ## Making the wait bounded A message arrives asynchronously, so the check is a poll — and a poll inside a task is the part people get wrong. A task must **end**; Cypress does not support handlers that run indefinitely, and the command fails once `taskTimeout` elapses. 1. Give the poll its **own deadline** inside the handler, shorter than the command timeout, and return `null` or throw a clear error when it expires. 2. Set the command's `timeout` option wider than that deadline — `cy.task('findSuspensionEmail', arg, { timeout: 30000 })` around a 20-second poll — so the failure you read is the handler's ("no message for [email protected] in 20s"), not the runner's generic task timeout. 3. Scope the search: filter by recipient and by a timestamp captured before the action, so a message left by an earlier test cannot satisfy the check. Without step 3 the test passes on someone else's email, which is worse than failing. ## What to return, and what to leave in Node Everything a task returns is serialised, so the payload has to be plain data: - **Return the parsed message** — subject, recipient, a text body, maybe an extracted link — not the provider's response object and never the client. - **Keep the client in module scope** inside `setupNodeEvents` so the connection and its credential live for the run and never enter the payload. - **Return `null`, not `undefined`,** when the handler has nothing to hand back; a handler returning nothing fails the command. - **Convert dates yourself.** A `Date` in the payload arrives in the spec as an ISO string, so decide the representation in the handler rather than discovering it in an assertion. One more thing worth stating plainly: as of Cypress 16 there is no `cy.exec()`. Suites that used to shell out to a mail CLI have to move that call inside a task, spawning the binary with Node's `child_process.execFileSync()` and an argument array. ## When to stop at the seam Having built the door, ask whether this particular check belongs behind it. A browser suite is expensive per test and its failures are read as UI failures. A notification contract is often better covered where the message is produced, with the browser suite asserting only the part the user sees — the confirmation banner and the environment's new state. A reasonable division is: the end-to-end suite proves the button suspends the environment and tells the operator so; a narrower test at the service boundary proves the suspension emits the message. If the team wants one test that spans both, put it behind a task, keep it to one flow rather than every notification, and accept that it will be among the slowest and most fragile tests you own.
- What happens if the Cypress task keeps polling past taskTimeout?The command fails the test with a task timeout and the handler is left running, because Cypress does not support tasks that never end. Give the poll its own shorter deadline so the failure message names the missing message rather than the runner giving up.
- When is a test-only inbox endpoint better than a task that polls a mail API?When the team already exposes one. `cy.request()` then needs no handler in `setupNodeEvents`, no extra credential in the Node process, and the failure reads as an HTTP response. A task is better when the check needs a client the browser cannot host or a secret that must stay in Node.
saying these in an interview costs you the question
- Tries to assert the sent email with cy.intercept()
- Opens a mailbox connection from inside the spec
- Reaches for cy.exec() to run a mail CLI, removed in Cypress 16
- Polls inside a task with no deadline of its own
- Matches any recent message instead of scoping to this test
- Returns the mail client itself from the task handler