How to Test Storybook with Cypress

Storybook can be really useful for a team developing a web app. But like any software, it’s prone to rot when left untested. On my current project, I finally have a good way to test it. Here’s how!

Breaking Storybook

Our project teams get a lot of mileage out of Storybook. It’s a harness for developing UI components, it’s a living style guide, and it can aid in collaboration with team members of other disciplines, like designers and business analysts.

As the team’s use of Storybook expands, it becomes more important to keep it alive and healthy. Storybook is effectively another app that shares code with your web app, so:

  • It has a separate configuration, build process, and code paths.
  • It’s easy to overlook testing it.

If you’re using Storybook on your project today, ask yourself these questions:

  1. If your Storybook build were to break, how would it first be noticed?
  2. If your Storybook were to fail at runtime, how would that first be noticed?

Hopefully, the answer to #1 is that CI would fail. If you’re not performing a Storybook build in CI, definitely go do that right now.

But #2 is a little bit trickier. “I’ll notice when I next use it to build a component” is a bad answer. “We’ll notice when the project manager tries to use it for an important presentation” is worse.

Until recently, I haven’t had a great answer for this. My teams have inadvertently broken Storybook several times, perhaps while adjusting some Webpack configuration or updating a package. If we’re all working on backend stories, a week or two can go by before anyone notices. Then somebody tries to use it, and…

But now, I finally have a good way to test Storybook! I’m using Cypress, the same browser testing framework we already have in place for testing our app.

Testing Storybook with Cypress

I’ll assume you’re already using Cypress to test your app. To begin, create a second Cypress config, pointing to your Storybook server’s URL and referencing a separate integrationFolder:


//cypress.storybook.json
{
  "baseUrl": "http://localhost:9009",
  "defaultCommandTimeout": 30000,
  "viewportWidth": 1000,
  "viewportHeight": 568,
  "integrationFolder": "cypress/storybook"
}

Next add convenience scripts to package.json:


  "scripts": {
    "start:storybook": "start-storybook -p 9009 -s public --ci",
    "cy:test:storybook": "cypress run --headless -C cypress.storybook.json",
    "ci:start-and-test-storybook": "cross-env START_SERVER_AND_TEST_INSECURE=1 yarn start-server-and-test start:storybook http://localhost:9009 cy:test:storybook",
    // (...)
  }

(cross-env provides a portable way to set environment variables, and start-server-and-test does exactly what it sounds like.)

Finally, let’s write a test:


// cypress/storybook/storybook.spec.ts
/**
 *
 * Storybook Test(s)
 *
 * Storybook is effectively an entirely separate application
 * with its own build configuration. In practice it can break
 * while we're working on our app, updating dependencies, etc.
 *
 * This test aims to catch when it breaks, at least in the most
 * fundamental, easily-detectable ways.
 *
 * (e.g., when the default story can't render)
 */

// https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
const getIframeDocument = () =>
  cy.get("#storybook-preview-iframe").its("0.contentDocument").should("exist");

const getIframeBody = () =>
  getIframeDocument().its("body").should("not.be.undefined").then(cy.wrap);

describe("Storybook", () => {
  it("visits storybook", () => {
    cy.visit("/");

    cy.get("#explorerbasic-elements--paragraphs-and-links").click();
    getIframeBody().should("contain.text", "Lorem ipsum");

    cy.get("#basic-elements--headings").click();
    getIframeBody().find("h2").should("contain.text", "Heading 2");
  });
});

It was a little tricky to work with Storybook’s iframe, but the above is working reliably for us in Cypress’ default headless Electron browser on Mac, Windows, and Linux.

Fixed!

With this in place, my team has been able to detect Storybook breakages immediately in CI instead of much later when a human is counting on it to do their job.

For now, the test only performs a couple of cursory spot-checks, but it’s enough to catch big problems like the one in the screenshot above. It also establishes a foothold, should we want to add additional Cypress Storybook tests in the future.

Have you ever had Storybook break by surprise while you worked on another area of the app? I’d encourage you to test it just like the rest of your software. This method should be possible with other browser testing frameworks (like Selenium).

Further reading:

Conversation
  • Andrew says:

    Hey, thanks for the post! Is there a reason you opted to not just navigate directly to the iframe like `http://localhost:9009/iframe.html?id=explorerbasic-elements–paragraphs-and-links`? You can even manipulate knobs and stuff via the query params.

    • John Ruble John Ruble says:

      Nice insight, Andrew! I hadn’t thought of that.

      I started from the top, attempting to visit Storybook the way a user would. Thinking about it further, I definitely want to preserve some test coverage for the bits of Storybook that live _outside_ the iframe, but for individual stories it may prove faster and/or more reliable to visit the iframe target directly.

      Do you know of a way to query a list of all the story IDs? (e.g., a list containing “explorerbasic-elements–paragraphs-and-links”)

      • Andrew Anguiano says:

        That’s a good question. I haven’t figured out a way to query like that, but I haven’t really looked into it. I just made a custom little Cypress command and some helper functions to navigate to stories and set knobs. So I have a command like `cy.visitStory(‘STORY NAME’, ‘VARIANT’, {KNOBS})` that I use.

  • Comments are closed.