Testing Svelte Store

Writing E2E tests for Svelte.js app store using Cypress.

Svelte.js is a very interesting "disappearing" framework for writing web applications. Recently, the framework's crew came out with a simple centralized data store to be used with Svelte apps. The initial "Hello World" example application had a bug 🙃 immediately fixed by a pull request. But this immediately showed me an opportunity to write a few end to end tests to prevent any future problems. Of course I picked Cypress.io (full disclosure, I am VP of Engineering at Cypress) - an open source MIT-licensed test runner.

Before (and if) my pull request with E2E tests is merged into the main repo sveltejs/template-store, you can find the source and tests in bahmutov/template-store repository.

To run the web application we need to execute npm run dev - this runs a static web server and Rollup bundler in watch mode. By default the app runs at http://localhost:5000. I have installed Cypress with npm i -D cypress (or just with yarn add cypress) and opened it once with $(npm bin)/cypress open. This scaffolded an empty settings file cypress.json in the project root. I have added the following two options there to avoid hard coding the test url. Also, because I do not plan to use Cypress SaaS dashboard I have disabled recoding video during E2E test runs.

1
2
3
4
{
"baseUrl": "http://localhost:5000",
"videoRecording": false
}

The simplest test case is to load the page and make sure the default greetings are displayed. I wrote file cypress/integration/spec.js with the following two tests.

1
2
3
4
5
6
7
8
9
10
11
12
13
describe('Template store app', () => {
beforeEach(() => {
cy.visit('/')
})

it('starts with "world"', () => {
cy.contains('h1', 'Hello world!')
})

it('contains greeting', () => {
cy.contains("It's nice to see you, world.")
})
})

The tests confirm the application loads and renders the expected initial text.

Starting tests

The data store reacts to the changed data, and the application should update the DOM if the store's data changes. Let us test it. First, we need to get the store reference. Because the website we are testing runs inside an iframe, we need to get the window reference. This will get us the window.app variable. The store is attached to the app object. A small utility function helps us avoid any boilerplate.

1
2
3
4
5
6
7
const getStore = () => cy.window().its('app.store')
it('can change name', () => {
getStore().then(store => {
store.set({ name: 'Cypress' })
})
cy.contains('h1', 'Hello Cypress!')
})

Changing the store updates the DOM

In this implementation, the store uses localStorage to persist the data, see main.js and useLocalStorage.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// useLocalStorage.js
export default function useLocalStorage(store, key) {
const json = localStorage.getItem(key);
if (json) {
store.set(JSON.parse(json));
}

store.onchange(state => {
localStorage.setItem(key, JSON.stringify(state));
});
}
// main.js
import useLocalStorage from './useLocalStorage.js'
useLocalStorage(store, 'my-app')

Let us test that updating the store really updates the item in the localStorage. This web application places its data under key my-app, which we can grab from our E2E tests.

1
2
3
4
5
6
7
8
9
10
11
12
it('uses localStorage as backup', () => {
getStore().then(store => {
store.set({ name: 'Cypress' })
})
const serialized = JSON.stringify({
name: 'Cypress',
})
cy
.window()
.its('localStorage.my-app')
.should('equal', serialized)
})

Great, but how do we test the initial data load? In Cypress runner each test run starts from nothing - the localStorage is blown away between each unit test. How do we set the localStorage to test that the store loads correctly? By using cy.visit() options we can control the window state before loading the website. I will place this test outside the describe('Template store app', ...) block because we want a different site visit behavior.

1
2
3
4
5
6
7
8
9
10
11
12
it('reads store from localStorage on load', () => {
cy.visit('/', {
onBeforeLoad: win => {
const serialized = JSON.stringify({
name: 'Local User',
})
win.localStorage.setItem('my-app', serialized)
},
})

cy.contains('h1', 'Hello Local User!')
})

Store load before page loads

Great, our application is loading correctly. From now on we can be safe refactoring and adding new features - as long as the E2E tests are passing.

Resources