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/**
});
};
}
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.