Let's take a small application that stores its data in the window.localStorage
object. The value starts with 0 and the user can increment it by clicking the "+1" button. The value is written back to the local storage. A simply E2E test could look like this:
1 | it('preserves the value', () => { |
Let's verify the value. We can attempt a direct synchronous Chai assertion:
1 | // 🚨 BROKEN TEST |
The test fails immediately
Hmm, where is the rest of the test commands? The Command Log is missing even the cy.visit
command!
Seems like no cy
commands were executed by the test before the expect(...).to.equal('3')
assertion failed. Why is that?
Command Queue
Cypress does not run its commands immediately. Instead it adds all commands to the queue and then starts executing them, read my blog post Visualize Cypress Command Queue. In our original test before adding the assertion expect(...).to.equal('3')
the test callback executes once, and Cypress creates a queue of commands:
1 | // 🚨 BROKEN TEST |
Once the test callback finishes executing, the queue is ready to run. Cypress Test Runner starts executing one by one each command from the head of the queue. The queue lets Cypress re-run entire chains of commands and queries if an application is updating. Powerful, but might be counterintuitive, especially compared to async / await
syntax. In our example, adding the expect(...).to.equal('3')
breaks the start of the execution. Even if the assertion is at the end of the test callback function, it executes immediately, throwing an error. The test has not started running, the Test Runner has not executed the "VISIT public/index.html" command yet! Thus the local storage is empty.
1 | // 🚨 BROKEN TEST |
You can see the queue by using my plugin cypress-command-chain. Once I installed the plugin, I simply import it from the spec file.
1 | // 🚨 BROKEN TEST |
The test still fails, but now I see the queued up commands.
Ok, our expect(...).to.equal('3')
assertion ran too early. We want to execute all Cypress commands and then run the assertion. This is the use case for cy.then command. Really, it should have been called cy.later.
1 | // ✅ FIXED TEST |
Nice, the synchronous assertion is now going to execute AFTER Cypress commands.
Implicit assertion
let's improve it a little. Notice that the Cypress Command Log does not show anything inside the callback. We only see the assertion itself, but we don't know where the value "3" came from. We don't "see" the window.localStorage.getItem
method call at all, since it is not tied to any Cypress command. Let's change it, and it will make our lives much easier and even remove the cy.then
need.
Let's rewrite the entire expression:
1 | // starting code |
We are working with the application's window
object. Good, we can use cy.window command to get it.
1 | cy.contains('#value', '3') |
From the win
object yielded by the cy.window
command we use its property localStorage
. We can do it using the cy.its command.
1 | cy.contains('#value', '3') |
We now see some of the steps reflected in the Command Log, and can even debug these steps. Let's continue. We call the local storage's method getItem
. There is a Cypress command for that: cy.invoke.
1 | cy.contains('#value', '3') |
The remaining callback receives the value from the chain of Cypress commands and all it does is to call expect(value).to.equal('3')
. We can convert the explicit assertion to the implicit assertion should(assertion)
.
1 | cy.contains('#value', '3') |
💡 Explicit and implicit assertions are equivalent. Implicit assertions in Cypress call the explicit Chai assertion passing the subject value.
1
2
3
4
5
6 cy.wrap(42).should('equal', 42)
// similar code as this
cy.wrap(42)
.then(value => {
expect(value).to.equal(42)
})Implicit assertions have one small and one huge advantages. They are more readable since there is less code (a small benefit), and Cypress can retry evaluating the command chain if the assertion fails (a huge advantage). See Cypress Retry-ability and Cypress V12 Is A Big Deal.
Set the local storage value
Now that you understand the Cypress command queue, you can fix the following failing test.
1 | // 🚨 BROKEN TEST |
We can leave the initial window.localStorage.setItem('value', '42')
as is. It is fine if we set the local storage before any Cypress commands are added to the queue or executed. But we need to set the second value "90" after we checked the page using the command cy.contains('#value', '42')
. We can use cy.then
callback
1 | // ✅ FIXED TEST |
We can rewrite the cy.then(callback)
using cy.window
, cy.its
, cy.invoke
commands to queue up the local storage update.
1 | it('uses the initial value', () => { |
We can now see each command for setting the local storage. We can even debug the cy.invoke
command, for example. Click on the "invoke" command to see the arguments and the actual local storage object reference in the DevTools.
Nice.
🙋 Have a Cypress question? A flaky test? Ask me, I need topics for blog posts and videos.