Component Driven Development with Storybook and Nightwatch
Table of Contents
Introduction
Storybook is arguably the most popular component library around at the moment, with a steady user base for React and growing communities for Vue, Svelte, Angular, and other UI libraries. It’s being used by thousands of teams around the world to build UIs with reusable components.
The popularity of Storybook can probably be attributed to identifying one essential need that frontend developers have today: in order to be able to build modern complex UIs, frontend teams need to be able to design, build, and test UI components in isolation. Storybook calls this technique component driven development (CDD).
Component Driven Development
Storybook aims to deliver a complete solution for managing large component libraries by offering compelling tools for documenting and testing. Each component is rendered in isolation with its various states and requirements.
Each representation of a component with various properties and arguments is called a story and all the stories belonging to a particular component are collected in a .stories.js[x]
(or .ts[x]
) file. Stories are written in a declarative syntax and each story simulates a particular component variation, by rendering the component with a specific set of props and mock data.
Here’s a basic component story example:
// Histogram.stories.js|jsx
import React from 'react';
import { Histogram } from './Histogram';
export default {
title: 'Histogram',
component: Histogram,
};
const Template = (args) => <Histogram {...args} />;
export const Default = Template.bind({});
Default.args = {
dataType: 'latency',
showHistogramLabels: true,
histogramAccentColor: '#1EA7FD',
label: 'Latency distribution',
};
Storybook is distributed as a Node web application which loads the existing stories and displays them in a sophisticated management interface which has integrated tools for documenting and testing the components.
The Storybook app can run locally or it can be deployed on a web server. In fact many popular component libraries are available to the public.
How to test component stories
Presently, Storybook is used mainly as a great collaboration tool between frontend developers and designers to document the UI components and even entire pages.
That is all fine until testing comes into the picture and starts complicating everything. That’s when the real fun begins, because in order to effectively develop UI components, an efficient way to easily test them is also needed.
Automated testing of components is needed to ensure that all stories are rendered properly with the specified props and mock data. Furthermore, more complex components can have stories which require simulating user actions and verifying behaviour.
Out of the box, Storybook provides built-in support for several testing strategies of components:
So there is some built-in support for testing and also a CLI test runner which uses Playwright and Jest under the hood. However its strength still remains more on the design side of the whole web application development spectrum.
In order to have strong confidence over the code quality for the components that you build, you will need to either extend the stories with additional support for component testing or import stories into other tests.
Integrate Nightwatch into your Storybook
Nightwatch is one of the leading end-to-end testing framework for Node.js and it has support for Storybook integration starting from v2.4 via the @nightwatch/storybook
plugin, currently available for React. In addition to end-to-end testing, Nightwatch now also supports component testing for React and Vue apps and starting with v2.5 it has also streamlined its support for testing on mobile devices as well.
Nightwatch can assist here with filling the gap between UI component design+development and QA+testing. In order to add support for automated testing of components in a Storybook environment, you only need to add install Nightwatch and extend your existing stories with test functionality.
In the following section we will attempt to do just that. We’ll take an existing Storybook which is available publicly on Github and we’ll go through these steps:
- install and configure Nightwatch
- run the existing component stories with Nightwatch
- extend one of the complex stories and with Nightwatch testing functionality
Let us begin then. We’ll use the World Food Programme’s Ui kit available on Github at https://github.com/wfp/designsystem.
Install Dependencies
We’ll first have to fork the project. My own fork is located at github.com/pineviewlabs/wfp-designsystem. I’m going to clone the project locally and install the existing dependencies:
git@github.com:pineviewlabs/wfp-designsystem wfp-designsystem
cd wfp-designsystem
npm install --legacy-peer-deps
The --legacy-peer-deps
flag is needed to make some older dependencies install. If you get npm audit
messages, you can disregard them for now, since this is just a tutorial.
Run Storybook
Once everything is installed, you can try running Storybook locally. To do that, run:
npm run storybook
This will build the Storybook files and run the project on your local host.
At the moment of writing this guide, the Storybook version used in the WFP design kit is 6.2. You can try to upgrade it to at least v6.5 but I already tried that and got a bunch of errors, so for the moment I am sticking with the 6.2 just to get everything working together.
Install Nightwatch
Nightwatch can be installed with just one command:
npm init nightwatch@latest
This will prompt you to install the browsers and location of the test files. I have selected all of them Chrome, Firefox, Safari, and Edge. Edge needs to be installed separately but the init tool will generate the config needed in Nightwatch.
For the source folder location enter the following:
src/components/**/*.stories.js
For base url enter:
http://localhost:9000/
Install the Storybook plugin
We’ll also need to install the Storybook plugin for Nightwatch:
npm install @nightwatch/storybook
You might need to pass --legacy-peer-deps
again, if you’re on newer NPM versions.
Last step is to load and configure the plugin in Nightwatch. To do that, edit nightwatch.conf.js
and add the following right after src_folders
:
// nightwatch.conf.js
module.exports = {
// .. other settings
plugins: ['@nightwatch/storybook'],
'@nightwatch/storybook': {
start_storybook: false,
storybook_url: 'http://localhost:9000/'
}
}
Nightwatch can automatically start/stop Storybook for us during the test run, but since this particular version of Storybook takes a bit of time to load, we have kept start_storybook
to false
and we will run it manually.
Run stories in Nightwatch
The @nightwatch/storybook
plugin is designed to work in such a way that component tests can be added on top of existing component stories, instead of expecting users to import stories in tests. When using Nightwatch with Storybook, the test is the .stories
file itself.
In our case, we are going to run the existing src/component/**/*.stories.js
files as tests. But before we can do that, we need to configure one last thing.
Customise esbuild
Nightwatch uses esbuild
under the hood in order to parse JSX files and this project contains some syntax features which esbuild
doesn’t support them by default. However, we can support them by enabling compilation using babel
.
To do so, edit nightwatch.conf.js
and add the following, right after the line which contains '@nightwatch/storybook'
:
// nightwatch.conf.js
module.exports = {
// other settings here...
esbuild: {
babel: {
filter: /\.[cm]?js$/
}
}
}
Add babel
Add a babel.config.json
file with the following content so that esbuild
can find it:
{
"exclude": ["node_modules/**"],
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-export-default-from"
]
}
Run Nightwatch
Now the time has come to actually run the component stories. The project already has some unit-level type of component tests written in Jest and using Enzyme. We’re going to not touch them and instead work with the actual .stories
files.
We’re just going to do an exploratory test run using Chrome, and we’ll do it in --serial
mode so we can observe the browser:
npx nightwatch --env chrome --serial
Running all the stories in the project in serial mode could take a while. But we can try to run them in parallel (you can replace “chrome” with either “firefox”, “safari”, or “edge”, depending on which browsers you have installed).
By default it will pick up the number of test workers based on the number of CPU cores, but we will set it to 4. We’ll also run it in --headless
mode so the browsers won’t popup and distract us (note that headless mode is not available in Safari):
npx nightwatch --env chrome --workers=4 --headless
So by now hopefully we have a working Nightwatch installation in the WFP Storybook project. At the moment, the stories will only be rendered and Nightwatch will perform a visibility check to ensure that everything is OK.
Next step though is to extend one of the stories and add some Nightwatch specific functionality.
Extend stories with Nightwatch test
We’re going to pick one of the larger stories in the project and start adding additional test functionality. It seems like src/components/Form/Form.stories.js
it’s a good candidate in terms of complexity so we’re going with that one.
Let’s run it first separately:
npx nightwatch src/components/Form/Form.stories.js -e chrome
There are four different stories in the file and the output should look similar to this:
[Form.stories.js component] Test Suite
───────────────────────────────────────────────────────────────────────
Using: chrome (107.0.5304.110) on MAC OS X.
Running "Default" story:
───────────────────────────────────────────────────────────────────────
✔ Passed [ok]: "components-forms-form--default.Default" story was rendered successfully.
✨ PASSED. 1 assertions. (1.078s)
Running "Detailed Form" story:
───────────────────────────────────────────────────────────────────────
✔ Passed [ok]: "components-forms-form--detailed-form.DetailedForm" story was rendered successfully.
✨ PASSED. 1 assertions. (583ms)
Running "Login" story:
───────────────────────────────────────────────────────────────────────
✔ Passed [ok]: "components-forms-form--login.Login" story was rendered successfully.
✨ PASSED. 1 assertions. (411ms)
Running "Contact" story:
───────────────────────────────────────────────────────────────────────
✔ Passed [ok]: "components-forms-form--contact.Contact" story was rendered successfully.
✨ PASSED. 1 assertions. (513ms)
✨ PASSED. 4 total assertions (5.768s)
Add Nightwatch file uploading test
Nightwatch can run Storybook interaction tests and accessibility tests, together with supporting the hooks api. However, for this article we're only going to explore the test()
function which Nightwatch adds on top of the Component Story Format. You can read more about the Storybook integration on the Nightwatch documentation.
We’re going to extend the DetailedForm
story so let’s go ahead and edit the src/components/Form/Form.stories.js
and add the following right before that line with export const Login
(around line 400):
The test()
function runs in the Node context so it has access to the file system, as opposed to the play()
function which is limited by the browser sandbox.
The test()
function receives both the Nightwatch browser
API (which can be used to issue commands and run assertions) and a component
object which points to the root element in the story and it is compatible with Nightwatch commands and assertions.
In this example will simulate the uploading of a file, using Nightwatch uploadFile()
api command and we’ll verify if the file drop zone element has been updated.
// src/components/Form/Form.stories.js
DetailedForm.test = async (browser, {component}) => {
await browser.uploadFile('input[type="file"]', require.resolve('./README.mdx'));
const fileElementContainer = await browser.findElement(`.${settings.prefix}--file-container`);
browser.strictEqual(await browser.hasDescendants(fileElementContainer), true, 'The file dropzone element has been populated.');
};
Also add this import at the top of the file:
// src/components/Form/Form.stories.js
import settings from '../../globals/js/settings';
We can now run only the DetailedForm
story:
npx nightwatch src/components/Form/Form.stories.js -e chrome --story=DetailedForm
Hopefully all is well and the output should look like below (there might be some warnings on top, but those aren’t critical):
[Form.stories.js component] Test Suite
───────────────────────────────────────────────────────────────────────
Using: chrome (107.0.5304.110) on MAC OS X.
Running "Detailed Form" story:
───────────────────────────────────────────────────────────────────────
✔ Passed [ok]: "components-forms-form--detailed-form.DetailedForm" story was rendered successfully.
✔ Passed [strictEqual]: The file dropzone element has been populated.
✨ PASSED. 2 assertions. (1.217s)
Conclusion
That’s pretty much it for now. I might write another similar post using a different public storybook library. Or maybe you might want to contribute one? The great humans at Storybook have recently started to put together a collection of public storybook libraries in their Component Encyclopedia so there’s plenty of opportunity to experiment and maybe even make open-source contributions.
You can find the code on Github:
If you made it thus far in the tutorial, I salute you! You are now ready to make a contribution to an open-source project that’s being used at the United Nations, which is quite something.
Thanks for reading and don’t forget to wear your hat as you please – indoors or out.