Simplify Cypress Calendar Test Example

Step by step guide to make a test dramatically simpler.

A user sent me a good question about this Cypress test: can it be simplified and can we compare the number of "empty" days computed in the first test with the number computed in the second test?

The original spec

Let's do it!

🎁 You can find the full source code for this blog post in the repo bahmutov/calendar-example.

Block ads

It is painful to load this site, since there are so many ads. The network requests to fetch the ads completely block the Command Log.

The ads during the tests

The first thing we can do is to block the ad domains using blocklist option in the cypress.config.js. We can also move the base URL there and allow for larger viewport

cypress.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { defineConfig } = require('cypress')

module.exports = defineConfig({
e2e: {
// baseUrl, etc
baseUrl: 'https://neradni-dani.com',
supportFile: false,
fixturesFolder: false,
viewportHeight: 1200,
viewportWidth: 1200,
blockHosts: [
'googleads.g.doubleclick.net',
'pagead2.googlesyndication.com',
],
},
})

Blocked all ads

Much better.

Wait for the page to load

Let's now figure out what the second test is querying on the page. If we hover over the get td[class="day old"] command, we notice a curious thing: the entire page is still loading.

The page is still loading when the cy.get command finishes

If we click on the GET command, we pin the DOM snapshot and can inspect it in the DevTools. Seems like the .months-container is still loading and has opacity less than 1.

The calendar uses low opacity at the start

Let's wait for the opacity to become 1.

1
2
3
4
5
6
7
8
9
10
11
12
it('Count and print how many empty fields there are for entire year', () => {
cy.visit('/kalendar-2022-srb.php')
cy.get('.months-container').should('have.css', 'opacity', '1')
cy.get('td[class="day new"]').then(($t) => {
cy.get('td[class="day old"]').then(($s) => {
let a = $t.length
let b = $s.length
let c = a + b
cy.log(`The number of empty fields is ${c}`)
})
})
})

If we hover or pin the GET command, it clearly shows the empty cells at the start of each month (the "old" days from the previous month)

The calendar fully loads before querying the cells

Empty days for the year

Let's simplify the second test

1
2
3
4
5
6
7
8
9
10
11
12
it('Count and print how many empty fields there are for entire year', () => {
cy.visit('/kalendar-2022-srb.php')
cy.get('.months-container').should('have.css', 'opacity', '1')
cy.get('td[class="day new"]').then(($t) => {
cy.get('td[class="day old"]').then(($s) => {
let a = $t.length
let b = $s.length
let c = a + b
cy.log(`The number of empty fields is ${c}`)
})
})
})

We are getting all table cells with class "day" and the class "new", and then get all table cells with the class "day" and the class "new". Then we add the number of these cells together. Here is an equivalent test:

1
2
3
4
5
6
7
8
9
10
it('Count and print how many empty fields there are for entire year', () => {
cy.visit('/kalendar-2022-srb.php')
cy.get('.months-container').should('have.css', 'opacity', '1')
cy.get('td.day.new, td.day.old')
.should('be.visible')
.its('length')
.then((n) => {
cy.log(`The number of empty fields is ${n}`)
})
})

The empty day table cells

Count the empty cells in each month

Now let's look at the first test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
it('Find and print how many empty fields there are for each month', () => {
cy.visit('https://neradni-dani.com/kalendar-2022-srb.php')
cy.get('table').each(($tables) => {
cy.wrap($tables)
.find('thead>tr:first-child>th')
.then(($nameOfMonth) => {
let nameM = $nameOfMonth.text()
cy.wrap($tables)
.find('tbody tr')
.then((rows) => {
let numOfRows = rows.length
cy.wrap($tables)
.find('tbody td>div')
.then((td) => {
let numOfFillInDays = td.length
cy.wrap($tables)
.find('tbody td')
.then((allDays) => {
let allD = allDays.length
let emptyDays = allD - numOfFillInDays - numOfRows
cy.wrap(emptyDays).as('tst')
cy.log(
`The number of empty fields is ${emptyDays} for month ${nameM}`,
)
})
})
})
})
})
})

We are iterating over every month table and count the number of cells with a DIV inside. Then we subtract it from the total number of cells, computing the number of empty cells. We also print month title. Let's start with just printing the month title. Once we get the month tables, we can iterate over every table, which is a jQuery object.

1
2
3
4
5
6
7
describe('test calendar', () => {
it('Find and print how many empty fields there are for each month', () => {
cy.visit('/kalendar-2022-srb.php')
cy.get('.months-container').should('have.css', 'opacity', '1')
cy.get('table.month').each(($table) => {})
})
})

The 12 tables are selected

The month tables

Now we want to find the title for each month. If we inspect the markup, the month's title is in the header cell th.month-title

The month title

If you have a jQuery object, you can find the element with a CSS selector directly using the jQuery method $.find.

1
2
3
4
cy.get('table.month').each(($table) => {
const name = $table.find('.month-title').text()
cy.log(`month ${name}`)
})

Find and log each month title

What about empty cells? To answer this question, search https://glebbahmutov.com/cypress-examples/ for "empty elements"

Search cypress-examples how to find empty elements

This leads you to the page with my Empty elements recipe.

Empty elements recipe

So we can use the CSS selector :empty to find elements without children elements. Will this work? Let's try it out

1
2
3
4
5
cy.get('table.month').each(($table) => {
const name = $table.find('.month-title').text()
const empty = $table.find('tbody td:empty').length
cy.log(`month ${name} has ${empty} empty days`)
})

Empty days for each month table

Nice

Compare the two numbers

Now let's compare the two numbers: the number of empty table cells with the total number of .day.new, .day.old cells. We can use a local variable total to sum the empty cells. All we need to do is to use cy.then(callback) to use its value. Any time we get something from the web application, we must use cy.then to use its value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
describe('test calendar', () => {
it('has the right number of empty days', () => {
cy.visit('/kalendar-2022-srb.php')
cy.get('.months-container').should('have.css', 'opacity', '1')

let total = 0
cy.get('table.month')
.each(($table) => {
const name = $table.find('.month-title').text()
const empty = $table.find('tbody td:empty').length
cy.log(`The number of empty fields is ${empty} for month ${name}`)
total += empty
})
.then(() => {
// we can use the total value only AFTER it was computed
cy.get('td.day.new, td.day.old')
.should('be.visible')
.its('length')
.should('equal', total)
})
})
})

The numbers match

And how does our code compare to the initial one?

The code before and after rewriting

Any time you are "fighting" the Cypress syntax, take a step back. Maybe you are fighting against the tool and its declarative syntax. If you know what to expect, the test should read naturally. Tip: you can always find Cypress examples and larger recipes by searching https://glebbahmutov.com/cypress-examples/ or cypress.tips/search.