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.

See also

Read my blog post Tests, closures and arrow functions that has a fine Dante reference.