My Favorite Cypress Plugins

LambdaTest

Voices of Community

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

Lots Of Testing

AGENDA

  1. Don't use plugins
  2. The many types of plugins
  3. My (favorite) plugins
    1. cypress-map
    2. cypress-each
    3. cypress-watch-and-reload
    4. cypress-grep
    5. cypress-recurse
    6. cypress-data-session
  4. The end
$ 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
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.js in the config file
  • Chai assertions
  • Chai-jQuery & Sinon-Chai assertions
  • Clock control
  • Network control
  • Cypress._ (Lodash), Cypress.Promise, Cypress.$, Cypress.Blob, Cypress.minimatch

How do I create a testing framework using Cypress?

You don't have to

Don't

🧑‍💻

Do

📚

🙇

🎓

Study (?)

$ npm i -D cypress cypress-map prettier

How I start every project...

Cypress v12

adds retriable chains of query commands.

"kiwi" is the last item 🧑🏻

get elements

take the last element

its text should be "kiwi"

🤖

it('shows the kiwi last', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li').last().should('have.text', 'kiwi')
})

get elements

take the last element

its text should be "kiwi"

🤖

it('shows the kiwi last', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li').last().should('have.text', 'kiwi')
})

Cypress v11

it('shows the kiwi last', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li').last().should('have.text', 'kiwi')
})

Cypress v12

it('shows the kiwi last', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li')
    .last()
    .invoke('text')
    .should('equal', 'kiwi')
})

Cypress v12

it('shows 30 last', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li')
    .last()
    .invoke('text')
    .then(parseInt)
    .should('equal', 30)
})

Cypress v12

no retries

import 'cypress-map'

it('shows 30 last', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li')
    .last()
    .invoke('text')
    .apply(parseInt) // query from cypress-map
    .should('equal', 30)
})

Cypress v12

import 'cypress-map'

it('shows 30 last', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li')
    .last()
    .invoke('text')
    .apply(parseInt) // query from cypress-map
    .should('equal', 30)
})

Cypress v12

import 'cypress-map'

it('shows numbers', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li')
    .map('innerText')
    .map(Number)
    .should('deep.equal', [10, 20, 30])
})

Cypress v12

import 'cypress-map'

it('shows numbers', () => {
  cy.visit('cypress/index.html')
  cy.get('#items li')
    .map('innerText')
    .map(Number)
    .should('deep.equal', [10, 20, 30])
})

Cypress v12

cypress-map

  • apply
  • map
  • mapInvoke
  • mapChain
  • reduce
  • tap
  • print
  • ...

cypress-map

cy.table for checking the full table (or part of it)

cypress-map

Cypress query commands for v12+

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

plugins

plugins

Node.js

Process

Browser

Cypress architecture

cypress-map

cypress-each

cypress-recurse

cypress-esbuild-preprocessor

cypress-grep

cypress-watch-and-reload

cypress-data-session

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

(component testing)

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 things

  • Overwriting existing behavior

Cypress is just JavaScript. Anything not bolted down can be overwritten and changed.

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

Unfortunately, Cypress team does not plan to help plugins implement UI elements 😢

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. Don't use plugins
  2. The many types of plugins
  3. My (favorite) plugins
    1. cypress-map
    2. cypress-each
    3. cypress-watch-and-reload
    4. cypress-grep
    5. cypress-recurse
    6. cypress-data-session
  4. The end

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...

AGENDA

  1. Don't use plugins
  2. The many types of plugins
  3. My (favorite) plugins
    1. cypress-map
    2. cypress-each
    3. cypress-watch-and-reload
    4. cypress-grep
    5. cypress-recurse
    6. cypress-data-session
  4. The end

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-map
  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!

100 hands-on lessons, 22 plugins, lots of videos

My Favorite Cypress Plugins

Thank you 👏

gleb.dev

@bahmutov

Gleb Bahmutov

Sr Director of Engineering at Mercari US

LambdaTest

Voices of Community

My Favorite Cypress Plugins

By Gleb Bahmutov

My Favorite Cypress Plugins

You can supercharge your end-to-end, component, and API Cypress tests using plugins. In this talk, I will show my favorite Cypress plugins, explain how they work, and how easy it is to write simple, elegant testing code. From accessing databases, receiving emails, and setting up data, to repeating commands and checking the page's accessibility - there is a Cypress plugin for everything!

  • 1,334