Visit The Blank Page Between Cypress Tests

Stop the web application and clearly separate the end-to-end tests by visiting the blank about page after each test.

When a Cypress test finishes, the web application stays in the browser's window. This could be confusing if the next test does not immediately start with cy.visit. For example, the JavaScript callbacks from the application visited in the first test are still executing, and could "leak" into the second test. Imagine the application scheduling code to execute after a delay:

src/App.js
1
2
3
4
5
6
console.log('rendering app')
ReactDOM.render(<App />, document.getElementById('root'))

setTimeout(() => {
console.log('running app code')
}, 100)

Imagine the test confirming the number of console logs calls. The application is printing a message on start up and when adding a todo. The first test successfully passes

cypress/integration/log-spec.js
1
2
3
4
5
6
7
8
9
10
11
it('logs message on startup', () => {
cy.visit('/', {
onBeforeLoad(win) {
cy.spy(win.console, 'log').as('log')
},
})

// the app has loaded
cy.get('.todo').should('have.length', 3)
cy.get('@log').should('have.been.calledOnce')
})

The first test confirms the "console.log" was called exactly once

In the screenshot above notice the second log call. It happens after the test has already finished and thus does not affect our assertion cy.get('@log').should('have.been.calledOnce').

📺 If you prefer watching the explanation to reading this blog post, I have recorded the video Visit The Blank Page Between The Tests.

Let's add a second test that confirms the console.log is called when adding a new Todo item.

cypress/integration/log-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
it('logs message on startup', () => {
cy.visit('/', {
onBeforeLoad(win) {
cy.spy(win.console, 'log').as('log')
},
})

// the app has loaded
cy.get('.todo').should('have.length', 3)
cy.get('@log').should('have.been.calledOnce')
})

it('logs message when adding a todo', () => {
// the spies and stubs are reset before each test
// thus we need to spy on the console again
cy.window()
.its('console')
.then((console) => {
cy.spy(console, 'log').as('log')
})

// the app has loaded
cy.get('.todo').should('have.length', 3)
cy.get('[data-cy=new-todo]').type('hello{enter}')
cy.get('@log').should('have.been.calledOnce')
})

The second test fails - there is an extra console.log call that now is included in the second test.

The second test fails due to the callback from the first test

In my case, if the application uses a delay of 30ms when calling the setTimeout, the application because flaky - sometimes the tests pass and sometimes they fail.

src/App.js
1
2
3
setTimeout(() => {
console.log('running app code')
}, 30) // flaky value

The test can pass or fail randomly

I do not have to tell you, how frustrating flaky tests are.

📚 Read my other blog posts about flaky tests on Cypress blog.

A similar situation when the app does something unexpected due to the previous test can happen for other reasons. For example, a long-running network requests can finish and unexpectedly update the app. At best, the application's behavior can be hard to explain. At worst, you can get the dreaded this element is detached from the DOM error.

Solution: visiting a blank page

A good solution to clearly separate the tests and stop any application callbacks is to visit a "neutral" blank page. Unfortunately, using the cy.visit('about:blank') would not work.

cypress/integration/log-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
afterEach(() => {
cy.visit('about:blank')
})

it('logs message on startup', () => {
cy.visit('/', {
onBeforeLoad(win) {
cy.spy(win.console, 'log').as('log')
},
})

// the app has loaded
cy.get('.todo').should('have.length', 3)
cy.get('@log').should('have.been.calledOnce')
})

it('logs message when adding a todo', () => {
// the spies and stubs are reset before each test
// thus we need to spy on the console again
cy.window()
.its('console')
.then((console) => {
cy.spy(console, 'log').as('log')
})

// the app has loaded
cy.get('.todo').should('have.length', 3)
cy.get('[data-cy=new-todo]').type('hello{enter}')
cy.get('@log').should('have.been.calledOnce')
})

Notice the URL in the browser - we did NOT visit the about:blank page, instead we have visited our baseUrl + about:blank!

cy.visit does not work with about:blank address

We need another way of visiting the blank page. We can use the window.location = 'about:blank' instead. Note: we also need to visit the page in every test.

cypress/integration/log-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
beforeEach(() => {
cy.visit('/', {
onBeforeLoad(win) {
cy.spy(win.console, 'log').as('log')
},
})
// the app has loaded
cy.get('.todo').should('have.length', 3)
})

afterEach(() => {
cy.window().then((win) => {
win.location.href = 'about:blank'
})
})

it('logs message on startup', () => {
cy.get('@log').should('have.been.calledOnceWithExactly', 'rendering app')
})

it('logs message when adding a todo', () => {
cy.get('@log').invoke('resetHistory') // reset the spy
cy.get('[data-cy=new-todo]').type('hello{enter}')
cy.get('@log').should('have.been.calledOnceWithExactly', 'added todo')
})

The passing tests show the expected calls - and nothing else.

The first test no longer leaks console log calls into the second test

The callback from the application is truly canceled. You can confirm it is never executed by looking at the DevTools console. The log message "running app code" is never printed, because the JavaScript VM executing the application code for localhost:3000 is stopped.

The DevTools console never shows the message "running app code" from the app callback

Notice the application page is blank - because we visit the blank pages after each test. We could leave the application running and instead visit the blank page before each test. In that case make sure the about:page callback is the very first callback executed for each test. A good idea is to place it into the support file, because that file is always loaded before the spec file loads.

cypress/support/index.js
1
2
3
4
5
beforeEach(() => {
cy.window().then((win) => {
win.location.href = 'about:blank'
})
})
cypress/integration/log-spec.js
1
2
3
4
5
6
7
8
9
beforeEach(() => {
cy.visit('/', {
onBeforeLoad(win) {
cy.spy(win.console, 'log').as('log')
},
})
// the app has loaded
cy.get('.todo').should('have.length', 3)
})

Nice.