---
title: "Testing the first built-in browser module, KV Storage, using Cypress"
url: "https://www.cypress.io/blog/testing-the-first-built-in-browser-module"
description: "If you live on the bleeding edge of browser technology, you are probably excited about new standard modules that soon (might) come built-in to modern browsers."
---

# Testing the first built-in browser module, KV Storage, using Cypress

July 2, 2019

By Gleb Bahmutov

If you live on the bleeding edge of browser technology, you are probably excited about new standard modules that soon (might) come built-in to modern browsers.

## Built-in storage module

There is a [TC39 proposal](https://github.com/tc39/proposal-javascript-standard-library?ref=cypress-io.ghost.io) to include a set of standard modules in each browser. The goal is to prevent the need to bundle and ship JavaScript code if you only want to work with things like browser storage, popup messages, or other "standard" web application operations.

The first standard web module has shipped in Chrome version 74+. It is a storage abstraction called [KV Storage](https://developers.google.com/web/updates/2019/03/kv-storage?ref=cypress-io.ghost.io). Its purpose is to modernize access to [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB?ref=cypress-io.ghost.io) and minimize the use of `localStorage` due to its [slow performance](https://hacks.mozilla.org/2012/03/there-is-no-simple-solution-for-local-storage/?ref=cypress-io.ghost.io).

**Note:** to enable `std: ...` modules you need to use Chrome version 74+ and turn on experimental web platform features via the **`chrome://flags/#enable-experimental-web-platform-features`** page**.**

The KV Storage module's API  is similar to `localStorage`, but a bit closer to JavaScript's `[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map?ref=cypress-io.ghost.io)`. For example, here is how you would store user-entered preferences.

```js
import {storage} from 'std:kv-storage';

const main = async () => {
  const oldPreferences = await storage.get('preferences');

  document.querySelector('form').addEventListener('submit', async () => {
    const newPreferences = Object.assign({}, oldPreferences, {
      // Updated preferences go here...
    });

    await storage.set('preferences', newPreferences);
  });
};

main();
```

Notice the `import` at the top - the name of the module is special, it starts with an `std:` prefix. In the future more standard modules will be added; I am particularly excited about [std:elements/toast](https://github.com/jackbsteinberg/std-toast?ref=cypress-io.ghost.io) component.

Let's see how we can interact with the first published `std:kv-storage` module from within our end-to-end Cypress tests.

## The demo application

**Note:** you can find the source code for these example tests in the [](https://github.com/bahmutov/cypress-kv-storage-demo?ref=cypress-io.ghost.io)[bahmutov/cypress-kv-storage-demo](https://github.com/bahmutov/cypress-kv-storage-demo?ref=cypress-io.ghost.io) repository.

Our application will show a value loaded from the storage and increment it upon clicking a button. When we reload the page, the incremented value should persist.

Here is our web application in all its glory - notice that we are loading our script with `type="module"` to make sure it runs in a modern web browser.

```html
<body>
  <div>Current count: <span id="counter"></span></div>
  <button id="inc">Increment</button>

  <!-- the code below only runs in modern browsers -->
  <script type="module">
    import {storage} from 'std:kv-storage';

    const counter = document.getElementById('counter')
    const button = document.getElementById('inc')

    const getCounter = async () => {
      let k = await storage.get('count')
      if (isNaN(k)) {
         // start with zero the very first time
        k = 0
        await storage.set('count', k)
      }
      return k
    }

    const showCounter = async () => {
      counter.innerText = await getCounter()
    }

    const increment = async () => {
      let k = await getCounter()
      k += 1
      await storage.set('count', k)
      await showCounter()
    }

    // increment and show new count on button click
    button.addEventListener('click', increment)
    // show stored value at the start
    showCounter()
  </script>
</body>
```

## First end-to-end test

Before we can start testing, we need to enable the experimental web platform features in the Chrome profile that Cypress runs within. We can do this via the [browser launching API](https://on.cypress.io/browser-launch-api?ref=cypress-io.ghost.io). Unfortunately KV Storage is only supported in our Chrome version 74+ browser and not in Cypress's Electron browser (as of Cypress version 3.3.1).

```javascript
// cypress/plugins/index.js
module.exports = (on, config) => {
  // https://on.cypress.io/browser-launch-api
  on('before:browser:launch', (browser = {}, args) => {
    // browser will look something like this
    // {
    //   name: 'chrome',
    //   displayName: 'Chrome',
    //   version: '63.0.3239.108',
    //   path: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    //   majorVersion: '63'
    // }

    if (browser.name === 'chrome') {
      // `args` is an array of all the arguments
      // that will be passed to Chrome when it launchers
      args.push('--enable-experimental-web-platform-features')
      return args
    }

    if (browser.name === 'electron') {
      console.error('Electron browser does not support KV:Storage')
      return args
    }
  })
}
```

We will start with a very basic test - just confirming that the counter is visible at the start when visiting the application.

```js
/// <reference types="cypress" />
it('shows counter', () => {
  cy.visit('/')
  cy.get('#counter').should('be.visible')
})
```

Before the test runs, we need to make sure we are running the Chrome browser - not Electron. We can confirm this by using a `before` hook placed in our `cypress/support/index.js` file.

```js
before(() => {
  expect(Cypress.browser)
    .to.have.property('family')
    .equal('chrome')
  
  expect(Cypress.browser)
    .to.have.property('name')
    .equal('chrome', 'this demo only runs in regular Chrome v74+')
    
  // could check browser major version too
})
```

We can always switch the browser running the tests using the dropdown in the top right corner of the Cypress Test Runner.

Great, our first test confirms that the counter is displayed!

Let's confirm that the counter is incremented when clicking the 'Increment' button.

```js
it('increments the counter on click', () => {
  cy.visit('/')
  cy.get('#counter').should('have.text', '0')
  cy.get('#inc').click()
  cy.get('#counter').should('have.text', '1')
})
```

The test passes ✅

## Resetting the data

Everything is going great, except ... if I rerun the test - it fails. The `std:kv-storage` uses IndexedDB to actually save the values and it is NOT reset between the tests.

We need to delete the database before each test, following [Cypress's best practices](https://on.cypress.io/best-practices?ref=cypress-io.ghost.io).

```js
beforeEach(() => {
  indexedDB.deleteDatabase('kv-storage:default')
})

it('increments the counter on click', () => {
  cy.visit('/')
  cy.get('#counter').should('have.text', '0')
  cy.get('#inc').click()
  cy.get('#counter').should('have.text', '1')
})
```

The test now passes every time. Let's confirm the value stays after we reload the page using the `[cy.reload()](https://on.cypress.io/reload?ref=cypress-io.ghost.io)` command.

```javascript
it('increments the counter on click', () => {
  cy.visit('/')
  cy.get('#counter').should('have.text', '0')
  cy.get('#inc').click()
  cy.get('#counter').should('have.text', '1')
  cy.reload()
  cy.get('#counter').should('have.text', '1')
})
```

The test passes ✅

## Setting data before load

Imagine we want to set the data in the storage before our application reads it. We could open IndexedDB and create the object store directly from our spec file, but it quickly becomes very verbose and messy. IndexedDB has a complex API - precisely why `std:kv-storage` was written. Even opening a database requires wrapping callbacks with promises. Take a look at the amount of extra code needed just to get started.

```js
it('starts with 100', () => {
  // first open a database
  // then visit the page
  cy.wrap(
    new Cypress.Promise((resolve, reject) => {
      const req = indexedDB.open('kv-storage:default', 1)
      req.onerror = reject
      req.onsuccess = event => {
        resolve(event.target.result)
      }
    }),
    { log: false }
  ).then(db => {
    cy.log('Opened DB')
    // TODO create object store
    // TODO save "count" item to 100
  })
  cy.visit('/')
  cy.get('#counter').should('have.text', '100')
})
```

Ughh, ugly.  What if we could use the nice promise-returning `std:kv-storage` methods from our spec file instead? Here is where our spec file bundling runs into troubles.

```js
// DOES NOT WORK
// we cannot import from "std:kv-storage" directly
// the Cypress bundler throws an error
import { storage } from 'std:kv-storage'

it('starts with 100', () => {
  cy.visit('/')
  // TODO: use "storage" methods to set "count"
  cy.get('#counter').should('have.text', '100')
})
```

There is no `std:kv-storage` npm module to bundle - this module is only available in the browser. The Chrome documentation in the [KV Storage](https://developers.google.com/web/updates/2019/03/kv-storage?ref=cypress-io.ghost.io) announcement suggests using `import maps` to declare paths to polyfills, but this is too complex for my taste.

Instead we can take advantage of the fact that Cypress tests run _in the same browser_ as the application code. If the app can import `std:kv-storage` then the spec code can access this storage module by reference. Here is how the application can do this.

```js
<script type="module">
  import {storage} from 'std:kv-storage';

  if (window.Cypress) {
    // pass storage module to the testing code
    window.Cypress.storage = storage
  }
  // the rest of the application code
</script>
```

Now let's read the `count` from the storage after incrementing it to confirm it gets saved correctly.

```js
it('saves the count in storage', () => {
  cy.visit('/').then(() => {
    // confirm our application has passed us "Cypress.storage" reference
    expect(Cypress).to.have.property('storage')
  })
  cy.get('#counter').should('have.text', '0')
  cy.get('#inc')
    .click()
    // the promise returned by async storage.get
    // is automatically part of Cypress chain
    .then(() => Cypress.storage.get('count'))
    // and the resolved value can be asserted against
    .should('equal', 1)

  cy.get('#inc')
    .click()
    .then(() => Cypress.storage.get('count'))
    .should('equal', 2)
})
```

The test passes ✅

We can write two [Cypress custom commands](https://on.cypress.io/custom-commands?ref=cypress-io.ghost.io) to abstract out setting and getting the `count` value. This should make our tests even more readable.

```js
Cypress.Commands.add('getCount', () => {
  return Cypress.storage.get('count')
})

Cypress.Commands.add('setCount', n => {
  return Cypress.storage.set('count', n)
})

it('saves the count in storage', () => {
  cy.visit('/')
  cy.get('#counter').should('have.text', '0')
  cy.get('#inc')
    .click()
    .getCount()
    .should('equal', 1)

  cy.get('#inc')
    .click()
    .getCount()
    .should('equal', 2)
})

it('reads count from storage', () => {
  cy.visit('/')
  // adding assertion ensures application has loaded
  // and prevents race conditions
  cy.get('#counter').should('have.text', '0')
  // now we are ready to write our value
  cy.setCount(100).reload()
  // and check it is used after reloading
  cy.get('#counter').should('have.text', '100')
})
```

But how can we _set the initial count_ value using `storage` at the application's start? If we set the count after `[cy.visit()](https://on.cypress.io/visit?ref=cypress-io.ghost.io)` it's too late - the application has already read the value. We need to play a trick - get the `storage` from the application code and immediately set the `count` before the application continues. Luckily we can do this by asserting that the `Cypress.storage` property is set and immediately calling "storage.set" on it.

```js
it('starts with 100', () => {
  let storage
  // be ready to set "count" as soon as "window.Cypress.storage" is set
  Object.defineProperty(Cypress, 'storage', {
    get () {
      return storage
    },
    set (s) {
      // here is our moment to shine
      // before application reads "count"
      storage = s
      storage.set('count', 100)
    }
  })
  cy.visit('/')
  cy.get('#counter').should('have.text', '100')
})
```

Here is the order of events:

-   Our application loads the built-in `storage` module.
-   Our application executes the `window.Cypress.storage = storage` statement which calls the "setter" method we have defined in the spec file.
-   The "setter" method calls `storage.set('count', 100)`. Notice that we do NOT have to wait for this asynchronous method to resolve - like a good database, the last "write" wins.
-   Our application continues executing after setting the `Cypress.storage` and calls `_await_ storage.get('count')`. By this point the `count` has been set to `100` by the test code.
-   The value `100` is displayed on the page and the last assertion in the test passes.

The test passes ✅

## Conclusions

If the [TC39 proposal](https://github.com/tc39/proposal-javascript-standard-library?ref=cypress-io.ghost.io) to build out a JavaScript standard library becomes a reality, modern web applications will become smaller, perform better, and in the end make users happier. With Cypress end-to-end tests we can also make developers happier by having automated tests for the very first module that shipped in Chrome 74+ - the storage abstraction `std:kv-storage`.

## More info

-   Source code in [](https://github.com/bahmutov/cypress-kv-storage-demo?ref=cypress-io.ghost.io)[bahmutov/cypress-kv-storage-demo](https://github.com/bahmutov/cypress-kv-storage-demo?ref=cypress-io.ghost.io)
-   Chrome [KV Storage](https://developers.google.com/web/updates/2019/03/kv-storage?ref=cypress-io.ghost.io) announcement
-   I have used the same `Object.defineProperty` trick to pass a reference from the application code and to set the initial state in another blog post ["Shrink the Untestable Code With App Actions And Effects"](https://www.cypress.io/blog/2019/02/28/shrink-the-untestable-code-with-app-actions-and-effects/?ref=cypress-io.ghost.io)