Skip to content

Instantly share code, notes, and snippets.

@hasparus
Last active April 22, 2022 10:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hasparus/a06b1da99e779dc8aac4793caa7a94a7 to your computer and use it in GitHub Desktop.
Save hasparus/a06b1da99e779dc8aac4793caa7a94a7 to your computer and use it in GitHub Desktop.
ensure all stories render the component they describe

It's easy to overlook that your component stories stopped working after changes to the component source. If we're not reusing our stories in unit tests and thus ensure they still work on CI in every Pull Request, we need to remember to check them during code review.

When you're hotfixing a production bug you have more serious concerns than not breaking the docs. That's understandable. Every app I worked on that used Storybook eventually had some broken stories. Fixing them lands at the bottom of the backlog; they're just some component examples and those components work on production now! You're going to do it only if you actually need those stories.

Could we write a simple test to ensure that all our stories render without crashing, and more than that, they actually render the component they're meant to render?

We could even consider snapshot testing with @storybook/addon-storyshots, but

  1. I'm not a huge fan of failing the test when CSS gets changed. (unless you're building a CSS-in-JS library). We want to write this test to lower the cost of maintenance and know earlier when something breaks.

  2. I'm actually not using Storybook itself in the app I'm working on, just PreviewJS with Component Story Format.

⬇ source below ⬇

/** @vitest-environment happy-dom */
import {
create as createTestRenderer,
ReactTestRendererTree,
} from "react-test-renderer";
import { describe, expect, it } from "vitest";
const allStories = import.meta.glob("../src/**/*.stories.tsx");
for (const [path, module] of Object.entries(allStories)) {
const segments = path.split("/");
// This assumes all stories files are named like `{ComponentName}.stories.tsx`
const componentName = segments[segments.length - 1]!.replace(
".stories.tsx",
""
);
const shortPath = segments.slice(-2).join("/");
describe(shortPath, () => {
it(`renders ${componentName}`, async () => {
for (const story of Object.values(await module())) {
if (typeof story === "function") {
const Component = story;
const renderer = createTestRenderer(<Component />);
const tree = renderer.toTree()!;
const found = breadthFirstSearch(tree, (node) => {
const type = node.type as string | React.ComponentType | undefined;
const name =
type &&
(typeof type === "string" ? type : type.displayName || type.name);
return name === componentName;
});
expect(found?.type).toBeInstanceOf(Function);
}
}
});
});
}
function breadthFirstSearch(
tree: ReactTestRendererTree,
predicate: (node: ReactTestRendererTree) => boolean
) {
const queue = [tree];
while (queue.length) {
const node = queue.shift();
if (!node) continue;
if (predicate(node)) return node;
if (node.rendered)
queue.push(
...(Array.isArray(node.rendered) ? node.rendered : [node.rendered])
);
}
return undefined;
}
// example stories file
import { useState } from "react";
import { hexToHsva } from "./color-conversion";
import { ColorPicker, HsvaColor } from "./ColorPicker";
export const Overview = () => {
const [state, setState] = useState<HsvaColor>(hexToHsva("#00beef"));
return <ColorPicker color={state} onChange={setState} />;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment