This is a very common question and comes up at least every couple of days on the Cypress chat channel.
Imagine you have an element on the page
1 | <div id="username">Mary</div> |
You would like to print the text from the element #username
to Cypress Command Log. You know that Cypress commands are asynchronous, so you place the value into a variable before calling cy.log
command.
1 | it('prints the text', () => { |
Hmm, the Command Log prints NULL.
Let's fix it!
🎁 You can find the source code for this blog post in the repo bahmutov/cypress-log-example.
If you would rather watch the explanation than read it, I have recorded a short video below.
Tip: for more videos about Cypress subscribe to my YouTube channel
The root cause
Cypress commands are asynchronous, but they are also chained first. When Cypress runs through the test the first time, it only internally queues the commands for execution. Every command is stored as an object in Cypress memory together with its arguments. In JavaScript, when you call a function, the primitive arguments are passed by value. Each argument's value at the moment of the call is copied and passed into the function. Let's write down as a comment the command and its argument as stored in memory. For example, cy.visit('index.html')
will become an object with the command "VISIT" to run and an argument string index.html
. This object is stored in Cypress command chain.
1 | // test code chained command with arguments |
At the moment when cy.log(username)
is called, the value of the argument is given by the variable username
. JavaScript looks up the current value, sees null
and then calls cy.log(null)
. That is JavaScript semantics - it has nothing to do with Cypress' logic.
The solution
We need to delay calling cy.log(username)
until the variable username
has a value. One solution is to move calling cy.log
into its own .then
block after the .then(($el) => (username = $el.text()))
finishes.
1 | cy.visit('index.html') |
We do not need a separate .then
callback, we can simply log the text immediate as we receive it.
1 | cy.visit('index.html') |
In this case, we do not even need a variable
1 | cy.visit('index.html') |
We do not even need a .then
callback. We can invoke the method text
and pass the result to the cy.log
method.
1 | cy.visit('index.html') |
Bonus: see the order of command chaining and execution
You can print each Cypress command as it is added to the chain of commands in memory by subscribing to the Cypress command events. You can even print the commands at the start and at the end of their actual execution.
1 | it.only('prints null with event trace', () => { |
The DevTools console shows the log
command without an argument
Let's see the trace for the corrected test
1 | it('prints text with event trace', () => { |
Notice in the trace that LOG
command was enqueued only after the command invoke('text')
has ran. By the time cy.log(s)
is added to the queue, the value s
exists and is passed to the call.
Bonus 2: cypress-command-chain
To better see the Cypress commands and their parameters in the queue, I have created a plugin called cypress-command-chain. Read the blog post Visualize Cypress Command Queue.
More info
- replace
cy.log
withcy.print
A Better Cypress Log Command - read Cypress Tips and Tricks