Categories
Technology

Tips & Tricks for Cypress

Photo by Hunter Haley on Unsplash

🏆 This post was featured in Software Testing Weekly, issue 55

This is a collection of simple and recurring scenarios when writing Cypress tests. For more complex recipes, check the official doc.

  • Setup
    • Abort cypress after first failed test
    • Read a test file from fixtures
  • Assertions
    • Assert the text of a page (string or number)
    • Assert the number of elements selected
    • Assert the result of two Cypress commands
  • Actions
    • Upload a file
  • Selectors
    • Given a list, return row that contains specific text
    • Selector is flaky due to page redesigns
    • Type text into input field and press enter
    • Select element inside iframe
    • Use selector X to narrow down area, and then use selector Y to find element
  • Waits
    • Wait until a network (ie. HTTP/XHR) request resolves
    • Wait until a condition becomes true
  • Mocks
    • Force a specific response to an HTTP request


Setup

Abort cypress after first failed test

Add this inside your describe block:

afterEach(function() {
  if (this.currentTest.state === "failed") {
    Cypress.runner.stop()
  }
})

More info on this GitHub thread.

Read a test file from fixtures

You have three ways to do it:

  • .fixture() inside an it
  • .fixture() inside a before or beforeEach
  • require

Assertions

Assert the text of a page (string or number)

Your page has text, some are words some are numbers. It’s not straightforward to get the text of a page:

cy.get("selector").invoke("text") // built-in with cypress
cy.get("selector").text() // requires https://github.com/Lakitna/cypress-commands

Note that the code above will return a Chainable object, not a string. Meaning you can do ...text().should("be.not.empty") but you cannot do expect(...text()).to.be.not.empty().

When asserting numbers, it’s safer to do:

cy.get("selector")
  .text()
  .then(str => Number(str))
  .should("be.above", 2)

Assert the number of elements selected

// assert number of elements selected
cy.get("selector")
  .find("child-selector")
  .should("have.length", 4)

// but for comparisons other than equal, you need this syntax
cy.get("selector")
  .find("child-selector")
  .its("length")
  .should("be.gte", 4) // greater than or equal

Assert the result of two Cypress commands

It is not recommended that you assign return values of Cypress commands (async):

const textBeforeClick = cy.get("#btn").text()
cy.get("#btn").click()
const textAfterClick = cy.get("#btn").text()
expect(textBeforeClick).not.to.eq(textAfterClick)

Alternatively, you can use aliasing to do this:

cy.get("#btn")
  .text()
  .as("textBeforeClick")
cy.get("#btn").click()
cy.get("#btn")
  .text()
  .should("not.equal", this.textBeforeClick)

Actions

Upload a file

Cypress.Commands.add("uploadFile", (selector, fileName, mimeType) =>
  cy.get(selector).then(input =>
    cy
      .fixture(fileName, "base64")
      .then(Cypress.Blob.base64StringToBlob)
      .then(blob => {
        const element = input[0]
        const testFile = new File([blob], fileName, {
          type: mimeType
        })
        const dataTransfer = new DataTransfer()
        dataTransfer.items.add(testFile)
        element.files = dataTransfer.files
        element.dispatchEvent(new Event("change", { bubbles: true }))
      })
  )
)

Selectors

Given a list, return row that contains specific text

cy.contains("text") is not as eficient or precise as the alternative below.

cy.contains("selector", "text") returns all elements that match the selector AND contain the text.

cy.contains("#results li.item", "Lisbon (District)")

Selector is flaky due to page redesigns

Write a selector that searches by data atribute, instead of id or css path.

<!-- code.html -->
<input data-cy="searchBar" class="..." />
// test.spec.js
cy.get("[data-cy='searchBar']")

Type text into input field and press enter

You can combine JS template strings with Cypress Enter special key.

const text = "text to input on search"
cy.get("selector").type(`${text}{enter}`)

Select element inside iframe

Use the cypress-iframe plugin and follow their instructions.

Use selector X to narrow down area, and then use selector Y to find element

Simple selectors tend to match more elements than you want. You might want use your simple selector after narrowing down the search with another selector. Use cy.find.

cy.contains("li.todo", "My task") // <-- narrow down
  .should("exist")
  .find('input[type="checkbox"]') // <-- get what you want
  .check()

Waits

Avoid as much as possible doing cy.wait(milli). There are other, more efficient, ways.

Wait until a network (ie. HTTP/XHR) request resolves

You need to wait for an HTTP request to finish to continue with your test. You don’t start the request, it is made implicitly by the system you’re testing. The UI doesn’t tell you for sure if the request finished. To be accurate you need to listen at the network level. (read more)

cy.server() // starts a listener of network requests
cy.route("GET", "/todos") // tells cypress the endpoint we want to spy
  .as("listAll") // gives it a name/alias

cy.visit("/") // this page implicitly calls the spied endpoint
cy.wait("@listAll") // cy.server spies the endpoint and waits until a reply

Wait until a condition becomes true

Using the waitUntil plugin you can execute/repeat code until a given condition becomes true. This is useful when your code depends on some external background tasks (e.g. cron job).

cy.waitUntil(
  () =>
    cy
      .request(targetSite)
      .its("status")
      .then(status => status === 200),
  {
    interval: 5000, // tries every 5s
    timeout: 30000, // gives up after 30s
    errorMsg: `Timed out pinging ${targetSite}`
  }
)

Your condition might throw an exception instead of returning false. For instance, if you need to wait until a page element becomes visible, you could try cy.get() but that method has a built-in assertion. That means it would fail and exit the waitUntil block, instantly failing the test. You need an alternative way, like a direct jQuery call:

cy.waitUntil(() => {
  // refreshes the page
  cy.reload()
  // returns true, if your element exists in the page
  return Cypress.$(cssSelector).length > 0
})
cy.get(cssSelector).should("exist")

Mocks

When writing E2E tests usually we avoid mocks, since the point is to test “the real deal”. If you must…

Force a specific response to an HTTP request

cy.route can both spy network requests or fake responses to those same requests. You just need to pass the response you want, see official doc.

cy.route("GET", "/todos").as("list") // spies the endpoint

const response = "..."
cy.route("GET", "/todos", response).as("list") // mocks a response