mykeels.com

Snapshots with Storybook and Jest

I like Storybook because it provides visual documentation for my components, and let’s me build them in isolation, encouraging good coding…

Snapshots with Storybook and Jest

I like Storybook because it provides visual documentation for my components, and let’s me build them in isolation, encouraging good coding principles like SOLID. But I always wondered how I could get more out of it, and justify the time spent building stories for components.

The first time I added Storybook to a team codebase, we already had tests with Jest that sometimes took snapshots of components. Many times, we found we were snapshot testing a component in jest, that already had a story.

I wondered if there was a way, to automatically generate snapshots from stories. This would leave the jest tests to focus on behavioral specs, rather than snapshotting, which only tests that a component renders, and has not changed since the last snapshot was done. And as a bonus, it will justify the time spent, providing visual documentation for our components.

It turns out there is such a way.

A storybook addon called @storybook/addon-storyshots helps do exactly this.

import React from "react";
import initStoryshots from "@storybook/addon-storyshots";

initStoryshots({
  suite: "MyApp",
  test: multiSnapshotWithOptions({}),
  storyKindRegex: /^((?!.*?App).)*$/
});

const isFunction = obj => !!(obj && obj.constructor && obj.call && obj.apply);
const optionsOrCallOptions = (opts, story) =>
  isFunction(opts) ? opts(story) : opts;

function snapshotWithOptions(options = {}) {
  return ({ story, context, renderTree, snapshotFileName }) => {
    const result = renderTree(
      story,
      context,
      optionsOrCallOptions(options, story)
    );

    async function match(tree) {
      let target = tree;
      const isReact = story.parameters.framework === "react";

      await sleep(400); // gives your complex components some time to get into a stable state after rendering

      if (isReact && typeof tree.childAt === "function") {
        target = tree.childAt(0);
      }
      if (isReact && Array.isArray(tree.children)) {
        [target] = tree.children;
      }

      if (snapshotFileName) {
        expect(target).toMatchSpecificSnapshot(snapshotFileName);
      } else {
        expect(target).toMatchSnapshot();
      }

      if (typeof tree.unmount === "function") {
        tree.unmount();
      }
    }

    if (typeof result.then === "function") {
      return result.then(match).catch(() => match(result));
    }

    return match(result);
  };
}

function multiSnapshotWithOptions(options = {}) {
  return ({ story, context, renderTree, stories2snapsConverter }) => {
    const snapshotFileName = stories2snapsConverter.getSnapshotFileName(
      context
    );
    return snapshotWithOptions(options)({
      story,
      context,
      renderTree,
      snapshotFileName: snapshotFileName.replace(/^src\\|\//, "") // assumes your source files are in src/**
    });
  };
}

storybook.test.js view raw

With this, every a snapshot of every story is taken during tests, so our visual documentation gains super powers, and we can focus on writing behavioral tests with Jest.

I hope this technique serves you as it has served me.

Tags