Add Types To Your Local Storage During End-to-End Tests
Published on September 16, 2025
If your web application stores data locally in the localStorage object, you can easily set / verify the data from your Cypress end-to-end tests. For example:
it('stores new players in local storage', () => { cy.visit('/player/') cy.get('[name=firstName]').should('be.focused').type('Joe') cy.get('[name=lastName]').type('Smith') cy.contains('button', 'Add player').click()
The test above uses the page to add new soccer players and then verifies the local storage has the expected "shape" and data.
📦 The code examples in this blog post come from my private "Gametime" repo which has a web app for tracking the soccer games. I wrote this app to help me coach the Cambridge youth soccer teams. I am thinking how to better open source this application, but everyone can use the app to track time and game results: glebbahmutov.com/gametime/
We can also set the local storage before loading the page; this makes the tests run much faster, since they don't have to recreate the state via UI.
The test confirms that the page can read the players from the local storage on page visit and shows each player's name.
Problems
The above test has a few issues:
the window.localStorage.setItem method is synchronous and runs before any Cypress command executes. This can lead to the unexpected results. For example, we could test the page reload and it would NOT work as written
1 2 3 4 5 6 7 8 9 10 11 12
// 🚨 INCORRECT, THIS TEST WILL NOT AS EXPECTED
// two different lists of players const players1 = [...] const players2 = [...] window.localStorage.setItem('players', JSON.stringify(players1)) cy.visit('/player/') // confirm we see players from the list "players1"
window.localStorage.setItem('players', JSON.stringify(players2)) cy.reload() // confirm we see the players from the list "players2"
Do you think this test works as intended? No. It will fail checking the players from the list "players1". For some reason, after the cy.visit command finishes you see the players ... from list 2! This is because the two localStorage.setItem commands execute immediately, while cy commands are queued up. So when the cy.visit command runs, the local storage already has the players2 list set.
Another problem is having pieces of JSON objects across multiple tests and specs. Many tests in this web app have players, teams, and game in progress pieces of state set before we visit the page
Some objects are simple, like a single player. Some are more complex and changing. For example, the "prepared game" becomes a game in progress with lots of fields:
constinitialGameInProgress: GameInProgress = { teamId, teamName: '', fieldPlayerIds: [] asstring[], subPlayerIds: [] asstring[], otherPlayerIds: [] asstring[], gameIsRunning: false, // game start, ms since epoch gameStartedAt: 0, /** * Time duration since the start of the game, ms */ gameClockMs: 0, gameClockText: '00:00', /** * for each player id, the total time they played, ms */ playersGameTime: {} asRecord<string, number>, subsToDo: [] asSubToDo[], gameFinished: false, ourTeamGoals: 0, opponentTeamGoals: 0, /** notes about individual goals */ notes: [] asGameNote[], opponentName: 'them', /** current field player positions, like "Goalkeeper" or "Center striker" */ playerPositions: {} asRecord<string, string>, }
If the type GameInProgress changes, we would need to check all tests that use window.localStorage.setItem('prepare-game', ...) to ensure the value is a valid object.
Solution
My preferred solution to both problems is writing custom Cypress commands with explicit typed interface. Here is how it looks in practice. First, we will provide several simple Cypress commands wrapping localStorage access.
The commands file is included from the Cypress support file together with plugins
cypress/support/e2e.ts
1 2 3 4
import'cypress-map' import'cypress-plugin-steps'
import'./commands'
Using the custom Cypress commands automatically ensures the data is in the right order between other Cypress commands.
1 2 3 4 5 6 7 8 9 10 11 12
// ✅ THE CORRECT TEST
// two different lists of players const players1 = [...] const players2 = [...] cy.setLocalStorage('players', players1) cy.visit('/player/') // confirm we see players from the list "players1"
cy.setLocalStorage('players', players2) cy.reload() // confirm we see the players from the list "players2"
In the test above, the second list players2 will be written into the local storage after all previous Cypress commands have finished.
Second, we describe the allowed types for these commands
Because my end-to-end tests reside in the same repo with the rest of the application code, I can reuse the "official" types and even TypeScript aliases.
If someone passes an object that does not satisfy the Team interface when using cy.setLocalStorage('teams', ...), TypeScript compiler will complain. For example, if the team object I am trying to set in the local storage is missing the id property: