Avoid Cypress Pyramid of Doom

How to use aliases to access multiple values instead of nesting multiple then callbacks.

Imagine an application with two input fields and a numerical result element. In the test we need to verify that the result is the sum of the inputs.

1
2
3
4
5
6
<body>
<p>Calculator</p>
<div>a = <input name="a" type="number" value="1" /></div>
<div>b = <input name="b" type="number" value="5" /></div>
<div>a + b = <span id="result">6</span></div>
</body>

You can find this page and the spec file in the repo bahmutov/cypress-multiple-aliases.

📺 If you would rather watch the explanation from this blog post, watch it here and subscribe to my YouTube channel.

If we grab each element, then (pun intended) the test will have a pyramid of callback functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('adds numbers', () => {
cy.visit('public/index.html')
cy.get('[name=a]')
.invoke('val')
.then(parseInt)
.then((a) => { // level 1
cy.get('[name=b]')
.invoke('val')
.then(parseInt)
.then((b) => { // level 2
cy.get('#result')
.invoke('text')
.then(parseInt)
.then((result) => { // level 3
expect(a + b).to.eq(result)
})
})
})
})

Can we avoid this? We could store each parsed number in an alias using .as command.

1
2
3
4
5
6
7
8
9
it('adds numbers via aliases', () => {
cy.visit('public/index.html')
cy.get('[name=a]').invoke('val').then(parseInt).as('a')
cy.get('[name=b]').invoke('val').then(parseInt).as('b')
cy.get('#result')
.invoke('text')
.then(parseInt)
.as('result')
})

We now need to access all three values at once. If we had just a single value, we could have used cy.get command. For three values, it would lead back to the pyramid of nested callbacks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
it('adds numbers via aliases', () => {
cy.visit('public/index.html')
cy.get('[name=a]').invoke('val').then(parseInt).as('a')
cy.get('[name=b]').invoke('val').then(parseInt).as('b')
cy.get('#result')
.invoke('text')
.then(parseInt)
.as('result')
// a pyramid again!
cy.get('@a').then(a => { // level 1
cy.get('@b').then(b => { // level 2
cy.get('@result').then(result => { // level 3
expect(a + b).to.eq(result)
})
})
})
})

Instead we can take advantage of the fact that each saved Cypress alias is also added into the test context object. We can access such properties using this.name later on. To make sure we access the a, b, and result properties after they have been set, we chain the access using .then

1
2
3
4
5
6
7
8
9
10
11
12
it('adds numbers via aliases', () => {
cy.visit('public/index.html')
cy.get('[name=a]').invoke('val').then(parseInt).as('a')
cy.get('[name=b]').invoke('val').then(parseInt).as('b')
cy.get('#result')
.invoke('text')
.then(parseInt)
.as('result')
.then(function () {
expect(this.a + this.b).to.eq(this.result)
})
})

The test is happy.

The passing test that uses Cypress aliases to avoid a pyramid of Doom of nested callbacks

Use the function syntax

Note that the callback that accesses the properties from the test context object using this.a, this.b, and this.result is a proper function that uses function () { ... } syntax. It cannot be () => { ... } expression, as such expression would not have the this pointing at the test context object; it would be the global object instead. Thus as a rule of thumb, whenever you use this inside a Cypress test, always have a proper function. If you can move your initial code into a beforeEach hook, then it is trivial:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
beforeEach(() => {
// set the aliases in the beforeEach hook
cy.visit('public/index.html')
cy.get('[name=a]').invoke('val').then(parseInt).as('a')
cy.get('[name=b]').invoke('val').then(parseInt).as('b')
})

// aliases are available here
it('adds numbers via aliases', function () {
cy.get('#result')
.invoke('text')
.then(parseInt)
.should('equal', this.a + this.b)
// or even simpler using cy.contains command
cy.contains('#result', this.a + this.b)
})

Tip: also check out my library cypress-aliases for simplifying this test even more.

Use valueAsNumber property

Since we are dealing with number inputs, we can get their numerical values without converting them.

1
2
3
4
5
6
// instead of this
cy.get('[name=a]').invoke('val').then(parseInt).as('a')
// we can do
cy.get('[name=a]')
.should('have.prop', 'valueAsNumber')
.as('a')

If you want to print the value to the Command Log, add an assertion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe('Use beforeEach hook and number values', () => {
beforeEach(() => {
cy.visit('public/index.html')
cy.get('[name=a]')
.should('have.prop', 'valueAsNumber')
.as('a')
// log the number to the Command Log
.should('be.finite')
cy.get('[name=b]')
.should('have.prop', 'valueAsNumber')
.as('b')
.should('be.finite')
})

it('has values set', function () {
cy.contains('#result', this.a + this.b)
})
})

The test grabs both numbers

See also