Cypress Local Storage Example

Access the "window.localStorage" in the right order during Cypress test.

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:

cypress/e2e/spec.cy.js
1
2
3
4
5
6
7
it('preserves the value', () => {
cy.visit('public/index.html')
cy.contains('#value', '0')
cy.contains('button#inc', '+1').click().click().click()
cy.contains('#value', '3')
// confirm the value stored in the local storage
})

The passing E2E test

Let's verify the value. We can attempt a direct synchronous Chai assertion:

1
2
3
4
5
6
7
8
9
// 🚨 BROKEN TEST
it('preserves the value', () => {
cy.visit('public/index.html')
cy.contains('#value', '0')
cy.contains('button#inc', '+1').click().click().click()
cy.contains('#value', '3')
// confirm the value stored in the local storage
expect(window.localStorage.getItem('value'), 'stored value').to.equal('3')
})

The test fails immediately

The test fails almost immediately

Hmm, where is the rest of the test commands? The Command Log is missing even the cy.visit command!

There were no "cy" commands

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
2
3
4
5
6
7
8
9
// 🚨 BROKEN TEST
// source code command command queue with arguments
cy.visit('public/index.html') // VISIT "public/index.html"
cy.contains('#value', '0') // CONTAINS "#value", "0"
cy.contains('button#inc', '+1') // CONTAINS "button#inc", "+1"
.click() // - CLICK
.click() // - CLICK
.click() // - CLICK
cy.contains('#value', '3') // CONTAINS "#value", "3"

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
2
3
4
5
6
7
8
9
10
// 🚨 BROKEN TEST
// source code command command queue with arguments
cy.visit('public/index.html') // VISIT "public/index.html"
cy.contains('#value', '0') // CONTAINS "#value", "0"
cy.contains('button#inc', '+1') // CONTAINS "button#inc", "+1"
.click() // - CLICK
.click() // - CLICK
.click() // - CLICK
cy.contains('#value', '3') // CONTAINS "#value", "3"
expect(...).to.equal('3') // runs the assertion immediately

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
2
3
4
5
6
7
8
9
10
11
// 🚨 BROKEN TEST
import 'cypress-command-chain'

it('preserves the value', () => {
cy.visit('public/index.html')
cy.contains('#value', '0')
cy.contains('button#inc', '+1').click().click().click()
cy.contains('#value', '3')
// confirm the value stored in the local storage
expect(window.localStorage.getItem('value'), 'stored value').to.equal('3')
})

The test still fails, but now I see the queued up commands.

Cypress commands were added to the queue but have not executed yet

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
2
3
4
5
6
7
8
9
10
11
// ✅ FIXED TEST
it('preserves the value', () => {
cy.visit('public/index.html')
cy.contains('#value', '0')
cy.contains('button#inc', '+1').click().click().click()
cy.contains('#value', '3')
// confirm the value stored in the local storage
.then(() => {
expect(window.localStorage.getItem('value'), 'stored value').to.equal('3')
})
})

The fixed test utilizes cy.then command

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
2
3
4
5
// starting code
cy.contains('#value', '3')
.then(() => {
expect(window.localStorage.getItem('value'), 'stored value').to.equal('3')
})

We are working with the application's window object. Good, we can use cy.window command to get it.

1
2
3
4
cy.contains('#value', '3')
cy.window().then((win) => {
expect(win.localStorage.getItem('value'), 'stored value').to.equal('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
2
3
4
5
6
cy.contains('#value', '3')
cy.window()
.its('localStorage')
.then((ls) => {
expect(ls.getItem('value'), 'stored value').to.equal('3')
})

Get the window and its local storage first

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
2
3
4
5
6
7
cy.contains('#value', '3')
cy.window()
.its('localStorage')
.invoke('getItem', 'value')
.then((value) => {
expect(value, 'stored value').to.equal('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
2
3
4
5
cy.contains('#value', '3')
cy.window()
.its('localStorage')
.invoke('getItem', 'value')
.should('equal', '3')

The implicit local storage value assertion

💡 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
2
3
4
5
6
7
8
9
10
// 🚨 BROKEN TEST
// can you fix this test? Why does it immediately show '90'?
it('uses the initial value', () => {
window.localStorage.setItem('value', '42')
cy.visit('public/index.html')
cy.contains('#value', '42')
window.localStorage.setItem('value', '90')
cy.reload()
cy.contains('#value', '90')
})

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
2
3
4
5
6
7
8
9
10
// ✅ FIXED TEST
it('uses the initial value', () => {
window.localStorage.setItem('value', '42')
cy.visit('public/index.html')
cy.contains('#value', '42').then(() => {
window.localStorage.setItem('value', '90')
})
cy.reload()
cy.contains('#value', '90')
})

We can rewrite the cy.then(callback) using cy.window, cy.its, cy.invoke commands to queue up the local storage update.

1
2
3
4
5
6
7
8
it('uses the initial value', () => {
window.localStorage.setItem('value', '42')
cy.visit('public/index.html')
cy.contains('#value', '42')
cy.window().its('localStorage').invoke('setItem', 'value', '90')
cy.reload()
cy.contains('#value', '90')
})

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.

Debugging the "cy.invoke" setItem method call

Nice.

🙋 Have a Cypress question? A flaky test? Ask me, I need topics for blog posts and videos.