A Better Cypress Log Command

I have a lot of problems with the cy.log command and now I wrote a replacement called cy.print.

The Cypress command cy.log is bad. Not like "ohh, it will print nothing unless you understand the asynchronous nature of Cypress command chains", but in the sense "cy.log does not do what you expect it to do, and when it does it is broken" kind-of-bad. Let me explain.

cy.log problems

Let's say, the page has an element with the user's age. You want to print the age to the Command Log. You might try doing the following:

1
2
3
4
5
it('logs the user age (NOT)', () => {
cy.visit('cypress/log-examples.html')
cy.contains('#age', /\d+/).invoke('text').then(Number)
// what's my age again?
})

How do we print the value yielded by the command chain?

You might think that chaining .log command at the end of the chain:

1
2
3
4
5
6
7
8
it('logs the user age (NOT)', () => {
cy.visit('cypress/log-examples.html')
cy.contains('#age', /\d+/)
.invoke('text')
.then(Number)
// what's my age again?
.log()
})

Right, it does nothing.

By default cy.log prints nothing

Ok, my trick to print the current subject is to assert it.

1
2
3
4
5
6
7
8
it('logs the user age (NOT)', () => {
cy.visit('cypress/log-examples.html')
cy.contains('#age', /\d+/)
.invoke('text')
.then(Number)
// what's my age again?
.should('be.within', 1, 99)
})

Using an assertion to print the current value

Tip: the more assertions you sprinkle through your command chains, the less test flake you will have.

Ok, that works, here is where cy.log can stub you again. Let's say you want to log the text before converting it to a number. You might think that adding .then(cy.log) into the chain would work.

1
2
3
4
5
6
7
8
9
it('logs the user age (NOT)', () => {
cy.visit('cypress/log-examples.html')
cy.contains('#age', /\d+/)
.invoke('text')
.then(cy.log)
.then(Number)
// what's my age again?
.should('be.within', 1, 99)
})

Where is the zero coming from?!!!

"Get out of here!" screams the cy.log. Weird, right? Why is the "age" zero? Let's replace .then(cy.log) with .then(console.log)

1
2
3
4
5
6
7
8
9
it('logs the user age (NOT)', () => {
cy.visit('cypress/log-examples.html')
cy.contains('#age', /\d+/)
.invoke('text')
.then(console.log)
.then(Number)
// what's my age again?
.should('be.within', 1, 99)
})

Strange, this works correctly.

Console log shows the expected value

Ohhh, yes. Cypress v10 broke cy.then + cy.log combination. The command cy.then yields the original subject IF the callback function yields undefined. For some reason, Cypress v10 switched cy.log from returning undefined to returning null, breaking all the code in my tests.

cy.log does not try too hard

Or at all. The cy.log command is not a query command, thus it will break any retries. Here is a test that works - it automatically retries getting the property name of the given object until the assertion passes.

1
2
3
4
5
6
7
it('retries (NOT)', () => {
const person = {}
cy.wrap(person).its('name').should('equal', 'Ann')
setTimeout(() => {
person.name = 'Ann'
}, 1000)
})

Cypress queries retry until the assertion passes

If we insert cy.log between the object and the cy.its command, our retries will break.

1
cy.wrap(person).log().its('name').should('equal', 'Ann')

cy.log replaces the object with null

Same if we use cy.wrap(person).cy.then(cy.log) - cy.then is not a query and stops retries.

Do not try to log an object

Let's say you spied on the network call and watch to log the server response object.

1
2
3
4
5
it('logs the object (NOT)', () => {
cy.intercept('/users').as('users')
cy.visit('cypress/log-examples.html')
cy.wait('@users').its('response.body').then(cy.log)
})

The endpoint returns an array of small objects

1
2
3
4
5
[
{ name: 'Joe', age: 1, role: 'student' },
{ name: 'Ann', age: 2, role: 'student' },
{ name: 'Mary', age: 3, role: 'student' },
]

Here is how cy.log prints them

Only short objects are fully serialized

Unfortunately, you cannot control serialization. My trick is to use an assertion again with the custom long truncate threshold.

1
2
3
4
chai.config.truncateThreshold = 200

cy.visit('cypress/log-examples.html')
cy.wait('@users').its('response.body').should('be.an', 'array')

Using Chai assertion to serialize and print the object

cy.print to the rescue

Cypress v12 has introduced the concept of query commands, which are a big deal. I have written an entire NPM package cypress-map of useful queries you can start using today. The latest addition to cypress-map is the cy.print command. Install the plugin as a dev dependency:

1
2
3
$ npm i -D cypress-map
# or using Yarn
$ yarn add -D cypress-map

Let's see if we can just insert cy.print into our first test.

1
2
3
4
5
6
7
8
9
it('prints the user age', () => {
cy.visit('cypress/log-examples.html')
cy.contains('#age', /\d+/)
.invoke('text')
.print()
.then(Number)
// what's my age again?
.should('be.within', 1, 99)
})

cy.print works by default

Even better, we can format the message using printf syntax, and the current subject value is inserted automatically

1
2
3
4
5
6
7
8
9
it('prints the user age', () => {
cy.visit('cypress/log-examples.html')
cy.contains('#age', /\d+/)
.invoke('text')
.print('my age is %d')
.then(Number)
// what's my age again?
.should('be.within', 1, 99)
})

cy.print with a custom message string

Let's say we want to log the array from the network intercept. By default cy.print serializes even long objects.

1
cy.wait('@users').its('response.body').print().should('be.an', 'array')

cy.print prints the entire array

But we probably do not want to see the entire array. Maybe we want to just see its length, and maybe the first object. cy.print lets you access the properties inside using {0.} notation.

1
2
3
4
5
cy.wait('@users')
.its('response.body')
.print('list with {0.length} users')
.print('first user {0.0.name}')
.should('be.an', 'array')

Prints subject properties

Finally, cy.print is a query, so it can be part of the chain that is retried, and it prints the current value.

1
2
3
4
5
6
7
8
9
10
11
12
it('retries', () => {
const person = {
name: 'Joe',
}
cy.wrap(person)
.print('first name is {0.name}')
.its('name')
.should('equal', 'Ann')
setTimeout(() => {
person.name = 'Ann'
}, 1000)
})

cy.print query retries when the entire chain retries

Bonus feature: pass your own callback to return the string to print. For anything more complicated, you can pass the callback to cy.print to return the string to print into the Command Log. Want to print just the first names from the array returned by the intercept?

1
2
3
4
5
cy.visit('cypress/log-examples.html')
cy.wait('@users')
.its('response.body')
.print((list) => `first names only ${list.map((l) => l.name).join(',')}`)
.should('be.an', 'array')

Format your own messages from the subject using cy.print callback

Nice!