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

Supercharge Your Cypress Tests With Plugins

By Gleb Bahmutov

Supercharge Your Cypress Tests With Plugins

Do you like writing End-to-End tests using Cypress? What if I told you that you can unlock the full power of Cypress by ... writing JavaScript code to extend the built-in Test Runner features? Be able to add more testing features, write shorter tests, have more power at your disposal? Most people only scratch the surface of the web application tests, but if you learn how to use the existing plugins (and there are lots of them!) and write your own can take your tests to the next level.

  • 2,140