Storybook, React, TypeScript and Jest

So I’ve been a fan of Storybook for a while. Being able to quickly visualize components and interact with them is very useful. I’ve just recently started using Jest as well, and so far I’m pretty impressed with it as well. In particular, the snapshot testing for UI stuff seems awesome and avoids a lot of tedium.

But the combination of Storybook stories along with Jest snapshots is a really compelling combination. Fortunately, there is an “addon” for Storybook that provides support for what they call StoryShots, i.e., deriving snapshot driven tests from the Storybook stories.

But using StoryShots was not without a few hiccups for me because I belong to the TypeScript tribe and while many tools provide support and documentation for TypeScript, there can be some rough edges. That was the case for me with trying to combine these technologies. All of the issues I ran into had relatively simple solutions, but finding them wasn’t so easy so I thought it would be a good idea to share the with others (and my future, forgetful self) for reference.

TL;DR

Install the dependencies with:

$ yarn add "@storybook/addon-storyshots" "@storybook/react" "@types/jest" "@types/react-dom" "@types/react-test-renderer" "@types/storybook__react" awesome-typescript-loader jest react-dom react-test-renderer ts-jest typescript

Add the following to your package.json file:

"jest": {
"transform": {
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"mapCoverage": true,
"testPathIgnorePatterns": [
"/node_modules/",
"/lib/"
],
"testRegex": "(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
]
}

Install Storybook and configure your package with by running this within your package directory:

$ npm i -g @storybook/cli
$ getstorybook

Add a file named .storybook/webpack.config.js containing:

// load the default config generator.
const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js');
module.exports = (baseConfig, env) => {
const config = genDefaultConfig(baseConfig, env);
// Extend it as you need.
// For example, add typescript loader:
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve('awesome-typescript-loader')
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
};

Finally, add the following test to be run by Jest:

import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();

The complete source code for these examples can be found at https://github.com/xogeny/typescript-storyshots.

With that out of the way, let’s step through the various pieces involved and provide a bit more explanation about setting all this up…

TypeScript compiler settings

So the first thing you have to be sure of is that you have your tsconfig.json file in order. An obvious thing is that you need to have compilerOptions.jsx set to react. Personally, I do not like the default behavior of tsc of putting the transpiled .js files next to their .ts counterparts. So I also set compilerOption.rootDir to ./ and compilerOption.outDir to ./lib.

N.B. — BTW if, while trying to get these tools working together, you run into an error that says:

ReferenceError: React is not defined

Then be sure to set compilerOptions.allowSyntheticDefaultImports to be false. I had this setting leftover from a previous project and it was very difficult to figure out the issue.

TypeScript with Jest

To use Jest with TypeScript, you need to add two dependencies: jest and ts-jest, e.g.,

$ yarn add jest "@types/jest" ts-jest --dev

The ts-jest README explains pretty clearly how to get Jest to recognize TypeScript code. I prefer to keep all my tests in a directory called test partly for historical reasons, partly because I like them in a separate directory from my code and partly because __tests__ doesn’t appear next to my src directory in Visual Studio Code. So I modify the settings for package.json a little bit so Jest searches the test directory:

"jest": {
"transform": {
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"mapCoverage": true,
"testPathIgnorePatterns": [
"/node_modules/",
"/lib/"
],
"testRegex": "(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
]
}

But this is just my preference. At this point, you can create a very simple test like this:

test("Simple sum", () => {
expect(3 + 5).toBe(8);
})

…and it should work if you run jest. I set jest as my test command in package.json when I run npm init . so if I run npm test I get:

PASS  test/sum.ts
✓ Simple sum (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.106s
Ran all test suites.

Woot!

Snapshots

Now if we want to use snapshots in their most basic form, we can do something like this:

test("Snapshot sum", () => {
// I'm too lazy to figure out what this should equal...
expect(3492 + 2592).toMatchSnapshot();
})

In this case, it might seem dumb to use a snapshot. I could just say .toBe(6084) . But the point, as we will see in a minute, is that with complex DOM structures, we don’t really know what the expected answer will be and trying to figure it out is pretty tedious. Running this test the first time will cause Jest to record the expected value (6084). And if we were to change the operands at some point, Jest would bark back with something like this:

FAIL  test/sum.ts
● Snapshot sum
expect(value).toMatchSnapshot()
Received value does not match stored snapshot 1.
  - 6084
+ 6085
at Object.<anonymous> (test/sum.ts:7:25)
at Promise.resolve.then.el (node_modules/p-map/index.js:42:16)
at process._tickCallback (internal/process/next_tick.js:109:7)
  ✕ Snapshot sum (124ms)

If we actually meant to change the operands and we want the snapshot to be updated to reflect the new test, we can do jest -u or, given my NPM configuration, npm test -- -u.

Ok, so the way we apply this to React is to create a basic component like this:

import React = require('react');
export interface SampleWidgetProps {
name: string;
}
export class SampleWidget extends React.Component<SampleWidgetProps, {}> {
render() {
return <div><h1>Hello {this.props.name}</h1></div>;
}
}

In order to create a Jest snapshot test for React components, we have to add another package, react-test-renderer but to use it with TypeScript we also need to add @types/react-test-renderer With that in place, our test will look like this:

import React = require('react');
import { create } from 'react-test-renderer';
import { SampleWidget } from '../src';
test("Say my name, say my name...", () => {
const tree1 = create(
<SampleWidget name="Michael" />
).toJSON();
expect(tree1).toMatchSnapshot();
});

Just like with our previous snapshot test, this records (a JSON) representation of the DOM and will detect any regressions if that representation changes.

Storybook and StoryShots

OK, the final step in this process it to get Storybook and StoryShots working. Let’s start with Storybook. Setting up storybook is now pretty easy because they have a simple command line tool. As per their documentation, all you need to do is:

$ npm i -g @storybook/cli
$ cd my-react-app
$ getstorybook

Note that up until this point, we only needed react and not react-dom. But to use @storybook/react, you’ll need both. Technically, react-dom only needs to be added as a dev dependency if you are only creating a library of components. Since this is TypeScript, we’ll want the types as well, i.e.,

$ yarn add react-dom "@types/react-dom" --dev

Storybook adds a script calledstorybook to your package.json file. So at this point, you should be able to do:

npm run storybook

…and it should should launch a server that renders some example stories.

In order to write our stories in TypeScript, we need to add the types for @storybook/react. Those can be installed with:

yarn add "@types/storybook__react" --dev

Let’s remove stories/index.js and replace it stories/index.tsx and include a trivial story for our SampleWidget component.

import * as React from 'react';
import { SampleWidget } from '../src';
import { storiesOf } from '@storybook/react';
storiesOf("TypeScript and Storybook", module)
.add('Sample Widget', () => <SampleWidget name="Michael" />);

This should compile no problem. But if we try to run Storybook via npm run storybook, we’ll run into a problem. We’ll get the error:

Can't resolve '../stories' in ...

To remedy this situation, we need to make sure that Storybook understands TypeScript. This is a relatively simple fix because Storybook leverages webpack and webpack has pretty good support for TypeScript. In order for webpack to automatically transpile TypeScript, we need to add the awesome-typescript-loader as a dependency, e.g.,

$ yarn add awesome-typescript-loader --dev

After that, we need to add a file called .storybook/webpack.config.js that looks like this:

// load the default config generator.
const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js');
module.exports = (baseConfig, env) => {
const config = genDefaultConfig(baseConfig, env);
// Extend it as you need.
// For example, add typescript loader:
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve('awesome-typescript-loader')
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
};

Note that this webpack.config.js file has nothing specific in it about our project. So you can simply copy and paste it across projects to enable TypeScript support. With these two steps (adding awesome-typescript-loader and adding a custom webpack.config.js) now we can run our stories with npm run storybook.

StoryShots

Now recall that this all started by talking about how cool it would be to combine Jest and Storybook so that we did regression tests on our stories. In order to use StoryShots, we first need to install the addon with:

$ yarn add "@storybook/addon-storyshots" --dev

Then, all we need to do is add one additional test to our test suite. Recall that I store my tests in test/, so I created a file called test/stories.ts containing:

import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();

This is the most basic way to generate tests based on Storybooks. For more complex setups, please consult the documentation.

Conclusion

TypeScript, React, Storybook, StoryShots and Jest are a great combination and they all work quite well together. Hopefully the few remaining “rough edges” can be smoothed away. But in the meantime, hopefully any lingering mysteries about how to make them work together are resolved by this article.

As mentioned before, all the source code used here is contained in the repository found at: https://github.com/xogeny/typescript-storyshots. Pull requests for improving the existing approach and/or updating to newer versions of these tools would be gladly accepted.

Good luck!