Cypress Declassified

Cypress Engineer Shares Proven & Emerging Best Practices

Presenters

Gleb Bahmutov

@bahmutov

Senior Director of Engineering

Mercari US

 

Ex-Distinguished Engineer

Ex-VP of Engineering

Cypress.io 

Eran Kinsbruner

@ek121268

Chief Evangelist

Perforce.io 

Agenda:

  • Market adoption
  • Most advanced automation features in Cypress
  • Best practices for scaling using Cypress
  • The plugin ecosystem
  • What's on the Cypress roadmap

Cypress Market Adoption

Cypress GitHub Stars

Meanwhile ...

Meanwhile ...

Framework CLIs

  • Vue CLI ✅
  • Nx CLI (Angular, React) ✅
  • Ng CLI ✅
  • Create-React-App ❌

People testing with Selenium / WebDriver / X

Cypress users?

No E2E tests

Cypress Powers

  • Direct access to the app in the browser

  • Clock control

  • Network control

DOM

storage

location

cookies

Cypress tests run in the same browser

DOM

storage

location

cookies

Cypress acts as a proxy for your app

if (navigator.battery) {
  readBattery(navigator.battery)
} else if (navigator.getBattery) {
  navigator.getBattery().then(readBattery)
} else {
  document.querySelector('.not-support').removeAttribute('hidden')
}

window.onload = function () {
  // show updated status when the battery changes
  battery.addEventListener('chargingchange', function () {
    readBattery()
  })

  battery.addEventListener('levelchange', function () {
    readBattery()
  })
}

window.navigator.battery

context('navigator.battery', () => {
  it('shows battery status of 50%', function () {
    cy.visit('/', {
      onBeforeLoad (win) {
        // mock "navigator.battery" property
        // returning mock charge object
        win.navigator.battery = {
          level: 0.5,
          charging: false,
          chargingTime: Infinity,
          dischargingTime: 3600, // seconds
          addEventListener: () => {}
        }
      }
    })

    // now we can assert actual text - we are charged at 50%
    cy.get('.battery-percentage')
      .should('be.visible')
      .and('have.text', '50%')

    // not charging means running on battery
    cy.contains('.battery-status', 'Battery').should('be.visible')
    // and has enough juice for 1 hour
    cy.contains('.battery-remaining', '1:00').should('be.visible')
  })
})
context('navigator.battery', () => {
  it('shows battery status of 50%', function () {
    cy.visit('/', {
      onBeforeLoad (win) {
        // mock "navigator.battery" property
        // returning mock charge object
        win.navigator.battery = {
          level: 0.5,
          charging: false,
          chargingTime: Infinity,
          dischargingTime: 3600, // seconds
          addEventListener: () => {}
        }
      }
    })

    // now we can assert actual text - we are charged at 50%
    cy.get('.battery-percentage')
      .should('be.visible')
      .and('have.text', '50%')

    // not charging means running on battery
    cy.contains('.battery-status', 'Battery').should('be.visible')
    // and has enough juice for 1 hour
    cy.contains('.battery-remaining', '1:00').should('be.visible')
  })
})

App Actions

it('deletes all heroes through UI', () => {
  cy.visit('/heroes')
  // confirm the heroes have loaded and select "delete" buttons
  cy.get('ul.heroes li button.delete')
    .should('have.length.gt', 0)
    // and delete all heroes
    .click({ multiple: true })
})

App Actions

it('deletes all heroes through UI', () => {
  cy.visit('/heroes')
  // confirm the heroes have loaded and select "delete" buttons
  cy.get('ul.heroes li button.delete')
    .should('have.length.gt', 0)
    // and delete all heroes
    .click({ multiple: true })
})

App Actions

// App code
constructor(private heroService: HeroService) {
  // @ts-ignore
  if (window.Cypress) {
    // @ts-ignore
    window.HeroesComponent = this
  }
}

App Actions

const getHeroesComponent = () =>
  cy.window()
    .should('have.property', 'HeroesComponent')

const getHeroes = () =>
  getHeroesComponent().should('have.property', 'heroes')

const clearHeroes = () =>
  getHeroes()
    .then(heroes => {
      cy.log(`clearing ${heroes.length} heroes`)
      // @ts-ignore
      heroes.length = 0
    })

it('deletes all heroes through app action', () => {
  cy.visit('/heroes')
  getHeroes().should('have.length.gt', 0)
  clearHeroes()
})

App Actions

cy.clock + cy.intercept

Application fetches a new list of fruits every 30 seconds

How do we:

a) confirm the displayed fruits?

b) run the test quickly?

/// <reference types="Cypress" />

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 🥝')
  })
})

Time-traveling debugger

More Info

Paid Features 💵

  • recording test artifacts
  • test parallelization
  • GitHub / X integration
  • test analytics

Running Lots of Tests

cypress run --record --parallel
{
  "retries": {
    // Configure retry attempts for `cypress run`
    // Default is 0
    "runMode": 2,
    // Configure retry attempts for `cypress open`
    // Default is 0
    "openMode": 0
  }
}

How it Works?

How Perfecto Uses Cypress

Execution in The Cloud

Benefits of Perfecto and Cypress

Cypress Plugins

  • Custom commands (Testing library)
  • Component testing
  • Code coverage
  • Visual testing
  • Email testing
  • A11y testing
  • API testing
  • Test grep, Cucumber syntax, etc
npm install -D @cypress/code-coverage
// cypress/support/index.js
import '@cypress/code-coverage/support'
// cypress/plugins/index.js
module.exports = (on, config) => {
  require('@cypress/code-coverage/task')(on, config)

  // add other tasks to be registered here

  // IMPORTANT to return the config object
  // with the any changed environment variables
  return config
}

Code Coverage Plugin

Code Coverage Plugin

$ open coverage/lcov-report/index.html

End-to-end tests are VERY effective at covering app's code

Cypress: Current and Future Work

Component Testing 🧩

Multi-domain 🌐

WebKit (Safari) 🖥

Q & A

Thank You

Visit perfecto.io or follow @perfectomobile

Gleb Bahmutov

@bahmutov

https://gleb.dev

Eran Kinsbruner

@ek121268