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

Cypress Declassified

By Gleb Bahmutov

Cypress Declassified

Everyone’s talking about Cypress. But they don’t know what they don’t know… Get the inside scoop on Cypress from Distinguished Engineer Gleb Bahmutov. He’ll be joined by Eran Kinsbruner, DevOps Chief Evangelist at Perforce. Together, they’ll dive into how dev teams are testing, learning, and optimizing with one of the fastest-growing front-end automation frameworks. You’ll Learn: - Most advanced features for automating with Cypress. - Best practices for scaling & optimizing with Cypress. - When and how to use Cypress with commercial tools. - What to expect on the roadmap for Cypress.

  • 285
Loading comments...

More from Gleb Bahmutov