Solve Flake In Cypress Typing Into An Input Element

How to work around flaky application when using cy.type and then solve the flake for good.

Imagine an application that resets the input field on start up. The resets are random but usually happen within the first 200-300 milliseconds. The application code looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
<input type="text" id="flaky-input" value="" />
<script>
function resetText() {
const input = document.getElementById('flaky-input')
input.value = ''
}
// reset the input several times during the first couple of seconds
setTimeout(resetText, 100)
setTimeout(resetText, 150)
setTimeout(resetText, 200)
setTimeout(resetText, 250)
</script>

The Cypress test simply tries to type into the input field.

1
2
3
4
5
it('is flaky without retries', () => {
cy.visit('/')
const text = 'hello there, friend!'
cy.get('#flaky-input').type(text).should('have.value', text)
})

The test video shows the flaky behavior - the first characters simply disappear.

The first characters entered by cy.type disappear and the test fails

🎁 You can find the above example amongst the test examples in the [bahmutov/cypress-recurse][cypress-recurse] repo.

Workaround using cypress-recurse

If you are a real user, and you see some of the characters disappear as you type, you would curse, clear the input field, and type the text again. We can do the same using a plugin I wrote cypress-recurse. This plugin performs any provided Cypress commands until the predicate function returns true or the entire command times out. Let's update our spec to make it flake-free:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { recurse } from 'cypress-recurse'

it('enters the text correctly', () => {
cy.visit('/')
const text = 'hello there, friend!'

recurse(
// the commands to repeat, and they yield the input element
() => cy.get('#flaky-input').clear().type(text),
// the predicate takes the output of the above commands
// and returns a boolean. If it returns true, the recursion stops
($input) => $input.val() === text,
)
// the recursion yields whatever the command function yields
// and we can confirm that the text was entered correctly
.should('have.value', text)
})

The Cypress Command Log shows how the test types in the characters the first time - but then the part of the input disappears. The predicate function ($input) => $input.val() === text returns false. The recurse function then repeats the first act function again, clearing the input and typing the entire string, just like a real user does. On the second attempt, the entire text is preserved, and the predicate function returns true, completing the step.

Cypress-recurse types the entire string again

We can control the recurse function through an options object argument. For example, we can delay each iteration and log less information.

1
2
3
4
5
6
7
8
9
10
11
recurse(
// the commands to repeat, and they yield the input element
() => cy.get('#flaky-input').clear().type(text),
// the predicate takes the output of the above commands
// and returns a boolean. If it returns true, the recursion stops
($input) => $input.val() === text,
{
log: false,
delay: 1000,
},
)

The test without extra logging

Preventing the flake

Using the cypress-recurse plugin in this case only works around the application's behavior. The real user would see the same broken application. It is better to prevent the input until the application is ready. Thus I advise to add a disabled attribute to the input element, and only remove it after the application is ready to process the input without clearing it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<input type="text" id="flaky-input" value="" disabled />
<script>
function resetText() {
const input = document.getElementById('flaky-input')
input.value = ''
}
function enableText() {
const input = document.getElementById('flaky-input')
input.removeAttribute('disabled')
}
// reset the input several times during the first couple of seconds
setTimeout(resetText, 100)
setTimeout(resetText, 150)
setTimeout(resetText, 200)
setTimeout(resetText, 250)
// enable the input element after the application
// is truly ready to process the user actions
setTimeout(enableText, 2000)
</script>

The test that uses cypress-recurse still works in this case, but much more important - the original test now works without any flake!

1
2
3
4
5
it('is waiting for the input element to become enabled', () => {
cy.visit('/')
const text = 'hello there, friend!'
cy.get('#flaky-input').type(text).should('have.value', text)
})

The cy.type command simply waits for the input element to be enabled (it is a built-in actionability check) before typing. You can see the input grayed out and the TYPE command waiting for two seconds until it starts typing in the video below.

The test without flake

If the application does not let the test runner interact with it until it is ready, the flake problem never appears, so that is the best solution in my opinion.

Videos

You can watch me solve the flake using the cypress-recurse plugin in the video below

Then watch the video of solving the flake problem for real by disabling the input element until the application is ready to receive the user's actions.

For more videos like this with Cypress tips and solutions, subscribe to my video channel at https://www.youtube.com/glebbahmutov.

More information