End-to-end Testing
How-tos & Guides
12 min read

How to save and restore state in Cypress tests

Learn how to share browser state across Cypress tests without making your tests harder to maintain.

Paul Akinyemi
Published August 12, 2022

Best Practices on Sharing State

Sharing state across multiple tests is considered a bad practice. Consider the following passage from Cypress’s “Best Practices” docs:

You only need to do one thing to know whether you’ve coupled your tests incorrectly, or if one test is relying on the state of a previous one. Change it to it.only on the test and refresh the browser. If this test can run by itself and pass - congratulations you have written a good test. If this is not the case, then you should refactor and change your approach.”

While there are situations where you might need to violate this, in general, your tests should be as loosely coupled as possible. However, this does mean that for common operations like logging in, you’ll need to execute those same steps in every single test. In this article, we’ll cover how to share browser state across tests without violating the principle described above.

How to Persist Browser State between Cypress tests

Browsers provide a few places to persist the state of your application across requests:

In this section, you’ll see several methods to save browser state in these locations. You’re going to build a simple web application to test: a web page with buttons that create browser state when clicked.

Setup

This tutorial assumes that you already have the following installed on your local machine:

Let’s begin by creating a directory for this project called cy-state-demo. After that, open the terminal and navigate to cy-state-demo. Inside your terminal, run the following commands:

npm install cypress --save-dev
npm install live-server --save-dev

Those commands will install cypress and a simple development server for your web page.

Your next task is to build the web application you’ll be testing. First, create a folder called build inside cy-state-demo. Inside build, create two files: index.html and script.js.

If your local development environment is either Linux or Mac, you can run the following commands:

mkdir build
touch build/index.html
touch build/script.js

At this point, the structure of cy-state-demo should look like this:

Next, open build/index.html and copy the following code into it:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>State demo</title>
  </head>

  <body>
    <button type="button" data-cy="cookie-trigger">Set cookie</button>

    <button type="button" data-cy="cookie-def-set">Set default cookie</button>

    <button type="button" data-cy="cookie-modify">Modify default cookie</button>

    <button type="button" data-cy="set-ls">Set local storage</button>

    <button type="button" data-cy="set-ss">Set session storage</button>
    <script src="script.js"></script>
  </body>
</html>

This HTML contains two things:

Next, we need JavaScript to make the buttons do their job. Open script.js and place the following in it:

document.querySelector("[data-cy=cookie-trigger]").addEventListener("click", () => {
  document.cookie = "key1=value1; expires=Wed, 18 Dec 2024 12:00:00 UTC";
});

document.querySelector("[data-cy=cookie-def-set]").addEventListener("click", () => {
  document.cookie = "default=set; expires=Wed, 18 Dec 2024 12:00:00 UTC";
});

document.querySelector("[data-cy=cookie-modify]").addEventListener("click", () => {
  document.cookie = "default=modify; expires=Wed, 18 Dec 2024 12:00:00 UTC";
});

document.querySelector("[data-cy=set-ls]").addEventListener("click", () => {
  localStorage.setItem("key1", "blue");
  localStorage.setItem("key2", "red");
});

document.querySelector("[data-cy=set-ss]").addEventListener("click", () => {
  sessionStorage.setItem("key1", "blue");
  sessionStorage.setItem("key2", "red");
});

The JavaScript above is a sequence of click event listeners for all the buttons. The listeners set values in the various types of browser storage.

The first two event listeners create two cookies, the third event listener modifies the value of a single cookie, and the final event listeners set keys in LocalStorage and SessionStorage respectively.

Next, start the development server by running the following in your terminal:

live-server build

That command should output text that looks like the following:

Serving "build" at http://127.0.0.1:8080
Ready for changes

While the command is running, you can access the web page at localhost://8080. That’s all the setup you need! Now let’s start building tests..

Persisting State With Cookies

Leave the server running and open a new terminal window. Next, run the following command:

npx cypress open

This will open the Cypress GUI. Select the E2E testing type, and choose your preferred browser for testing. This tutorial will use the Electron browser.

At this point, you should see a screen like this one:

Select the ‘Create new empty spec’ option, and name it cookies.cy.js. Cypress will create a default passing test for you. At this point, if you go back to your editor, the structure of your project should look like this:

Open cypress.config.js, and add the following code into the e2e object:

baseUrl: "http://localhost:8080",

Then open cookies.cy.js and replace its contents with the following:

describe("cookie walkthrough", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("tests that the cookie is set", () => {
    cy.get("[data-cy=cookie-trigger]").click();

    cy.getCookie("key1").should("be.a", "object").should("have.property", "value");
  });

  it("tests that the cookie is cleared", () => {
    cy.getCookies().should("be.empty");
  });

  it("sets the cookie again and preserves it", () => {
    cy.get("[data-cy=cookie-trigger]").click();

    cy.getCookie("key1").should("have.property", "value", "value1");

    Cypress.Cookies.preserveOnce("key1");
  });

  it("tests that the cookie was preserved", () => {
    cy.getCookie("key1").should("have.property", "value", "value1");
  });

  it("tests that the cookie was preserved only once", () => {
    cy.getCookies().should("be.empty");
  });
});

But what does this code do? It starts with a beforeEach hook that navigates to the web application.

The first it block clicks the set cookie button and then asserts that the cookie was set.

The second test asserts that Cypress cleared the cookie.

The third test sets the cookie again, and then uses the Cookies.PreserveOnce method to tell Cypress not to clear that cookie in the next test.

The fourth test makes sure the cookie was preserved, and the fifth test checks that the cookie was only preserved once.

Note: The PreserveOnce method is currently deprecated, and will be removed in a future Cypress version. It is mentioned here for two reasons:

If you run the file and return to the Cypress GUI, you should see that all tests passed:

There’s another, more permanent way to preserve cookies: Cookies.Default. Cypress will whitelist any cookies that match the keys you pass to this function.

In other words, Cypress won’t clear those cookies between tests. This works differently from Cookies.PreserveOnce, which will only preserve a cookie for the next test.

Let’s write a few tests to illustrate.

Open the file cypress/support/e2e.js and paste the following code into it:

Cypress.Cookies.defaults({
  preserve: "default",
});

Create a new file in the cypress/e2e folder called defaultCookie.cy.js. Put the following code into it:

describe("default cookie testing", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("sets the default cookie", () => {
    cy.get("[data-cy=cookie-def-set]").click();

    cy.getCookie("default").should("be.a", "object").should("have.property", "value", "set");
  });

  it("checks that the default cookie is till set", () => {
    cy.getCookie("default").should("be.a", "object").should("have.property", "value", "set");
  });

  it("modifies the default cookie", () => {
    cy.get("[data-cy=cookie-modify]").click();

    cy.getCookie("default").should("be.a", "object").should("have.property", "value", "modify");
  });

  it("tests that the default cookie was unaffected", () => {
    cy.getCookie("default").should("be.a", "object").should("have.property", "value", "modify");
  });

  after(() => {
    cy.clearCookie("default").should("be.null");
  });
});

This looks like a lot of code, but the tests are a lot like the previous set. The first test uses the button to set a cookie with the key ‘default’, and the next test makes sure that the cookie remains set. After that, the cookie’s value is modified, and the last test checks that the cookie remains unchanged.

Persisting State With Local Storage

Next, we’ll explore how to persist LocalStorage between tests. Your first option is a workaround that comes from this Cypress issue.

It involves creating a couple of custom Cypress commands to store and retrieve the contents of LocalStorage.

Open your cypress/support/commands.js file and put the following code into it:

let LOCAL_STORAGE_MEMORY = {};

Cypress.Commands.add("mSaveLocalStorage", () => {
  Object.keys(localStorage).forEach((key) => {
    LOCAL_STORAGE_MEMORY[key] = localStorage[key];
  });
});

Cypress.Commands.add("mRestoreLocalStorage", () => {
  Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => {
    localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
  });
});

The mSaveLocalStorage command copies the keys inside localStorage and their values into a plain object which Cypress won’t clear.

The mRestoreLocalStorage command iterates over the object you’re using as a store and copies its keys and their values back into localStorage.

Let’s write a few tests to show how you can use these commands.

Create a new file inside your cypress/e2e folder called ls.cy.js, and put these tests in it:

describe("testing local storage", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("tests that local storage is set", () => {
    cy.get("[data-cy=set-ls]")
      .click()
      .then(() => {
        expect(localStorage.getItem("key1")).to.eq("blue");
        expect(localStorage.getItem("key2")).to.eq("red");
      });
  });

  it("tests that ls is empty, then sets and saves it", () => {
    expect(localStorage.getItem("key1")).to.be.null;
    expect(localStorage.getItem("key2")).to.be.null;

    cy.get("[data-cy=set-ls]")
      .click()
      .then(() => {
        expect(localStorage.getItem("key1")).to.eq("blue");
        expect(localStorage.getItem("key2")).to.eq("red");
        cy.mSaveLocalStorage();
      });
  });

  it("tests that ls saved successfully", () => {
    expect(localStorage.getItem("key1")).to.be.null;
    expect(localStorage.getItem("key2")).to.be.null;

    cy.mRestoreLocalStorage().then(() => {
      expect(localStorage.getItem("key1")).to.eq("blue");
      expect(localStorage.getItem("key2")).to.eq("red");
    });
  });
});

The first test sets LocalStorage and verifies its contents. The second test asserts that Cypress cleared LocalStorage, then uses the button to set it again. Finally, it saves the current state of LocalStorage with the custom command.

The last test verifies that Cypress cleared LocalStorage again, restores LocalStorage, and then tests that the restoration worked.

Another option for persisting LocalStorage between Cypress tests is to use a plugin. The cypress-localstorage-commands plugin provides several custom Cypress commands. These commands let you save, restore and manipulate LocalStorage. They also let you disable LocalStorage and test your application in that state.

You’re going to write a few tests to show you how to get started with the plugin.

Open a new terminal window and run the following command to install the plugin:

npm i --save-dev cypress-localstorage-commands

Next, open cypress/support/commands.js and add the following line of code:

import "cypress-localstorage-commands";

Create a new file inside cypress/e2e called lsPlugin.cy.js and place the following tests in it:

describe("testing local storage", () => {
  beforeEach(() => {
    cy.visit("/");
  });

  it("tests that local storage is set", () => {
    cy.get("[data-cy=set-ls]")
      .click()
      .then(() => {
        expect(localStorage.getItem("key1")).to.eq("blue");
        expect(localStorage.getItem("key2")).to.eq("red");
      })
      .saveLocalStorage("snap1");
  });

  it("tests that ls is now empty", () => {
    expect(localStorage.getItem("key1")).to.be.null;
    expect(localStorage.getItem("key2")).to.be.null;
  });

  it("tests that plugin works", () => {
    cy.restoreLocalStorage("snap1").then(() => {
      expect(localStorage.getItem("key1")).to.be.eq("blue");
      expect(localStorage.getItem("key2")).to.eq("red");
    });
  });
});

The first test starts by setting the value of LocalStorage. It then uses the cy.saveLocalStorage command provided by the plugin to store a snapshot of LocalStorage.

The next test asserts that LocalStorage is now empty. The last test uses the cy.restoreLocalStorage command to restore the contents of LocalStorage, and test that the restoration worked.

The plugin can do much more than this, but that’s outside the scope of this article. If you’d like to learn more, consult its documentation.

Persisting State With Session Storage

The way Cypress handles SessionStorage can be a bit inconsistent. In some cases, Cypress clears SessionStorage as it should, but there’s also a long-open issue about Cypress not clearing SessionStorage between tests.

If Cypress is clearing sessionStorage and you want to persist it, adapt the localStorage workaround like this:

let SESSION_STORAGE_MEMORY = {};

Cypress.Commands.add("saveSessionStorage", () => {
  Object.keys(sessionStorage).forEach((key) => {
    SESSION_STORAGE_MEMORY[key] = sessionStorage[key];
  });
});

Cypress.Commands.add("restoreSessionStorage", () => {
  Object.keys(SESSION_STORAGE_MEMORY).forEach((key) => {
    sessionStorage.setItem(key, SESSION_STORAGE_MEMORY[key]);
  });
});

If you want to clear sessionStorage between tests, you can do so manually:

beforeEach(() => {
  cy.window().then((win) => {
    win.sessionStorage.clear();
  });
});

Persist State with the Session API

The Cypress session API, introduced in Cypress 8.2.0, is an API designed to help your tests run faster by allowing you to build up state (in LocalStorage, SessionStorage, and cookies) and cache that state so it can be restored later. The API is currently experimental.

According to the Cypress blog, this is the purpose of cy.session:

While logging in during each test that requires being logged-in is a best practice, the process of logging in can be slow, which people sometimes attempt to work around by logging in just once per spec file in a before hook, or by using the Cypress.Cookies API to persist cookies across tests. However, having tests rely on the state of previous tests is not a best practice, and should be avoided.

The new cy.session() command solves this problem by caching and restoring cookies, localStorage and sessionStorage after a successful login. The steps that your login code takes to create the session will only be performed once when it’s called the first time in any given spec file. Subsequent calls will restore the session from cache.

Now that you know what cy.session does, let’s see how to use it.

Because the session API is still experimental, first you need to add the following line to your e2e object in cypress/cypress.config.js:

experimentalSessionAndOrigin: true;

Note: Amongst other things, enabling the session API means Cypress will clear the page at the beginning of each test, so cy.visit() must now be called explicitly at the beginning of each test.

Next, create a file called sessionAPI.cy.js in the cypress/e2e folder. Put the following tests in it:

describe("testing the session API", () => {
  beforeEach(() => {
    cy.session("s1", () => {
      cy.visit("/");
      cy.get("[data-cy=set-ls]")
        .click()
        .then(() => {
          expect(localStorage.getItem("key1")).to.be.eq("blue");
          expect(localStorage.getItem("key2")).to.eq("red");
        });
      cy.get("[data-cy=set-ss]")
        .click()
        .then(() => {
          expect(sessionStorage.getItem("key1")).to.be.eq("blue");
          expect(sessionStorage.getItem("key2")).to.eq("red");
        });

      cy.get("[data-cy=cookie-trigger]")
        .click()
        .get("[data-cy=cookie-def-set]")
        .click()
        .then(() => {
          cy.getCookie("key1").should("be.a", "object").should("have.property", "value", "value1");
          cy.getCookie("default").should("be.a", "object").should("have.property", "value", "set");
        });
    });
  });

  it("tests that the session is restored properly", () => {
    cy.visit("/");
    expect(localStorage.getItem("key1")).to.be.eq("blue");
    expect(localStorage.getItem("key2")).to.eq("red");
    expect(sessionStorage.getItem("key1")).to.be.eq("blue");
    expect(sessionStorage.getItem("key2")).to.eq("red");

    cy.getCookie("key1").should("be.a", "object").should("have.property", "value", "value1");
    cy.getCookie("default").should("be.a", "object").should("have.property", "value", "set");
  });

  it("shows that setup only runs once", () => {
    cy.visit("/");
    expect(localStorage.getItem("key1")).to.be.eq("blue");
    expect(sessionStorage.getItem("key2")).to.eq("red");

    cy.getCookie("key1").should("be.a", "object").should("have.property", "value", "value1");
    cy.getCookie("default").should("be.a", "object").should("have.property", "value", "set");
  });
});

This code calls cy.session in the BeforeEach hook and passes two arguments to it. The first parameter is the name of the session. The second parameter is the setup function that creates the state cy.session will cache.

In this case, the setup function clicks all the buttons on our web page to set our cookies, localStorage, and sessionStorage. It then asserts that all state was set correctly.

The next two tests are nothing new: Each test asserts that the state is restored after calling cy.visit. The second test exists to show that Cypress cached the state instead of running the setup function again.

Here’s how we know that the setup function is only run once. If you open the cypress GUI and click on the name of each test, you should see something like this:

Then this:

Which shows you that the session was saved and restored from cache.

Conclusion

In this article, we went over multiple ways to save and restore browser state in Cypress. You can find the full code for this article here. Happy testing!

Reflect: A testing tool with built-in support for sharing state across tests

Reflect is a no-code testing tool that can test virtually any action you can take on a browser. In addition to support for setting cookies, localstorage, and sessionstorage values at the beginning of a test, you can also test actions like drag-and-drops, hovers, and file uploads.

Creating a test in Reflect is easy: the tool records your actions as you use your site and automatically translates those actions into a repeatable test that you can run any time.

Get automated test coverage for your app today — try it for free.

Get started with Reflect today

Create your first test in 2 minutes, no installation or setup required. Accelerate your testing efforts with fast and maintainable test suites without writing a line of code.

Copyright © 2022 Reflect Software Inc. All Rights Reserved.