Use Async Await In Cypress Specs

Solve the biggest Cypress.io beginners frustration using my cypress-await plugin.

The biggest gripe people have when they start using Cypress is the "lack" of await keyword in front of every Cypress command. This complaint comes up again and again: why do I need to use cy.then(callback) to work with the data from the application? Why can't I just do what other test runners do:

1
2
// I WANT TO DO THIS!!!!
const n = await cy.get('#todo li').its('length')

I have written that how Cypress declarative approach might be simpler and shorter to write. I have recorded videos on how to avoid pyramid of Doom of callbacks. Nothing really lowers people's frustration. They want await cy... really badly.

Ok then. Let's do this. Cypress lets you use your own spec file preprocessor to bundle or transpile the test code. I wrote cypress-await that uses Babel to automatically transpile testing specs that use await cy... into cy....then(callback)

1
2
3
4
5
6
7
8
9
10
11
// your code
it('confirms the number of todos', async () => {
const n = await cy.get('#todo li').its('length')
expect(n).to.equal(5)
})
// transpiled code behind the scenes
it('confirms the number of todos', () => {
cy.get('#todo li').its('length').then(n => {
expect(n).to.equal(5)
})
})

The code still retries using the built-in assertions like cy.get and the entire query, so you are not losing anything using this plugin. There is even a "sync" mode that allows you to skip writing await in front of every Cypress command and simply get the data variable = cy...

cypress-await vs plain Cypres syntax

Nice!

Install and use

Just like any Cypress plugin following the README instructions.

cypress.config.js
1
2
3
4
5
6
7
8
9
10
11
const { defineConfig } = require('cypress')
// https://github.com/bahmutov/cypress-await
const cyAwaitPreprocessor = require('cypress-await/src/preprocessor')

module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('file:preprocessor', cyAwaitPreprocessor())
},
},
})

You can even transpile some spec files using a minimatch or file path suffix. For example, the majority of our spec files might be "plain" Cypress standard, while only some using await keyword.

1
2
3
4
cypress/
e2e/
basic.cy.js
user.await.cy.js // need to transpile this spec

We can set up the specPattern to transpile only files that end with .await.cy.js string

cypress.config.js
1
2
3
4
5
setupNodeEvents(on, config) {
on('file:preprocessor', cyAwaitPreprocessor({
specPattern: '.await.cy.js'
}))
}

You can debug the transpiled source to see what happens under the hood:

cypress.config.js
1
2
3
4
5
6
setupNodeEvents(on, config) {
on('file:preprocessor', cyAwaitPreprocessor({
specPattern: '.await.cy.js',
debugOutput: true,
}))
}

The terminal will show the transpiled spec source code:

Transpiled spec file output

Sync mode

Hmm, one of the attractive parts of Cypress is its declarative syntax. Putting await in front of every Cypress command just to remove it seems useless. The only time we really want the value is when we assign the result of executing a Cypress command to a variable. Thus my plugin cypress-await has another "sync" preprocessor. You use it the same way: point Cypress file preprocessor at the "sync" preprocessor.

cypress.config.js
1
2
3
4
5
6
7
8
9
10
11
const { defineConfig } = require('cypress')
// https://github.com/bahmutov/cypress-await
const cyAwaitPreprocessor = require('cypress-await/src/preprocessor-sync-mode')

module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('file:preprocessor', cyAwaitPreprocessor())
},
},
})

Now you can write Cypress specs the way you probably wanted to write it all this time:

cypress/e2e/request.sync.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
it('checks the server using cy.request (sync)', () => {
const n = cy.request('GET', '/todos').its('body.length')
cy.visit('/')
if (n) {
cy.get('li.todo').should('have.length', n)
cy.contains('[data-cy=remaining-count]', n)
} else {
cy.log('no todos found')
cy.get('li.todo').should('have.length', 0)
cy.get('footer.footer').should('not.be.visible')
}
})

Successful request test

The plugin transforms every variable = cy.... assignment into cy....then(variable => ...) and moves all statements after the assignment into the callback.

1
2
3
4
5
6
7
8
9
10
11
12
13
// "sync" spec file
variable = cy....
exp1
exp2
exp3
...
// transpiled spec file
cy....then(variable => {
exp1
exp2
exp3
...
})

Examples

I have a few examples of using the plugin in my bahmutov/cypress-todomvc-await-example repo. It is almost too boring:

Spying on the network request to know how many items to find on the page

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
31
32
// network.sync.cy.js
it('shows the number of items returned by the server (sync)', () => {
cy.intercept('GET', '/todos').as('load')
cy.visit('/')
cy.get('.loaded')
const n = cy.wait('@load').its('response.body.length')
if (n) {
cy.get('li.todo').should('have.length', n)
cy.contains('[data-cy=remaining-count]', n)
} else {
cy.log('no todos found')
cy.get('footer.footer').should('not.be.visible')
}
})

// "Plain" Cypress syntax
it('shows the number of items returned by the server', () => {
cy.intercept('GET', '/todos').as('load')
cy.visit('/')
cy.get('.loaded')
cy.wait('@load')
.its('response.body.length')
.then((n) => {
if (n) {
cy.get('li.todo').should('have.length', n)
cy.contains('[data-cy=remaining-count]', n)
} else {
cy.log('no todos found')
cy.get('footer.footer').should('not.be.visible')
}
})
})

Comparing items on the page

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
// spec.sync.cy.js
it('shows the number of items (sync)', () => {
cy.visit('/')
cy.get('.loaded')
const $li = cy.get('li.todo').should(Cypress._.noop)
if ($li.length) {
cy.contains('[data-cy=remaining-count]', $li.length)
} else {
cy.log('no todos found')
cy.get('footer.footer').should('not.be.visible')
}
})

// "Plain" Cypress syntax
it('shows the number of items', () => {
cy.visit('/')
cy.get('.loaded')
cy.get('li.todo')
.should(Cypress._.noop)
.then(($li) => {
if ($li.length) {
cy.contains('[data-cy=remaining-count]', $li.length)
} else {
cy.log('no todos found')
cy.get('footer.footer').should('not.be.visible')
}
})
})

📺 Watch video Await Cypress Command Results