Supercharge Your Cypress Tests With Plugins

TDC FUTURE

TECHNOLOGY CREATING TOMORROW

Gleb Bahmutov

Sr Director of Engineering

our planet is in imminent danger

survival is possible* but we need to act now

  • change your life
  • join an organization

Speaker: Gleb Bahmutov PhD

C / C++ / C# / Java / CoffeeScript / JavaScript / Node / Angular / Vue / Cycle.js / functional programming / testing

Gleb Bahmutov

Sr Director of Engineering

50 people. Atlanta, Philly, Boston, NYC, the world

Fast, easy and reliable testing for anything that runs in a browser

AGENDA

  1. Why plugins?
  2. The many types of plugins
  3. My (favorite) plugins
    1. cypress-each
    2. cypress-watch-and-reload
    3. cypress-grep
    4. cypress-recurse
    5. cypress-data-session
  4. The future
$ npm install -D cypress
// ui-spec.js
it('loads the app', () => {
  cy.visit('http://localhost:3000')
  cy.get('.todoapp').should('be.visible')
})

Mocha BDD syntax

Chai assertions

it('adds 2 todos', () => {
  cy.visit('http://localhost:3000')
  cy.get('.new-todo')
    .type('learn testing{enter}')
    .type('be cool{enter}')
  cy.get('.todo-list li')
    .should('have.length', 2)
})
$ npx cypress open

Cypress tests run in the browser and Cypress can interact with the OS via its plugin file that runs in the Node process

describe('intercept', () => {
  it('returns different fruits every 30 seconds', () => {
    cy.clock()

    // return difference responses on each call
    // notice the order of the intercepts
    cy.intercept('/favorite-fruits', ['kiwi 🥝']) // 3rd, 4th, etc
    cy.intercept('/favorite-fruits', { times: 1 }, ['grapes 🍇']) // 2nd
    cy.intercept('/favorite-fruits', { times: 1 }, ['apples 🍎']) // 1st

    cy.visit('/fruits.html')
    cy.contains('apples 🍎')
    cy.tick(30000)
    cy.contains('grapes 🍇')
    // after using the first two intercepts
    // forever reply with "kiwi" stub
    cy.tick(30000)
    cy.contains('kiwi 🥝')
    cy.tick(30000)
    cy.contains('kiwi 🥝')
    cy.tick(30000)
    cy.contains('kiwi 🥝')
  })
})

Cypress Power

controls app clock and mocks network

Cypress Power

describe('intercept', () => {
  it('returns different fruits every 30 seconds', () => {
    cy.clock()

    // return difference responses on each call
    // notice the order of the intercepts
    cy.intercept('/favorite-fruits', ['kiwi 🥝']) // 3rd, 4th, etc
    cy.intercept('/favorite-fruits', { times: 1 }, ['grapes 🍇']) // 2nd
    cy.intercept('/favorite-fruits', { times: 1 }, ['apples 🍎']) // 1st

    cy.visit('/fruits.html')
    cy.contains('apples 🍎')
    cy.tick(30000)
    cy.contains('grapes 🍇')
    // after using the first two intercepts
    // forever reply with "kiwi" stub
    cy.tick(30000)
    cy.contains('kiwi 🥝')
    cy.tick(30000)
    cy.contains('kiwi 🥝')
    cy.tick(30000)
    cy.contains('kiwi 🥝')
  })
})

controls app clock and mocks network

Cypress is a complete testing solution

  • The test runner
  • Built-in Electron browser
  • Node interface via plugin file
  • Chai assertions
  • Chai-jQuery & Sinon-Chai assertions
  • Clock control
  • Network control
  • Cypress._ (Lodash), Cypress.Promise, Cypress.$, Cypress.Blob, Cypress.minimatch

Want Ramda?

$ npm i -D ramda
# or
$ yarn add -D ramda
import * as R from 'ramda'

Cypress.R = R

cypress/support/index.js

it('works with Ramda', () => {
  cy.wrap(Cypress.R.range(1, 5))
    .should('deep.equal', [1, 2, 3, 4])
})

cypress/integration/spec.js

Plugin Badges

Plugin written by the Cypress team

Plugin is written by a Cypress user outside the core team (most common)

Outside plugin, but reviewed by the Cypress team

Trying to prototype a feature by using a plugin first

Example:  Custom Command Plugin

<form>
  <label for="fname">First name:</label><br />
  <input type="text" id="fname" name="fname" /><br />
  <label for="lname">Last name:</label><br />
  <input type="text" id="lname" name="lname" />
</form>

Select the <input> element by its label's text

Example:  Custom Command Plugin

const getInputByLabel = (label) => {
  return cy
    .contains('label', label)
    .invoke('attr', 'for')
    .then((id) => {
      // no need to even return the `cy.get`
      // Cypress automatically yields it
      cy.get('#' + id)
    })
}

Example:  Custom Command Plugin

const getInputByLabel = (label) => {
  return cy
    .contains('label', label)
    .invoke('attr', 'for')
    .then((id) => {
      // no need to even return the `cy.get`
      // Cypress automatically yields it
      cy.get('#' + id)
    })
}
getInputByLabel('Last name:').type('Smith')

Example:  Custom Command Plugin

Cypress.Commands.add('getByLabel', (label) => {
  cy.log('**getByLabel**')
  cy.contains('label', label)
    .invoke('attr', 'for')
    .then((id) => {
      cy.get('#' + id)
    })
})
cy.getByLabel('Last name:').type('Smith')

Example:  Custom Command Plugin

// load type definitions that come with Cypress module
/// <reference types="cypress" />
declare namespace Cypress {
  interface Chainable {
    /**
     * Finds a form element using the label's text
     * @param label string
     * @example
     *  cy.getByLabel('First name:').type('Joe')
     */
    getByLabel(label: string): 
      Chainable<JQuery<HTMLElement>>
  }
}

add types in src/index.d.ts

Publish To NPM

const registerCommand = (name = 'getByLabel') => {
  const getByCommand = (label) => {
    cy.log(`**${name}**`)
    cy.contains('label', label)
      .invoke('attr', 'for')
      .then((id) => {
        cy.get('#' + id)
      })
  }

  Cypress.Commands.add(name, getByCommand)
}

module.exports = {
  registerCommand,
}

Publish To NPM

Publish To NPM

$ npm i -D cypress-get-by-label
# or
$ yarn add -D cypress-get-by-label
const { registerCommand } = require('cypress-get-by-label')
registerCommand()
cy.getByLabel('First name')

// or we could register under a different name
registerCommand('getFormField') 
cy.getFormField('Your age')

cypress/support/index.js

Publish To NPM

$ npm i -D cypress-get-by-label
# or
$ yarn add -D cypress-get-by-label

cypress-testing-library, cypress-file-upload, cypress-axe, cypress-drag-drop, cypress-xpath, @bahmutov/cy-api, etc

const { registerCommand } = require('cypress-get-by-label')
registerCommand()
cy.getByLabel('First name')

// or we could register under a different name
registerCommand('getFormField') 
cy.getFormField('Your age')

cypress/support/index.js

cypress-real-events

import 'cypress-real-events/support'

it('over the theme switcher', () => {
  cy.visit('/')
  cy.get('[data-cy=add-todo]').type('text title')
  cy.get('#theme-switcher').realHover()
})

cypress-real-events

import 'cypress-real-events/support'

it('over the theme switcher', () => {
  cy.visit('/')
  cy.get('[data-cy=add-todo]').type('text title')
  cy.get('#theme-switcher').realHover()
})
  • Adding new commands

  • Adding new features to the test engine Mocha

  • Overwriting Mocha features

Add New Things

it('is visible', () => {
  cy.get('header').should('be.visible')
  cy.get('main').should('be.visible')
  cy.get('footer').should('be.visible')
})
it('is visible', () => {
  const selectors = ['header', 'main', 'footer']
  selectors.forEach(selector => {
    cy.get(selector).should('be.visible')
  })
})

It would be nice to create a separate test for each selector...

it.each = (items, title, cb) => {
  items.forEach(item => {
    it(title, function () {
      return cb.call(this, item)
    })
  })
}
const selectors = ['header', 'main', 'footer']

it.each(selectors, 'is visible', (selector) => {
  cy.get(selector).should('be.visible')
})

Add New Things

// create a separate test for each selector
const selectors = ['header', 'footer', '.new-todo']
it.each(selectors)('element %s is visible', (selector) => {
  cy.visit('/')
  cy.get(selector).should('be.visible')
})
// creates tests
// "element header is visible"
// "element footer is visible"
// "element .new-todo is visible"
const data = [
  // each entry is an array [selector, assertion]
  ['header', 'be.visible'],
  ['footer', 'exist']
  ['.new-todo', 'not.be.visible']
]
it.each(data)('element %s should %s', (selector, assertion) => {
  cy.visit('/')
  cy.get(selector).should(assertion)
})
// creates tests
// "element header should be.visible"
// "element footer should exist"
// "element .new-todo should not.be.visible"
// repeat the same test 5 times
it.each(5)('test %K of 5', function (k) {
  // note the iteration index k is passed to each test
  expect(k).to.be.within(0, 4)
})

Repeat the test N times

const items = [1, 2, 3, 4, 5, 6, ...]
it.each(items, 3)(...)
// tests item 1, 4, 7, ...

Filter, take every Nth item

// split all items among 3 specs
// spec-a.js
it.each(items, 3, 0)(...)
// spec-b.js
it.each(items, 3, 1)(...)
// spec-c.js
it.each(items, 3, 2)(...)

Split into N chunks, test one of them

// pick 2 random items from the array and create 2 tests
it.each(Cypress._.sampleSize(items, 2))(...)

Pick N samples from all items

Add New Things To GUI

Re-run the test when the application files  change

Overwrite Existing Things

it('works', () => {
  ...
})

it('loads', () => {
  ...
})

it('saves', () => {
  ...
})

It would be nice to run just the "it loads" test...

Overwrite Existing Things

const _it = it
const grep = Cypress.env('grep')
it = (title, cb) => {
  if (title.includes(grep)) {
    _it(title, cb)
  } else {
    _it.skip(title, cb)
  }
}
cypress run --env grep=loads
# or
CYPRESS_grep=loads cypress run

Pass plugin config via Env

require('cypress-grep')() // overwrites "it"

it('works', () => {
  ...
})
it('loads', {tags: '@smoke'}, () => {
  ...
})
it('saves', () => {
  ...
})
$ cypress run --env grep=loads
# run all tests tagged "@smoke"
$ cypress run --env grepTags=@smoke
# run all tests NOT tagged "@smoke"
$ cypress run --env grepTags=-@smoke
# run all tests tagged "@smoke" and "@fast"
$ cypress run --env grepTags=@smoke+@fast
# run the test 5 times
$ cypress run --env grep=loads,burn=5
# omit filtered tests
$ cypress run --env grep=loads,grepOmitFiltered=true
# omit specs without filtered tests
$ cypress run --env grep=loads,grepFilterSpecs=true
# run tests without any tags
$ cypress run --env grepUntagged=true
// run filtered tests 100 times
Cypress.grep('hello world', null, 100)

Grep tests from DevTools

AGENDA

  1. Why plugins?
  2. The many types of plugins
  3. My (favorite) plugins
    1. cypress-each
    2. cypress-watch-and-reload
    3. cypress-grep
    4. cypress-recurse
    5. cypress-data-session
  4. The future

Q: How do I click on the Next button until I get to the last page?

  1. get the Next button
  2. if it is disabled
    1. stop (the last page)
    2. else click on it
  3. go to step 1
import {recurse} from 'cypress-recurse'
recurse(
  () => cy.get('[value=next]'),
  ($button) => $button.attr('disabled') === 'disabled',
  {
    log: 'Last page',
    delay: 500,
    post() {
      cy.get('[value=next]').click()
    },
  },
)
  1. get the Next button
  2. if it is disabled
    1. stop (the last page)
    2. else click on it
  3. go to step 1
import {recurse} from 'cypress-recurse'
recurse(
  () => cy.get('[value=next]'),
  ($button) => $button.attr('disabled') === 'disabled',
  {
    log: 'Last page',
    delay: 500,
    post() {
      cy.get('[value=next]').click()
    },
  },
)
  • Reload the page until the text appears
  • Scroll the page until the text is found
  • Check the list until it is sorted
  • Call cy.request until it succeeds
  • Call cy.task until it yields expected result
  • Visual screenshot until it matches
  • Type into a flaky input box
  • Do X until Y...

Data Creation Problem

function registerUser(username, password) {
  ...
}
function loginUser(username, password) {
  ...
}
it('registers user', () => {
  const username = 'Test'
  const password = 'MySecreT'

  registerUser(username, password)
  loginUser(username, password)

  cy.location('pathname')
    .should('equal', '/rooms')
})

Data Creation Problem

function registerUser(username, password) {
  ...
}
function loginUser(username, password) {
  ...
}
it('registers user', () => {
  const username = 'Test'
  const password = 'MySecreT'

  registerUser(username, password)
  loginUser(username, password)

  cy.location('pathname')
    .should('equal', '/rooms')
})

Run Again ...

Data Creation Problem

  1. Do not create the data if it exists
  2. Start using the existent cached data
registerUser(username, password)
loginUser(username, password)

Before

cy.dataSession({
  name: 'user',
  setup() {
    registerUser(username, password)
  },
  validate: true,
})
loginUser(username, password)

After

Runs the test twice. The same user is re-used

Look up any stored data session from the DevTools console

  • Cypress.getDataSession(name)
  • Cypress.getDataSessionDetails(name)
  • Cypress.clearDataSession(name)
  • Cypress.clearDataSessions()
  • Cypress.dataSessions(enable)
  • Cypress.setDataSession(name, value)
  • Cypress.formDataSessionKey(name)
cy.dataSession({
  name: 'user',
  setup() {
    registerUser(username, password)
  },
  validate: true,
})
loginUser(username, password)

What if the user exists in the database?

cy.dataSession({
  name: 'user',
  init() {
    cy.task('findUser', username)
  },
  setup() {
    registerUser(username, password)
  },
  validate: true,
})
loginUser(username, password)

What if the user exists in the database?

Find the user and cache in memory

The user already is in the database,

and we just opened Cypress

Re-run Cypress and cached user is used

cy.dataSession({
  name: 'user',
  init() {
    cy.task('findUser', username)
  },
  setup() {
    registerUser(username, password)
  },
  validate: true,
})
loginUser(username, password)

Can we "cache" the login session?

cy.dataSession({
  name: 'user',
  init() {
    cy.task('findUser', username)
  },
  setup() {
    registerUser(username, password)
  },
  validate: true,
})
cy.dataSession({
  name: 'logged in',
  setup() {
    loginUser(username, password)
    cy.getCookie('connect.sid')
  },
  validate: true,
  recreate(cookie) {
    cy.setCookie('connect.sid', cookie.value)
    cy.visit('/rooms')
  },
  dependsOn: ['user'],
})

From 4.5 seconds to 240 ms

Cypress is extendable

  1. cypress-get-by-label
  2. cypress-real-events
  3. cypress-each
  4. cypress-watch-and-reload
  5. cypress-grep
  6. cypress-recurse
  7. cypress-data-session

But there are lots lots lots more!

Thank you 👏

TDC FUTURE

TECHNOLOGY CREATING TOMORROW

@bahmutov

Gleb Bahmutov

Sr Director of Engineering