Conditional testing

Conditional testing is strongly discouragedopen in new window in Cypress. But if you must do something conditionally in Cypress depending on the page, here are few examples.

Toggle checkbox

Let's say that you have a checkbox when the page loads. Sometimes the checkbox is checked, sometimes not. How do you toggle the checkbox?

<div>
  <input type="checkbox" id="done" />
  <label for="done">Toggle me!</label>
</div>
<script>
  // sometimes the checkbox is checked
  const dice = Math.random()
  if (dice < 0.5) {
    document.querySelector('input').checked = true
  }
</script>
// first solution using jQuery $.prop
cy.get('input').then(($checkbox) => {
  const initial = Boolean($checkbox.prop('checked'))
  // let's toggle
  cy.log(`Initial checkbox: **${initial}**`)
  // toggle the property "checked"
  $checkbox.prop('checked', !initial)
})

Alternative solution

<div>
  <input type="checkbox" id="done" />
  <label for="done">Toggle me!</label>
</div>
<script>
  // sometimes the checkbox is checked
  const dice = Math.random()
  if (dice < 0.5) {
    document.querySelector('input').checked = true
  }
</script>
cy.get('input')
  .as('checkbox')
  .invoke('is', ':checked') // use jQuery $.is(...)
  .then((initial) => {
    cy.log(`Initial checkbox: **${initial}**`)
    if (initial) {
      cy.get('@checkbox').uncheck()
    } else {
      cy.get('@checkbox').check()
    }
  })

Element does not exist or is invisible

Imagine we want to pass the test if the element does not exist or is invisible. We will use Cypress.dom utility methods and retry the assertion using .should(cb) method.

<div id="either">
  <div id="disappears">Should go away</div>
  <div id="hides">Should not see me</div>
</div>
<script>
  setTimeout(() => {
    const disappears = document.getElementById('disappears')
    disappears.parentNode.removeChild(disappears)
  }, 500)

  setTimeout(() => {
    const hides = document.getElementById('hides')
    hides.style.display = 'none'
  }, 1000)
</script>
const isNonExistentOrHidden = ($el) =>
  !Cypress.dom.isElement($el) || !Cypress.dom.isVisible($el)

// let's assert an element does not exist
cy.get('#either #disappears').should(($el) => {
  expect(isNonExistentOrHidden($el)).to.be.true
})
// let's assert an element becomes invisible
cy.get('#either #hides').should(($el) => {
  expect(isNonExistentOrHidden($el)).to.be.true
})

Click a button if present

Let's say that we want to click a button if it is present on the page. We need to avoid triggering the built-in existence assertion in the cy.get or cy.contains commands, and click the button only after checking it ourselves.

<div>
  <p>The button might appear here</p>
  <div id="output"></div>
</div>
<script>
  if (Math.random() < 0.5) {
    const output = document.getElementById('output')
    const btn = document.createElement('button')
    btn.innerHTML = 'Click Me'
    output.appendChild(btn)
    btn.addEventListener('click', () => {
      console.log('Clicked')
    })
  }
</script>
// get the button but disable the built-in cy.contains assertions
// by appending our own dummy .should() assertion
cy.contains('button', 'Click Me')
  .should((_) => {})
  .then(($button) => {
    if (!$button.length) {
      // there is no button
      cy.log('there is no button')
      return
    } else {
      cy.window().then((win) => {
        cy.spy(win.console, 'log').as('log')
      })
      cy.wrap($button).click()
      cy.get('@log').should(
        'have.been.calledOnceWith',
        'Clicked',
      )
    }
  })

Click a button if not disabled

Let's say that we want to click a button if it not disabled. Otherwise, just print a message to the console.log

<div>
  <p>
    The button might be disabled
    <button id="btn">Click Me</button>
  </p>
</div>
<script>
  const btn = document.getElementById('btn')
  btn.addEventListener('click', function () {
    alert('Clicked')
  })

  if (Math.random() < 0.5) {
    btn.setAttribute('disabled', 'disabled')
  }
</script>

Let's prepare a stub around window.alert method.

// spy on the "window.alert"
cy.window().then((win) => {
  cy.stub(win, 'alert').as('alert')
})
cy.contains('#btn', 'Click Me')
  // cy.contains has a built-in "existence" assertion
  // thus by now we know the button is there
  .then(($btn) => {
    if ($btn.attr('disabled')) {
      console.log('Cannot click a disabled button')
    } else {
      cy.wrap($btn).click()
      cy.get('@alert')
        .should('have.been.calledOnce')
        // we can immediately reset the stub call history
        .invoke('resetHistory')
    }
  })

You can even use the jQuery helper method isopen in new window to check if the button is currently disabled.

cy.contains('#btn', 'Click Me').then(($btn) => {
  if ($btn.is(':disabled')) {
    cy.log('Button is disabled')
  } else {
    cy.log('Can click it')
  }
})

jQuery has :enabled check too which you can use. It is the opposite of :disabled check.

cy.contains('#btn', 'Click Me').then(($btn) => {
  if ($btn.is(':enabled')) {
    cy.log('Clicking...')
    cy.wrap($btn).click()
    cy.get('@alert').should('have.been.calledOnce')
  } else {
    cy.log('Button is disabled')
  }
})

If you want, you can invoke the $.is(':enabled') method to check. 📺 Watch this recipe explained in the video Check If A Button Is Enabled Before Clickingopen in new window.

<div>
  <p>
    The button might be disabled
    <button id="btn">Click Me</button>
  </p>
</div>
<script>
  const btn = document.getElementById('btn')
  btn.addEventListener('click', function () {
    btn.innerText = 'Clicked'
  })

  if (Math.random() < 0.5) {
    btn.setAttribute('disabled', 'disabled')
  }
</script>
cy.contains('#btn', 'Click Me')
  .invoke('is', ':enabled')
  .then((enabled) => {
    if (enabled) {
      cy.contains('#btn', 'Click Me')
        .click()
        .should('have.text', 'Clicked')
    } else {
      cy.log('**not clicking**')
    }
  })

Click a button if a class is present

Sometimes you want to click the button, but only if the element has a class

<button id="first" class="urgent important" disabled="disabled">
  Click NOW
</button>
<script>
  // notice that at first the button is disabled
  // and we need to use the built-in action checks in cy.click
  const first = document.getElementById('first')
  first.addEventListener('click', () => {
    console.log('first click')
  })
  // remove the disabled property
  setTimeout(() => {
    first.disabled = false
  }, 1000)
</script>
// spy on the console.log to make sure it is called
cy.window()
  .its('console')
  .then((console) => cy.spy(console, 'log').as('log'))

cy.get('#first').then(($first) => {
  if ($first.hasClass('important')) {
    // note that we cannot simply use jQuery .click() method
    // since it won't wait for the button to be enabled
    // $first.click()
    // instead we wrap the element and use the cy.click() command
    cy.wrap($first).click()
  }
})
// confirm the button was called correctly
cy.get('@log').should('be.calledOnceWith', 'first click')

Count elements if they exist

Let's confirm the number of elements is even, if they exist. If the elements do not exist, do nothing.

<ul id="people">
  <li>Joe</li>
  <li>Anna</li>
</ul>

First, let's have a list of elements that do exist.

const isEven = (n) => n % 2 === 0

cy.get('#people li')
  .should(Cypress._.noop)
  .its('length')
  .then((n) => {
    if (n) {
      expect(n).to.satisfy(isEven)
    }
  })

The same code works if the elements are not found.

cy.get('#invalid-selector li')
  .should(Cypress._.noop)
  .its('length')
  .then((n) => {
    if (n) {
      expect(n).to.satisfy(isEven)
    }
  })

Count elements if they exist after delay

📺 Watch this recipe explained in the video Conditional Count Elements After They Have Been Loadedopen in new window.

<ul id="people"></ul>
<script>
  setTimeout(() => {
    const list = document.getElementById('people')
    if (Math.random() < 0.5) {
      list.innerHTML = `
        <li>Joe</li>
        <li>Anna</li>
      `
    }
    list.setAttribute('data-loaded', 'true')
  }, 900)
</script>

The list above is initialized after a delay of almost one second. The list might be empty or it might have list items. How do we get the number of list items if they exist? The following code will NOT work. Yes, it handles the elements no existing, but the length will always be zero, since we are checking the elements before the list has a chance to appear in the DOM:

// 🚨 INCORRECT, the number of elements will always be zero
cy.get('#people li')
  .should(Cypress._.noop)
  .its('length')
  .should('be.oneOf', [0, 2])

We could always wait one second and check the elements. This is slow but works when the list items are added or not.

// ⚠️ WORKS BUT HAS A BUILT-IN DELAY
cy.wait(1000)
cy.get('#people li')
  .should(Cypress._.noop)
  .its('length')
  .should('be.oneOf', [0, 2])

A much better solution is to first retry until the data is loaded, then use conditional testing to get the elements if they exist. We know the data has been loaded when the element #people gets the attribute data-loaded=true. This attribute is set in all cases, even if there will be no list items inserted.

// ✅ RECOMMENDED
// the top-level CY.GET command uses built-in existence assertion
// to retry until the attribute is set and the element is found
cy.get('#people[data-loaded=true]')
  // the LI elements might be there or not
  // so we disable the built-in existence assertion
  // using the "noop" should callback
  .find('li')
  .should(Cypress._.noop)
  .its('length')
  .should('be.oneOf', [0, 2])

For more, read the blog post Negative Assertions And Missing Statesopen in new window.

Getting a cookie using cy.getCookieopen in new window command does not retry, thus you can simply work with the yielded value.

function printCookieMaybe(cookie) {
  if (cookie) {
    cy.log(`Found the cookie with value: ${cookie.value}`)
  } else {
    cy.log('No cookie for you')
  }
}
cy.getCookie('my-cookie').then(printCookieMaybe)
cy.setCookie('my-cookie', 'nice')
cy.getCookie('my-cookie').then(printCookieMaybe)

Perform different actions depending on the URL

As always, when getting something from the page, you get its value in the .then(callback). If you get the current URL using cy.locationopen in new window or cy.urlopen in new window, you can decide what to do next based on its value:

cy.location('pathname').then((pathname) => {
  if (pathname.includes('/about/')) {
    cy.log('At the About page')
  } else {
    cy.log('Another page')
  }
})

Skip the rest of the test if an element exists

<output id="app" />
<script>
  // in half of the cases, the element will be there
  if (Math.random() < 0.5) {
    document.getElementById('app').innerHTML = `
      <div data-dynamic="true">Dynamic</div>
    `
  }
</script>

Our test first checks the element with id "app". If it has at that moment a child with text "Dynamic", then we confirm that element has an attribute "data-dynamic=true". If the #app element does not have a child element with text "Dynamic" then we stop the test by not executing any more Cypress commands

// using the CSS :has selector to find the element
cy.get('#app:has(div)')
  // we don't know if the element exists or not
  // so we bypass the built-in existence assertion
  // using the no-operator should(callback)
  .should(Cypress._.noop)
  .then(($el) => {
    if (!$el.length) {
      cy.log('No element, stopping')
      // note: the test cannot have any more commands
      // _after_ this cy.then command if you really
      // want to stop the test
      return
    }

    // the element exists, let's confirm something about it
    cy.contains('#app div', 'Dynamic').should(
      'have.attr',
      'data-dynamic',
      'true',
    )
    // equivalent assertion without using Cypress chain
    // and just using jQuery and Chai-jQuery combination
    // (no retry-ability, immediate assertion)
    expect($el.find('div'), 'has the attribute').to.have.attr(
      'data-dynamic',
      'true',
    )
  })

Note: we only skip the rest of the test commands inside the callback. If you want to really stop the test at run-time, see the cypress-skip-testopen in new window plugin.

cypress-if

If you MUST use conditional commands in your tests, check out my cypress-ifopen in new window plugin.

import 'cypress-if'

cy.get('#agreed')
  .if('not.checked')
  .click() // IF path
  .else()
  .log('The user already agreed') // ELSE path

Read the blog post Conditional Commands For Cypressopen in new window.

cypress-recurse

You can implement advanced "run Cypress commands until this condition becomes true" flows using my plugin cypress-recurseopen in new window. For example, let's test a loading element that might never change its text. We still don't want to fail the test, but we want to log a message if so.

<div id="loader">Loading ...</div>
<script>
  if (Math.random() < 0.5) {
    const delay = 500 + Math.round(3000 * Math.random())
    setTimeout(() => {
      document.getElementById('loader').innerText =
        'Matches found: ' +
        String(Math.random()).slice(2, 3) +
        ' took ' +
        delay +
        'ms'
    }, delay)
  }
</script>

We do not know if the loading finishes quickly or slowly. Thus we want to keep checking, and as soon as it goes away, we want to confirm that Matches found: <number> is shown. But if the loading text does not go away in 5 seconds, no big deal. We will just log a message.

// cy.recurse comes from cypress-recurse plugin
cy.recurse(
  () => cy.get('#loader').invoke('text'),
  (text) => !text.includes('Loading'),
  {
    timeout: 5_000,
    doNotFail: true,
    yield: 'value',
    log: 'Loaded',
    // pause between attempts
    delay: 500,
  },
).then((text) => {
  // conditional testing depending on the text
  if (text.includes('Loading')) {
    cy.log('Never showed matches')
  } else {
    cy.contains('#loader', /Matches found: \d/)
  }
})

Related blog posts Cypress Needs Soft Assertionsopen in new window and Negative Assertions And Missing Statesopen in new window.

A better loading test

The above example mixes up loading and error states. A much better application would set something observable to notify us when it finishes loading, even if it does not change the "Loading ..." text. For example, let's set a class when finishing loading in all cases.

<style>
  .loaded {
    color: green;
    font-weight: bold;
  }
</style>
<div id="loader">Loading ...</div>
<script>
  const delay = 500 + Math.round(3000 * Math.random())
  setTimeout(() => {
    const el = document.getElementById('loader')
    // always set CSS class name when finished loading
    el.classList.add('loaded')
    if (Math.random() < 0.5) {
      el.innerText =
        'Matches found: ' +
        String(Math.random()).slice(2, 3) +
        ' took ' +
        delay +
        'ms'
    }
  }, delay)
</script>

Now the test is much simpler to write and is as fast as possible.

cy.get('#loader')
  .should('have.class', 'loaded')
  .invoke('text')
  .then((text) => {
    // conditional testing depending on the text
    if (text.includes('Loading')) {
      cy.log('Never showed matches')
    } else {
      cy.contains('#loader', /Matches found: \d/)
    }
  })

More examples