ScreenshotNeo

BlogHow-to

How to Test HTML Emails With Cypress

Capture test emails through a local SMTP server or inbox API, then assert on their content, links, and rendered behavior in Cypress.

By the ScreenshotNeo team4 October 20268 min read

Test HTML emails in Cypress by capturing the message through a local SMTP server or a test-inbox API, then asserting on its headers and body. To check what a reader sees or whether a link works, load the captured HTML into the Cypress browser and test it there. Avoid logging into a consumer mailbox website: Cypress calls that an anti-pattern and recommends using an API or talking directly to your server. Cypress FAQ: checking email.

Use local SMTP capture when your test application can send mail to a local test server. Use a hosted inbox API when the app sends through an external provider or cannot redirect SMTP. In either case, trigger the email from the Cypress test, retrieve the matching message programmatically, and wait for it to arrive instead of relying on a fixed sleep.

Choose an email capture route

Route Use it when Tradeoff
Local SMTP capture The application can send to a temporary SMTP server in the test environment. Local control, with setup for the server, message storage, and Cypress task handoff.
Hosted test inbox API The app uses a third-party email provider or cannot be redirected to local SMTP. Simple API retrieval, with an external service dependency and credentials to manage.
Temporary email provider or plugin A disposable address or an existing provider integration suits the project. Check maintenance, data handling, and compatibility with your Cypress version. Cypress lists email integrations as community extensions, not endorsements. Cypress plugin directory.

Choose based on where mail goes in the test environment, what the test must prove, and whether you accept an external inbox dependency. For either route, use a unique recipient or clear captured messages between tests so an old email cannot satisfy a new assertion.

Hosted inbox example with Mailosaur

Mailosaur documents a Cypress integration that sends a test message to its inbox, searches for the message, and exposes fields including the HTML body. Its guide describes searching by recipient, sender, subject, or body; the message lookup waits for delivery. Confirm package compatibility with your Cypress version before adopting the vendor’s setup.

1. Install and configure the integration

Follow the Mailosaur Cypress quickstart to install cypress-mailosaur and import it from Cypress support setup. Provide the API key through the documented CYPRESS_MAILOSAUR_API_KEY environment variable. Do not commit the key to source control.

# Install the integration using the command in the Mailosaur quickstart.
# Set the key in your shell or CI secret store; do not put it in the repository.
export CYPRESS_MAILOSAUR_API_KEY="your-api-key"

The quickstart also documents an npm starter project command. Follow its current instructions for your project structure and Cypress configuration; Cypress plugin file conventions have changed over time.

2. Trigger the email and assert its contents

Use a server ID and test address from your Mailosaur account. The example below assumes your application has a registration flow that sends a confirmation message. Replace the example selectors, route, server ID, and expected copy with those in your app.

describe('registration email', () => {
  it('sends a confirmation message with working HTML content', () => {
    const serverId = Cypress.env('MAILOSAUR_SERVER_ID');
    const recipient = `cypress-${Date.now()}@${serverId}.mailosaur.net`;

    cy.visit('/register');
    cy.get('[name="email"]').type(recipient);
    cy.get('[name="password"]').type('example-password');
    cy.get('form').submit();

    cy.mailosaurGetMessage(serverId, {
      sentTo: recipient,
      subject: 'Confirm your registration'
    }).then((message) => {
      expect(message.to[0].email).to.equal(recipient);
      expect(message.subject).to.contain('Confirm');
      expect(message.html.body).to.contain('Confirm your email');
      expect(message.text.body).to.contain('Confirm your email');

      const confirmationLink = message.html.links.find((link) =>
        link.text.includes('Confirm')
      );
      expect(confirmationLink, 'confirmation link').to.exist;
      expect(confirmationLink.href).to.include('/confirm');
    });
  });
});

The exact message fields and search options are described in the Mailosaur Cypress email testing guide. Adapt assertions to the shape returned by the installed integration version. A unique recipient makes the search specific and avoids matching a previous run.

Checking the source string confirms that expected markup or copy exists. It does not prove how the template behaves in a browser. To test visible content and a link, write the captured HTML into the Cypress document, then interact with the rendered DOM. This tests browser rendering; it does not certify identical appearance across Gmail, Outlook, or other mail clients.

cy.mailosaurGetMessage(serverId, {
  sentTo: recipient,
  subject: 'Confirm your registration'
}).then((message) => {
  const html = message.html.body;
  expect(html).to.contain('Confirm your email');

  cy.document().then((document) => {
    document.open();
    document.write(html);
    document.close();
  });

  cy.contains('a', 'Confirm your email')
    .should('be.visible')
    .and('have.attr', 'href')
    .then((href) => {
      expect(href).to.be.a('string').and.not.be.empty;
      cy.visit(href);
    });

  cy.location('pathname').should('include', '/confirm');
});

If the confirmation URL is absolute and points to the app under test, visiting it directly checks the destination route. If the email link contains a token, assert that it is present and exercise the actual confirmation flow. Avoid logging tokens or full message contents in CI output.

Local SMTP capture pattern

When the application can send to local SMTP, run a temporary SMTP capture server as part of the Cypress Node-side setup. Store each received message, including recipient, headers, plain-text body, and HTML body. Register tasks such as resetEmails and getLastEmail; the test clears the store before triggering the application, then retrieves the matching message after sending.

  1. Configure the test application’s SMTP host and port to point to the capture server.
  2. Start the server in Cypress’s Node-side event setup and retain messages in memory or a test-local store.
  3. Register tasks to clear messages and retrieve one by recipient. Return serializable data from tasks.
  4. In the spec, generate a fresh recipient, clear prior state, submit the form, and poll retrieval until a matching message arrives or the test timeout expires.
  5. Assert headers, text and HTML bodies, then render the HTML in the browser if interaction or visible content matters.
  6. Stop the server during Cypress shutdown so it does not leak across runs.

The Cypress tutorial demonstrates this approach by capturing both plain-text and HTML bodies and exposing them to tests through tasks. Its example assumes the message has arrived by the time retrieval runs; if that is flaky, retry the task until a message appears or a reasonable timeout is reached. Avoid using an arbitrary sleep as the main synchronization mechanism. The tutorial is dated May 11, 2021 and uses older plugin conventions, so adapt the architecture to your current Cypress configuration. Cypress tutorial: testing HTML emails.

What to assert

  • Delivery and routing: the intended recipient, sender address or display name, and subject.
  • Plain-text body: expected wording, verification code, and useful fallback content.
  • HTML body: key copy, CTA markup, and a non-empty destination URL.
  • Behavior: the relevant link reaches the expected route or state when exercised.
  • Rendering and accessibility: inspect relevant viewport sizes and consider accessibility and visual checks. Browser rendering of the HTML alone does not establish rendering fidelity in every email client.

If the application sends both HTML and text alternatives, assert both. Keep assertions focused on user-visible behavior and important structure rather than brittle snapshots of the entire generated email.

Reliability, performance, and cost

  • Wait for the message: email delivery is asynchronous. Prefer an API helper that waits or a retry loop with a bounded timeout. Do not treat a fixed delay as proof of delivery.
  • Isolate tests: use a fresh recipient per test or clear local capture state. Match more than one field, such as recipient and subject, when searching an inbox.
  • Keep secrets out of source: place hosted inbox API keys in environment variables or CI secrets. Do not print credentials or sensitive email content in logs.
  • Control external dependencies: a hosted inbox introduces network and service availability into the test. Keep local SMTP tests for fast, repeatable coverage where practical, and reserve provider-backed checks for flows that need them.
  • Keep messages small: retrieve only the message needed for the assertion and avoid repeated broad searches. Render HTML only when the test needs browser behavior.
  • Account for cost: local capture avoids a hosted inbox dependency, while hosted providers may require a service plan. Check the provider’s current pricing and limits; the research sources do not establish a price.

Troubleshooting

Symptom Likely cause Fix
No message found The app is still sending, the test recipient differs, or the app is pointed at another mail transport. Verify test SMTP/provider configuration and recipient. Use a delivery-aware lookup or bounded retry rather than a fixed sleep.
A prior message satisfies the test The inbox search is broad or a local store was not cleared. Use a unique recipient per test, clear local messages before the action, and match the subject or other identifying fields.
HTML assertion fails but email arrived The app sent only text, the expected wording changed, or the returned body shape differs from the installed client version. Inspect the captured message fields, assert the intended copy, and check whether the application is expected to send an HTML alternative.
Link is missing or points to the wrong host The template’s base URL or test environment configuration is incorrect. Assert the link destination and configure the email template’s public application URL for the test environment.
Rendered content is blank The document was not replaced with the captured HTML, or the email HTML relies on remote assets or scripts. Write the HTML into the Cypress document and assert a stable text node. Test essential email content without depending on remote images loading.
Mailosaur authentication or setup fails The API key or server ID is missing, incorrect, or configured under a different environment variable. Follow the current quickstart, confirm the environment variable is available to Cypress, and keep the key outside source control.
Local capture task returns no data The SMTP server has not received mail before the task runs, or task state is not shared as expected. Retry retrieval until the message appears or a bounded timeout expires; ensure the SMTP server and task share the intended Node-side store.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. It can capture a rendered page as PNG, JPEG, WebP, or PDF; for an email preview, point it at a browser-accessible page that renders the email template. It is for capturing a page, not for receiving or asserting on email delivery.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month, with no card.

FAQ

How do I check that an email was sent out?

Trigger the workflow in Cypress, then retrieve the message through a local capture task or test-inbox API and assert on its recipient, subject, and content.

Should Cypress log into Gmail or another mailbox UI?

No. Cypress identifies mailbox UI checks as an anti-pattern; programmatic access through an API or server-side capture is more suitable for automated tests.

Does checking the HTML in Cypress prove it looks right in every email client?

No. It checks the HTML and its behavior in the test browser. Client-specific rendering needs separate checks in the email clients and viewports that matter to your product.

Can I test plain-text email too?

Yes. Capture and assert on the text alternative as well as HTML when the application sends both.