From 1cc70841f609e2571dd5fe35d7b76bd2b1c798fc Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Fri, 12 Jan 2024 14:00:54 -0500 Subject: [PATCH] add recipe use sanity assertions --- docs/index.md | 1 + docs/recipes/use-sanity-assertions.md | 247 ++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 docs/recipes/use-sanity-assertions.md diff --git a/docs/index.md b/docs/index.md index a9fa2fe7ea..063a0504fd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -369,6 +369,7 @@ The Cypress API enables you to configure the behavior of how Cypress works inter - [Conditional testing](./recipes/conditional-testing.md) - Avoid "Mixing async and sync code" by controlling the [value the code yields](./recipes/yield-value.md) +- [use sanity assertions](./recipes/use-sanity-assertions.md) ### Working with network diff --git a/docs/recipes/use-sanity-assertions.md b/docs/recipes/use-sanity-assertions.md new file mode 100644 index 0000000000..ff83d34902 --- /dev/null +++ b/docs/recipes/use-sanity-assertions.md @@ -0,0 +1,247 @@ +# 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. + +## Compare two numbers + + + +```css hide +.dollars::before { + content: '$'; + margin-right: 2px; +} +.dollars { + width: 6rem; + text-align: right; +} +``` + +```html hide +
+ Received 19.80 +
+
Spent 19.80
+``` + +```js +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.contains](https://on.cypress.io/contains) 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.get](https://on.cypress.io/get) and [cy.contains](https://on.cypress.io/contains) commands. This can cause problems. + +## 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. + + + +```css hide +.dollars::before { + content: '$'; + margin-right: 2px; +} +.dollars { + width: 6rem; + text-align: right; +} +``` + +```html hide +
Received --
+
Spent --
+ +``` + +The original test fails because `--` converted to the nunmber is a `NaN` value, and `cy.contains(selector, NaN)` throws an error. + +```js skip +// 🚨 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. + +```js skip +// 🚨 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.then](https://on.cypress.io/then) 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.should](https://on.cypress.io/should) assertion; it does retry. But we cannot use other Cypress commands inside the `should(callback)`. Another dead end. + +```js skip +// 🚨 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! + +```js +// 🚨 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. + +```js +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: + +```js +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 `--`. + + + +```css hide +.dollars::before { + content: '$'; + margin-right: 2px; +} +.dollars { + width: 6rem; + text-align: right; +} +``` + +```html hide +
Received --
+
Spent --
+ +``` + +**Beware:** using a negative assertion like "should not have text --" might work _for now_. + +```js skip +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. + +```js +// ✅ 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. + + + +```css hide +.dollars::before { + content: '$'; + margin-right: 2px; +} +.dollars { + width: 6rem; + text-align: right; +} +``` + +```html hide +
Received --
+
Spent --
+ +``` + +```js +const amount = (19.8).toFixed(2) +cy.contains('#received', amount) +cy.contains('#spent', amount) +``` + +Nice and easy. + +