Cypress: Waiting


Cypress comes with a built-in functionality that makes it retry every command several times before it times out (with a default time out of 4000ms). Because of this built-in functionality, Cypress is capable to wait by itself until a desired state is reached (e.g. a button becomes enabled).

To see this behavior we can prepare a simple example (Check $Tag.Cypress.SimpleTestEnvironment on how to prepare a simple test application).   First, we prepare a HTML file e.g. home.html with the following content:

<html lang="en">
<head>
    <title>A simple HTML document</title>
	<script>
		function enableButton1()	{
			console.log("enableButton1")
			var checkbox1 = document.getElementById('checkbox1');
			var button1 = document.getElementById('button1')
			if (checkbox1.checked == true) {
				button1.disabled = false;
				console.log("Button enabled");
			}
			else {
				button1.disabled = true;
				console.log("Button disabled");	
			}
		}
	</script>
</head>
<body>
    <h1>Example 1<h1>
	<input type="checkbox" onclick="enableButton1()"  id="checkbox1" />
	<button type="button" id="button1" disabled>Button1</button> 
</body>
</html>

This is basically a simple html page with a check box and a button. If we click on the check box the button gets enabled or disabled accordingly:

Next we create a simple cypress script to do a simple test on the web page we just created. Prepare a new cypress script (e.g. example1.js) with the following content:

describe('example1', () => {
  beforeEach(() => {
    cy.visit('../../../home.html')
  })

  it('Test the button', () => {
    cy.get('#checkbox1').click()
    cy.get('#button1').should('not.be.disabled')
  })
})

So let’s execute this cypress test and see what happens:

If everything works as expected you should be able to see, that our cypress test passes through without any problems.

So, this example was quite a strait forward because we didn’t have any significant delays involved. So every cypress command had an immediate effect so that when cypress moved fast forward the expected new states were immediately available.

Now we change the tested website a bit to simulate some kind of delays so that if our cypress test checks the checkbox the buttons state is toggled after a delay of 3000ms. With this delay, if cypress would not wait for the state of the button to change, the test would fail. But as we stated before Cypress waits per default 4000ms during which it checks the expected state of the affected element several times. As soon as the expected state is met Cypress moves on with the next command.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>A simple HTML document</title>
	<script>
		function enableButton1() {
			console.log("enableButton1")
			var checkbox1 = document.getElementById('checkbox1');
			var button1 = document.getElementById('button1')
			if (checkbox1.checked == true) {
				console.log("enabling button");
				setTimeout(delayedEnableButton, 3000);
			}
			else {
				console.log("disbling button");
				setTimeout(delayedDisableButton, 3000);
			}
		}
		
		function delayedEnableButton() {
			console.log("time passed");
			button1.disabled = false;
			console.log("Button enabled");
		}
		
		function delayedDisableButton() {
			console.log("time passed");
			button1.disabled = true;
			console.log("Button disabled");
		}
	</script>
</head>
<body>
    <h1>Example 1<h1>
	<input type="checkbox" onclick="enableButton1()"  id="checkbox1" />
	<button type="button" id="button1" disabled>Button1</button> 
</body>
</html>

So lets restart our cypress test:

So, we can see that this time cypress really waits for the state of the button to reach the expected state. As soon as this is the case, after 3000ms, Cypress detects this and moves on.

This little example shows that Cypress is capable to cope with delays out of the box, without us having to care about that, for example by having to use cy.wait() or something.

Waiting by adjusting timeouts

While Cypress has the built-in functionality to keep trying the same command there are of course limits to this. To see the limits, we just have to increase the delays in our test page to something bigger than the default timeout of cypress which is 4000ms. So let’s change the delays to 5000ms and see what happens.

Adjust the home.html file as follows:

Replace
setTimeout(delayedEnableButton, 3000);
by
setTimeout(delayedEnableButton, 5000); Restart the Cypress test:

As we can see, this time the test fails, because we have overwhelmed the patience of cypress 😊 which is per default 4000ms.

One way to resolve this issue would be to increase the default timeout of Cypress. This could be done globally in the cypress.json file as follows:

cypress.json
{
  "defaultCommandTimeout": 6000
}

In this case, cypress would by default wait for 6000ms, and with that our test would pass because our delays are only 5000ms.

But this would have a negative side effect. By doing this, our test would take longer to fail in case of a real error. In case of multiple tests and multiple errors, the delay of each failed test would add up to the overall delay of the who test run.

Of course, we could define the timeout locally (for a specific test) instead of globally (for all tests). This could also be done per “describe” block:

describe('example1', { defaultCommandTimeout: 6000 },() => {
  beforeEach(() => {
    cy.visit('../../../home.html')
  })

  it('Test the button', () => {
    cy.get('#checkbox1').click()
    cy.get('#button1').should('not.be.disabled')
  })
})

Or per “it” block:

it('Test the button', { defaultCommandTimeout: 6000 }, () => {
    cy.get('#checkbox1').click()
    cy.get('#button1').should('not.be.disabled')
})

And we could go even more granular by setting the timeout per command:

cy.get('#button1', { timeout: 6000 }).should('not.be.disabled')

Waiting in case of page reload

There are some scenarios where Cypress’s waiting behavior might be quite confusing. One such scenario is if a page redirect happens during a test run.

We can try to check Cypress behavior in case of page reload.

So, first, prepare a web page with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>A simple HTML document</title>
    <script>
        function redirect1() {
            console.log("redirect1");
            var input1 = document.getElementById('input1')
            input1.disabled = false;
        }     
    </script>
</head>
<body>
    <h1>Example 1<h1>
    <button type="button" id="button1" onclick="redirect1()" >Button1</button> 
    <input type="text" id="input1" name="input1" disabled>
</body>
</html>

And then prepare a cypress script with the following content:

describe('example1', () => {
  beforeEach(() => {
    cy.visit('../../../home.html')
  })

  it('Test the button', () => {
    cy.get('#button1').click()
    cy.get('#input1').type('xyz')
  })
})

If we run that test cypress script against the page we prepared above, we get a result as this:

Now we make a slight change to see how a page reload can affect cypress behavior. For this we change the page content as follows:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>A simple HTML document</title>
    <script>
        function redirect1() {
            console.log("redirect1");
            var input1 = document.getElementById('input1')
            input1.disabled = false;
            location.replace("home.html");
        }     
    </script>
</head>
<body>
    <h1>Example 1<h1>
    <button type="button" id="button1" onclick="redirect1()" >Button1</button> 
    <input type="text" id="input1" name="input1" disabled>
</body>
</html>

If we run the same cypress script again it will fail:

It seems that in case of a page reload cypress loses its ability to wait for a condition to become true.