Use Sanity Assertions

Imagine we need to confirm that the two numbers shown on the page match. There are multiple ways we could write this test.

📺 Watch this recipe explained in the video Use Sanity Assertionsopen in new window.

Compare two numbers

.dollars::before {
  content: '$';
  margin-right: 2px;
}
.dollars {
  width: 6rem;
  text-align: right;
}
<div>
  Received <span id="received" class="dollars">19.80</span>
</div>
<div>Spent <span id="spent" class="dollars">19.80</span></div>
cy.get('#received')
  .invoke('text')
  .then(Number)
  .then((received) => {
    cy.contains('#spent', received)
  })

Great. Get an element, grab its text, convert to a number, then use cy.containsopen in new window to find the other matching number. Notice that we did not check anything in the test above, except the existence of the elements #received and #spent using the built-in existence assertion in the cy.getopen in new window and cy.containsopen in new window commands. This can cause problems.

Handle NaN

When converting text to a number, it is not enough to check using should be a number assertion. If the text cannot be converted to a number, we get a NaN value, which is a number. We need to either explicitly check against NaN or use an arithmetic assertion like greaterThan(...) or within(...).

.dollars::before {
  content: '$';
  margin-right: 2px;
}
.dollars {
  width: 6rem;
  text-align: right;
}
<div>Received <span id="received" class="dollars">--</span></div>
<div>Spent <span id="spent" class="dollars">NaN</span></div>
// 🚨 DOES NOT WORK
// passes accidentally with "NaN" value
cy.get('#received')
  .invoke('text')
  .then(Number)
  .should('be.a', 'Number')
  .then((received) => {
    cy.contains('#spent', String(received))
  })
// ✅ Check if the subject is a number
// and is not a NaN
cy.get('#received')
  .invoke('text')
  .then(Number)
  .should('be.a', 'Number')
  .and('not.be.a.NaN')
  .then((received) => {
    cy.contains('#spent', String(received))
  })
// ✅ Make the sanity check more precise
// "within" assertion confirms the subject
// is a number and is within min and max
cy.get('#received')
  .invoke('text')
  .then(Number)
  .should('be.within', 0, 1000)
  .then((received) => {
    cy.contains('#spent', String(received))
  })

Dynamic data

Imagine the same application is loading the two numbers after a delay. Initial the app shows -- and later switches this text to the actual values.

.dollars::before {
  content: '$';
  margin-right: 2px;
}
.dollars {
  width: 6rem;
  text-align: right;
}
<div>Received <span id="received" class="dollars">--</span></div>
<div>Spent <span id="spent" class="dollars">--</span></div>
<script>
  setTimeout(() => {
    document.getElementById('received').innerText = '19.80'
  }, 1000)
  setTimeout(() => {
    document.getElementById('spent').innerText = '19.80'
  }, 2000)
</script>

The original test fails because -- converted to the nunmber is a NaN value, and cy.contains(selector, NaN) throws an error.

// 🚨 DOES NOT WORK
// since "--" makes NaN which cy.contains does not accept
cy.get('#received')
  .invoke('text')
  .then(Number)
  .then((received) => {
    cy.contains('#spent', received)
  })

We can convert NaN to a String to pass to the cy.contains command. But all the cy.contains command will do is forever try to find an element with id spent and text NaN. It will never go back to the first element #received to grab the updated text.

// 🚨 DOES NOT WORK
// since "cy.then" breaks the retries
// and it never "sees" the updated "#received" element text
cy.get('#received')
  .invoke('text')
  .then(Number)
  .then((received) => {
    cy.contains('#spent', String(received))
  })

The command cy.thenopen in new window does not retry. Thus when the cy.contains inside fails, it WILL NOT go back to the very first cy.get('#received') command to grab the updated element. We can try using the cy.shouldopen in new window assertion; it does retry. But we cannot use other Cypress commands inside the should(callback). Another dead end.

// 🚨 DOES NOT WORK
// since we cannot use "cy.contains" inside a "should(callback)"
cy.get('#received')
  .invoke('text')
  .should((received) => {
    cy.contains('#spent', received)
  })

Ok, how about just using the text from the first element? It will prevent -- being converted into NaN problem. The test passes, but it passes accidentally when it matches the text -- in the two elements. The test never "sees" two equal numbers!

// 🚨 DOES NOT WORK
// it passes _accidentally_ when it matches the initial text "--"
cy.get('#received')
  .invoke('text')
  .then((received) => {
    cy.contains('#spent', received)
  })

We can do better. Our problem is that we want to compare two numbers, yet we never checked if the elements have numbers or something else. Let's add sanity assertions: we are looking for two numbers. Once we see the numbers, we can compare their values without any retries.

cy.log('**sanity assertions**')
cy.get('#received')
  .invoke('text')
  // this is a sanity assertion
  .should('match', /^\d+\.\d\d$/)
  .then(Number)
  .then((received) => {
    // another sanity assertion confirming a valid number
    expect(received, 'received number').to.be.a('number').and.to
      .not.be.a.NaN
    // we can also use stricter and shorter assertion
    // which also handles NaN case
    expect(received, 'received is positive').to.be.greaterThan(0)
    cy.contains('#spent', received)
  })

You can confirm the number using cy.contains by passing a regular expression to match a number with two digits after the dot:

cy.log('**cy.contains with a regular assertion**')
// the regular assertion acts like a sanity check
cy.contains('#received', /^\d+\.\d\d$/)
  .invoke('text')
  .then(Number)
  .then((received) => {
    cy.contains('#spent', received)
  })

Negative assertions

Just a ward of caution. You might try using negative assertions to confirm that the elements stop showing the initial text --.

.dollars::before {
  content: '$';
  margin-right: 2px;
}
.dollars {
  width: 6rem;
  text-align: right;
}
<div>Received <span id="received" class="dollars">--</span></div>
<div>Spent <span id="spent" class="dollars">--</span></div>
<script>
  setTimeout(() => {
    document.getElementById('received').innerText = '19.80'
  }, 1000)
  setTimeout(() => {
    document.getElementById('spent').innerText = '19.80'
  }, 2000)
</script>

Beware: using a negative assertion like "should not have text --" might work for now.

cy.get('#received').should('not.have.text', '--')
cy.get('#spent').should('not.have.text', '--')
// compare the two values
cy.get('#received')
  .invoke('text')
  .then((received) => {
    cy.contains('#spent', received)
  })

But what happens when we change the initial HTML to ... instead of --? The test will immediately pass accidentally. My advice is to confirm the expected value instead of checking it against all possible invalid values.

// ✅ Using positive assertions
cy.contains('#received', /^\d+\.\d\d$/)
cy.contains('#spent', /^\d+\.\d\d$/)

Know your data

Here is the best solution to this test. If you know the expected values, you can write simple and robust test.

.dollars::before {
  content: '$';
  margin-right: 2px;
}
.dollars {
  width: 6rem;
  text-align: right;
}
<div>Received <span id="received" class="dollars">--</span></div>
<div>Spent <span id="spent" class="dollars">--</span></div>
<script>
  setTimeout(() => {
    document.getElementById('received').innerText = '19.80'
  }, 1000)
  setTimeout(() => {
    document.getElementById('spent').innerText = '19.80'
  }, 2000)
</script>
const amount = (19.8).toFixed(2)
cy.contains('#received', amount)
cy.contains('#spent', amount)

Nice and easy.