Testing emails from meteor with cypress

Im using cypress for behaviour testing my meteor 3 app. One of those tests checks the content of the registration email, ‘clicks’ the registration link and sets the new users password, thus checking the new user storyboard. I thought I might share some pointers on how to do this in case its useful.

The relevant section of the test looks like:

    cy.get("input[name='username']").type('ella')
    cy.get("input[name='email']").type('ella@test.com')
    cy.get("input[name='firstName']").type('Ella')
    cy.get("input[name='lastName']").type('User')
    cy.get('button').contains('OK').click()
    cy.wait(500) // allow some time for email to register
    cy.get('#account-dropdown').click()
    cy.get('a.dropdown-item').contains('Sign out').click()
    // capture contents of enrollment email and then visit the link contained
    cy.task('getLastEmail', 'ella@test.com').then((email) => {
      const url = email.body.match(/http:\/\/.*/g)[0]
      cy.visit(url)
      cy.get("input[name='newpassword1']").type('password')
      cy.get("input[name='newpassword2']").type('password')
      cy.get('button').contains('Set Password').click()
      cy.contains('Hello Ella')
    })

Where the user has previously authenticated as a user that has the authority to add a new users. The important bit is the getLastEmail cypress task which is defined in the cypress.config.js file:

const { defineConfig } = require('cypress')
const ms = require('smtp-tester')

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on) {
      // see https://www.cypress.io/blog/testing-html-emails-using-cypress for useful background

      // starts the SMTP server at localhost:7777
      const port = 7777
      const mailServer = ms.init(port)
      console.log('mail server at port %d', port)

      // [receiver email]: email text
      let lastEmail = {}

      const handler = (addr, id, email) => {
        // store the email by the receiver email
        lastEmail[email.headers.to] = {
          body: email.body,
          html: email.html,
        }
      }

      // process all emails
      mailServer.bind(handler)

      on('task', {
        getLastEmail(userEmail) {
          // cy.task cannot return undefined
          // thus we return null as a fallback
          return lastEmail[userEmail] || null
        },
      })

      on('after:run', () => {
        mailServer.unbind(handler)
        mailServer.stop()
      })
    },
    baseUrl: 'http://localhost:3000'
  },
...

Hopefully this might be useful to others trying to do the same thing. And please do tell me if you see something dumb.

2 Likes

Looks very useful! I’m guessing if there’s any Meteor settings applied for the email package that it’s using the same connection details as the fake SMTP server?

You can use MAIL_URL, i.e. MAIL_URL=smtp://localhost:7777. The full script I use in my package.json which uses the start-server-and-test is:

    "test:e2e": "meteor reset --db && MAIL_URL=smtp://localhost:7777 start-server-and-test meteor http://localhost:3000 'meteor npx cypress run'",

I was wondering the same. Just tried it on my end using the same MAIL_URL approach, and it hooked into the fake SMTP server without issues. Makes testing those flows way more manageable. Curious if anyone’s tried this with other testing setups besides Cypress?