cypress-mailslurp

https://github.com/mailslurp/cypress-mailslurp

Table of Contents

scripts/readme.js

/**
 * Readme building script
 *
 * Takes templates/README.tpl.md
 * Reads special comments in `test/*.use.ts` files and extract code blocks
 * ```
 * //<gen>block_name
 * Code here
 * //</gen>
 * ```
 * Replaces `{{block_name}}` in README tpl with the content of the block
 * Writes over <rootDir>/README.md
 */
const fs =require( 'fs');
const { join } =require("path");
const glob =require("fast-glob");
const { diff } = require("jest-diff");
const log = console.log
const commentStart='\/\/<gen>'
const commentEnd='\/\/</gen>'

function minIndent(inp) {
	const match = inp.match(/^[ \t]*(?=\S)/gm);

	if (!match) {
		return 0;
	}

	return match.reduce((r, a) => Math.min(r, a.length), Infinity);
}

function stripIndent(inp) {
	const indent = minIndent(inp);

	if (indent === 0) {
		return inp;
	}

	const regex = new RegExp(`^[ \\t]{${indent}}`, 'gm');

	return inp.replace(regex, '');
}

async function getFileContent(path){
    const content = await fs.promises.readFile(path, { encoding: 'utf-8'} )
    return content.toString()
}

async function checkFile(content) {
    const startCount = (content.match(new RegExp(commentEnd, 'gm')) || []).length
    const endCount = (content.match(new RegExp(commentEnd, 'gm')) || []).length
    if(startCount !== endCount) {
        throw Error(`Expected matching start and end comments ${startCount} ${endCount}`)
    }
}


async function getGenBlocks(content){
    const pKeys = new RegExp(`${commentStart}([0-9a-zA-Z_]*)`, 'g')
    const matchKeys =  [...content.matchAll(pKeys)]
    return [].concat(...matchKeys.map(([_, key]) => {
        const pBlock =  new RegExp(`${commentStart}${key}[\\r\\n]*([\\s\\S]+)${commentEnd}`, 'g')
        log(`Key ${key} match ${pBlock}`)
        const blocks = [...content.matchAll(pBlock)]
        log(`Found ${blocks.length} block for ${key}`)
        return blocks.map(it => {
            log(`Inside ${key} block with ${it.length} params` )
            const [_,body] = it
            return { id: key, body: stripIndent(body.split(commentEnd)[0])}
        })
    }))
}

(async () => {

    // *.use.ts test classes have a special comment -> //<gen>inbox_send ----> //</gen>
    const useCases= await glob([
        join(__dirname,'../cypress/**/*.{ts,js,json}'),
        join(__dirname,'../src/*.{ts,js,json}'),
        join(__dirname,'../cypress.config.ts')
    ])
    log(`SEARCHING ${useCases.join('\n\t')}`)
    const blockMap = {};
    for(const useCase of useCases) {
        log(`Get content for ${useCase}`)
        const content = await getFileContent(useCase)
        log(`Check file ${useCase}`)
        await checkFile(content)
        log(`Generate blocks ${useCase}`)
        const blocks = await getGenBlocks(content)
        log(`${blocks.length} blocks found`)
        for (const block of blocks) {
            log(`Writing block ${block.id}`)
            blockMap[block.id] = block.body
        }
    }

    log("Now get template and join")
    let templateReadme = await getFileContent(join(__dirname, '../templates/README.tpl.md'))
    const variables = new RegExp('\\{\\{([a-zA-Z_]*)\\}\\}', 'g')
    const names = Array.from(new Set(Array.from(templateReadme.matchAll(variables)).map(([_,name]) => name).sort()).keys()).map(it => it.toString())
    log("Found variable names " + names)

    const definedNames = Object.keys(blockMap).sort()

    log(`Check out defined = (${definedNames}) templateNames = (${names})`)
    if (JSON.stringify(definedNames)!=JSON.stringify(names)) {
        throw new Error(`Defined names and template names do not match: `  + diff(definedNames,names))
    }

    for (const [key, value] of Object.entries(blockMap)){
        log(`Replace key in template`)
        templateReadme = templateReadme.replace(new RegExp('{{' + key + '}}', 'g'), (value).replace(/\n+$/, "") )
    }
    log("Check readme")
    if (templateReadme.indexOf('//</gen>') > -1) {
        throw new Error(`README contains an unprocessed end comment //</gen>`)
    }

    log("Finished, write readme")
    await fs.promises.writeFile(join(__dirname, "../README.md"), templateReadme, { encoding: 'utf-8'})


})().catch(err => {
    log(`ERROR: ${err}`, err)
    process.exit(1)
});

cypress/support/e2e.js

import './commands';

cypress/support/commands.js

import '../../dist/index';

cypress/plugins/index.js

/// <reference types="cypress" />
module.exports = (on, config) => {
  // `on` is used to hook into various events Cypress emits
  // `config` is the resolved Cypress config
};

cypress/e2e/javascript-example.cy.js

describe('MailSlurp plugin example', () => {
  it('Can get mailslurp instance and controllers', () => {
    cy.mailslurp().then(mailslurp => {
      // has instance and methods
      expect(mailslurp).to.exist;
      expect(typeof mailslurp.createInbox).to.equal('function');
      // has controllers
      expect(mailslurp.inboxController).to.exist;
    });
  });
  it('Can call mailslurp methods', () => {
    cy.mailslurp()
      .then(mailslurp => mailslurp.createInbox())
      .then(inbox => {
        expect(inbox.emailAddress).to.contain('@mailslurp');
        return inbox;
      });
  });
  context('Share variables', () => {
    before(() => {
      return cy
        .mailslurp()
        .then(mailslurp => mailslurp.createInbox())
        .then(inbox => {
          expect(inbox).to.exist;
          cy.wrap(inbox.id).as('inboxId');
          cy.wrap(inbox.emailAddress).as('emailAddress');
        });
    });
    it('can access inbox using this', function() {
      expect(this.inboxId).to.exist;
      expect(this.emailAddress).to.contain('@mailslurp');
    });
  });
});

tsconfig.json

{
  // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
  "include": ["src", "types", "src/index.d.ts"],
  "compilerOptions": {
    "module": "esnext",
    "lib": ["dom", "esnext"],
    "importHelpers": true,
    // output .d.ts declaration files for consumers
    "declaration": true,
    // output .js.map sourcemap files for consumers
    "sourceMap": true,
    // match output dir to input dir. e.g. dist/index instead of dist/src/index
    "rootDir": "./src",
    // stricter type-checking for stronger correctness. Recommended by TS
    "strict": true,
    // linter checks for common issues
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    // use Node's module resolution algorithm, instead of the legacy TS one
    "moduleResolution": "node",
    // transpile JSX to React.createElement
    "jsx": "react",
    // interop between ESM and CJS modules. Recommended by TS
    "esModuleInterop": true,
    // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
    "skipLibCheck": true,
    // error out if import and file system have a casing mismatch. Recommended by TS
    "forceConsistentCasingInFileNames": true,
    // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
    "noEmit": true,
    "types": ["cypress"]
  }
}

package.json

{
  "name": "cypress-mailslurp",
  "author": "mailslurp",
  "description": "Cypress email and SMS plugin for MailSlurp. Create test email accounts to send and receive emails in Cypress tests. Read SMS and TXT messages too and test against fake mailservers.",
  "version": "1.10.0",
  "license": "MIT",
  "main": "dist/index.js",
  "typings": "dist/index.d.ts",
  "files": [
    "dist",
    "src"
  ],
  "engines": {
    "node": ">=10"
  },
  "scripts": {
    "start": "tsdx watch",
    "build": "tsdx build",
    "lint": "tsdx lint --fix src test cypress",
    "cypress": "cypress run",
    "cypress-open": "cypress open",
    "readme": "node scripts/readme.js"
  },
  "prettier": {
    "printWidth": 80,
    "semi": true,
    "singleQuote": true,
    "trailingComma": "es5"
  },
  "module": "dist/cypress-mailslurp.esm.js",
  "devDependencies": {
    "cypress": "^13.6.2",
    "eslint-plugin-chai-friendly": "^0.6.0",
    "eslint-plugin-cypress": "^2.15.1",
    "fast-glob": "^3.3.2",
    "jest-diff": "^29.5.0",
    "tsdx": "^0.14.1",
    "tslib": "^2.2.0",
    "typescript": "^4.2.4"
  },
  "dependencies": {
    "mailslurp-client": "^15.20.1"
  }
}

cypress/.eslintrc.json

{
  "plugins": [
    "cypress",
    "chai-friendly"
  ],
  "extends": [
    "plugin:chai-friendly/recommended",
    "plugin:cypress/recommended"
  ]
}

cypress/fixtures/example.json

{}

cypress.config.ts

import { defineConfig } from 'cypress'
export default defineConfig({
  // set timeouts so MailSlurp can wait for emails and sms
  defaultCommandTimeout: 30000,
  responseTimeout: 30000,
  requestTimeout: 30000,
  e2e: {
    setupNodeEvents(on, config) {
      return require('./cypress/plugins/index.js')(on, config)
    },
    // examples run against the playground app
    baseUrl: 'https://playground.mailslurp.com',
    // these examples require no test isolation
    testIsolation: false
  },
})

src/index.ts

/// <reference types="./">
import {Config, MailSlurp} from "mailslurp-client";
function register(Cypress: Cypress.Cypress) {
    Cypress.Commands.add('mailslurp' as any, ((config?: Config) => {
        // read the API Key from environment variable (see the API Key section of README)
        const apiKey = config?.apiKey ?? Cypress.env('MAILSLURP_API_KEY');
        if (!apiKey) {
            throw new Error(
                'Error no MailSlurp API Key. Please either pass the mailslurp command a valid Config object or set the `CYPRESS_MAILSLURP_API_KEY` ' +
                'environment variable to the value of your MailSlurp API Key to use the MailSlurp Cypress plugin. ' +
                'Create a free account at https://app.mailslurp.com/sign-up/. See https://docs.cypress.io/guides/guides/environment-variables#Option-3-CYPRESS_ for more information.'
            );
        }
        const mailslurp = new MailSlurp({ ...config, apiKey, basePath: 'https://cypress.api.mailslurp.com' });
        return Promise.resolve(mailslurp);
    }) as any);
}
register(Cypress);

src/index.d.ts

/// <reference types="cypress" />
import { MailSlurp, Config } from "mailslurp-client";

declare global {
    namespace Cypress {
        interface Chainable {
            mailslurp: (config?: Config) => Promise<MailSlurp>;
        }
    }
}

cypress/e2e/short-test.cy.ts

/// <reference types="cypress" />
/// <reference types="../../src" />
//<gen>cy_plugin_test_usage
describe('sign up using disposable email', function () {
    it('can set config', () => {
        //<gen>cy_config_dynamic
        cy.mailslurp({ apiKey: 'YOUR_KEY' })
        //</gen>
    })
    it('can set config and use', () => {
        cy.mailslurp({ apiKey: Cypress.env("MAILSLURP_API_KEY") })
            .then((mailslurp) => {
                cy.then(() => mailslurp.createInbox())
                    .then(inbox => {
                        expect(inbox).to.exist
                    })
            })
    })
    //<gen>cy_example_short
    it('can sign up using throwaway mailbox', function () {
        // create a mailslurp instance
        cy.mailslurp().then(function (mailslurp) {
            // visit the demo application
            cy.visit('/');
            // create an email address and store it on this
            cy.then(() => mailslurp.createInbox())
                .then((inbox) => {
                    // save inbox id and email address to this
                    cy.wrap(inbox.id).as('inboxId');
                    cy.wrap(inbox.emailAddress).as('emailAddress');
                })
            // fill user details on app
            cy.get('[data-test=sign-in-create-account-link]').click()
            cy.then(function () {
                // access stored email on this, make sure you use Function and not () => {} syntax for correct scope
                cy.get('[name=email]').type(this.emailAddress)
                cy.get('[name=password]').type('test-password')
                return cy.get('[data-test=sign-up-create-account-button]').click();
            })
            // now wait for confirmation mail
            cy.then({
                // add timeout to the step to allow email to arrive
                timeout: 60_000
            }, function () {
                return mailslurp
                    // wait for the email to arrive in the inbox
                    .waitForLatestEmail(this.inboxId, 60_000, true)
                    // extract the code with a pattern
                    .then(email => mailslurp.emailController.getEmailContentMatch({
                        emailId: email.id,
                        contentMatchOptions: {
                            // regex pattern to extract verification code
                            pattern: 'Your Demo verification code is ([0-9]{6})'
                        }
                    }))
                    // save the verification code to this
                    .then(({matches}) => cy.wrap(matches[1]).as('verificationCode'))
            });
            // confirm the user with the verification code
            cy.then(function () {
                cy.get('[name=code]').type(this.verificationCode)
                cy.get('[data-test=confirm-sign-up-confirm-button]').click()
                // use the email address and a test password
                cy.get('[data-test=username-input]').type(this.emailAddress)
                cy.get('[data-test=sign-in-password-input]').type('test-password')
                // click the submit button
                return cy.get('[data-test=sign-in-sign-in-button]').click();
            })
            cy.get('h1').should('contain', 'Welcome');
        });
    });
    //</gen>
});

cypress/e2e/methods-test.cy.ts

/// <reference types="cypress" />
/// <reference types="../../src" />
import {MailSlurp} from "mailslurp-client";

describe('methods', function () {
  it('can call common methods', async function () {
      cy.log("Creating inbox")
    //<gen>cy_plugin_create_inbox
    await cy.mailslurp()
        .then((mailslurp: MailSlurp) => mailslurp.createInboxWithOptions({}))
        .then(inbox => {
          expect(inbox.emailAddress).to.contain("@mailslurp")
          // save the inbox values for access in other tests
          cy.wrap(inbox.id).as('inboxId')
          cy.wrap(inbox.emailAddress).as('emailAddress')
        })
    //</gen>
      cy.log("Sending email")
    //<gen>cy_plugin_send_email
    await cy.mailslurp()
        .then((mailslurp: MailSlurp) => mailslurp.sendEmail(this.inboxId, {
          to: [this.emailAddress  ],
          subject: 'Email confirmation',
          body: 'Your code is: ABC-123',
        }))
    //</gen>
    //<gen>cy_plugin_wait
      cy.log("Waiting for email")
      await cy.mailslurp().then({
          // set a long timeout when waiting for an email to arrive
          timeout: 60_000,
      }, (mailslurp: MailSlurp) => mailslurp.waitForLatestEmail(this.inboxId, 60_000, true))
          .then(email => {
              expect(email.subject).toContain('Email confirmation')
              const code = email.body.match(/Your code is: (\w+-\d+)/)[1]
              expect(code).toEqual('ABC-1223')
          })
      //</gen>
  })
});

cypress/e2e/integration-test.cy.ts

/// <reference types="cypress" />
/// <reference types="../../src" />
//<gen>cy_plugin_test_usage
describe('basic usage', function () {
  it('can load the plugin', async function () {
    // test we can connect to mailslurp
    const mailslurp = await cy.mailslurp();
    const userInfo = await mailslurp.userController.getUserInfo();
    expect(userInfo.id).to.exist
  })
});
describe('store values', function () {
  //<gen>cy_store_values
  before(function() {
    return cy
        .mailslurp()
        .then(mailslurp => mailslurp.createInbox())
        .then(inbox => {
          // save inbox id and email address to this (make sure you use function and not arrow syntax)
          cy.wrap(inbox.id).as('inboxId');
          cy.wrap(inbox.emailAddress).as('emailAddress');
        });
  });
  it('can access values on this', function() {
    // get wrapped email address and assert contains a mailslurp email address
    expect(this.emailAddress).to.contain('@mailslurp');
  });
  //</gen>
})
//</gen>
//<gen>cy_example_test
describe('user sign up test with mailslurp plugin', function() {
  // use cypress-mailslurp plugin to create an email address before test
  before(function() {
    return cy
      .mailslurp()
      .then(mailslurp => mailslurp.createInbox())
      .then(inbox => {
        // save inbox id and email address to this (make sure you use function and not arrow syntax)
        cy.wrap(inbox.id).as('inboxId');
        cy.wrap(inbox.emailAddress).as('emailAddress');
      });
  });
  it('01 - can load the demo application', function() {
    // get wrapped email address and assert contains a mailslurp email address
    expect(this.emailAddress).to.contain('@mailslurp');
    // visit the demo application
    cy.visit('/');
    cy.title().should('contain', 'React App');
  });
  // use function instead of arrow syntax to access aliased values on this
  it('02 - can sign up using email address', function() {
    // click sign up and fill out the form
    cy.get('[data-test=sign-in-create-account-link]').click();
    // use the email address and a test password
    cy.get('[name=email]')
      .type(this.emailAddress)
      .trigger('change');
    cy.get('[name=password]')
      .type('test-password')
      .trigger('change');
    // click the submit button
    cy.get('[data-test=sign-up-create-account-button]').click();
  });
  it('03 - can receive confirmation code by email', function() {
    // app will send user an email containing a code, use mailslurp to wait for the latest email
    cy.mailslurp()
      // use inbox id and a timeout of 30 seconds
      .then(mailslurp =>
        mailslurp.waitForLatestEmail(this.inboxId, 30000, true)
      )
      // extract the confirmation code from the email body
      .then(email => /.*verification code is (\d{6}).*/.exec(email.body!!)!![1])
      // fill out the confirmation form and submit
      .then(code => {
        cy.get('[name=code]')
          .type(code)
          .trigger('change');
        cy.get('[data-test=confirm-sign-up-confirm-button]').click();
      });
  });
  // fill out sign in form
  it('04 - can sign in with confirmed account', function() {
    // use the email address and a test password
    cy.get('[data-test=username-input]')
      .type(this.emailAddress)
      .trigger('change');
    cy.get('[data-test=sign-in-password-input]')
      .type('test-password')
      .trigger('change');
    // click the submit button
    cy.get('[data-test=sign-in-sign-in-button]').click();
  });
  // can see authorized welcome screen
  it('05 - can see welcome screen', function() {
    // click sign up and fill out the form
    cy.get('h1').should('contain', 'Welcome');
  });
});
//</gen>