Cypress Cannot Add Out-Of-Band Commands

While Cypress test is running you cannot insert or add new test commands from "outside"

Single chain of commands

Cypress test acts like a human user interacting with the web page. A human cannot click two buttons at once - the user can only do one action at a time. Thus the test runner always runs a single command at a time. A command has to fully finish before the next command starts. All commands are queued up at the start of the test, and then execute one by one.

1
2
3
cy.get('selector') // first command
.find('another selector') // second
.click() // third

At the start of the test all commands are "read" and added to the queue

1
2
3
4
# Queue of commands
1. GET "selector"
2. - FIND "another selector"
3. - CLICK

Then the test starts running, reading the commands one by one from the queue and executing them.

👀 You can see the queue by using the plugin cypress-command-chain, read the blog post Visualize Cypress Command Queue.

Dynamic command chain

A command can add other Cypress commands by inserting them into the command chain. For example, here is a conditional test that clicks on the element IF it finds the parent element:

1
2
3
4
5
6
7
8
9
10
11
cy.get('selector')
// disable the built-in "should exist" assertion
.should(Cypress._.noop)
.then($el => {
if ($el.length) {
cy.log('Found the element')
cy.wrap($el)
.find('another selector')
.click()
}
})

Here is the command chain of known commands at the start of the test:

1
2
3
# Queue of commands
1. GET "selector"
2. - THEN callback

That's it. Cypress does not know what is inside the callback function yet. The test starts running, executes the GET "selector" command, and passes the jQuery object (which could be empty) to the "THEN callback" command. Cypress then adds new commands depending on the if ($el.length) condition. If there are no elements, it simply finishes the callback, completes the THEN callback commands and finishes the test. But if there is an element there, Cypress queues up the new commands:

1
2
3
4
5
6
7
# Updated queue of commands
1. GET "selector" | done
2. - THEN callback | running
3. LOG "Found ..."
4. WRAP $el
5. - FIND "another selector"
6. - CLICK

Cypress completes the "THEN callback" command and moves on to the next command in the queue "LOG Found ...", etc.

Out-of-band commands

Cypress commands like "THEN callback" can add new commands to the queue. What about if some other test code tries to add a new command? For example, what if we do the following

1
2
3
4
cy.wait(10_000) // sleep 10 seconds
setTimeout(() => {
cy.log('New command!')
}, 5_000)

You will get an error, Cypress does not know where this command is meant to join the queue.

Cypress throws an error when trying to add an out-of-band command

The same can happen when you try to add new commands from Cypress.on or cy.intercept(..., req => ...) callbacks.

1
2
3
4
5
6
cy.intercept('/ajax', req => {
req.continue(res => {
// 🚨 not going to work
cy.log('got response!')
})
})

Nope, the callback res => { ... } happens while some other Cypress command is running and it cannot insert the cy.log correctly.

Use the cy.then command instead

Instead of using setTimeout... you can use cy.then to add or insert new Cypress commands into the queue.

1
2
3
4
5
6
cy.log('first').then(() => {
// insert more commands
// before the "LOG third" command
cy.log('second')
})
cy.log('third')

The new log command was inserted between two existing commands

Adding new Cypress commands from cy.then is super useful for conditional testing and recursive tests.

Add your own queue

You can even store all the data for the commands you want to run and then schedule them for later using the cy.then command. For example, if you want to print the network data from the server, instead of trying to use cy.log(...) from the cy.intercept callback, store the response data and then print it later:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cy.intercept('/ajax', req => {
req.continue(res => {
// 🚨 not going to work
cy.log('got response!', res.body)
})
})
// ✅ store the responses to be printed later
const responses = []
cy.intercept('/ajax', req => {
req.continue(res => {
responses.push(res.body)
})
})
// test commands
// now ensure there are some responses
// and print them
cy.wrap(responses).should('not.be.empty')
.then(() => {
responses.forEach(r => {
cy.log('got response!', r)
})
// if we want to print more responses later
responses.length = 0
})

Hope it helps.