Welcome to the continuation of the Confident React App blog post series. We’ve covered some considerable ground so take your time reading the series (part 1 | part 2 | part 3).
In this post, we’ll add a tool to our arsenal for testing components in the browser without much hassle: Storybook.
Telling component stories
We already used some important tools and techniques to our code base to increase our confidence:
- Automated tests
- Static type checking
- Behavior Driven Development
But can you spot what’s missing so far? Well, it’s the application running in a browser, just like the end-user will. A dummy app for testing the components would be fairly easy to create but there a few downsides for this approach:
- The real app depends on external APIs/resources so it’s slower.
- It could be impossible to test some features due to the unavailability of the external APIs/resources.
- The app code is more complex since it integrates many parts which makes it hard to verify components in isolation.
- Apps can have many complex states which makes it hard to verify all use cases, especially error states.
- It’s harder to iterate fast in the UI when there are too many dependencies.
A common way to avoid the pitfalls above is to create a separate app for the purpose of showcasing components and app states in a more structured and isolated way. Enter Storybook: the tool of choice for the question at hand. Storybook is quite powerful and versatile so I recommend reading through its examples and documentation. Let’s add it to our project:
npx -p @storybook/cli sb init
yarn flow-typed install @storybook/[email protected] @storybook/[email protected]
If everything goes well after the long install, there will be a bunch of new files in our project. Storybook is also already running with some demo content.
Storybook uses an original language for its API: you are supposed to showcase the features of your application in stories by module. For example, we want to showcase the <ListGroup> component so a story is added for it with “chapters” that show all it can do.
Edit the file src/stories/index.js
and add the following code:
// @flow
import React from 'react';
import { storiesOf } from '@storybook/react';
import { ListGroup, ListGroupItem } from '../components/listGroup';
storiesOf('ListGroup', module)
.add('with items', () => (
<ListGroup>
<ListGroupItem>item 1</ListGroupItem>
<ListGroupItem>item 2</ListGroupItem>
</ListGroup>
));
Now, if you go to http://localhost:9009/?path=/story/listgroup–with-items, you can actually see and try for the first time the component!

… And it sucks. Even though the generated HTML is perfectly valid, it doesn’t look at all to the Bootstrap version:

This is exactly why we always need to test the application, or parts of it, in the browser; even with all the tools and techniques added so far, we’re still very far from the desired outcome.
You gotta have style
We’ve been talking a lot about Bootstrap and even implemented some code that relies on it, but we never added it as a dependency to our project.
There are many ways of fulfilling this step. For simplicity, we’re adding Bootstrap as a runtime dependency so only its production CSS is loaded by the page. To do so, we have to add custom tags to Storybook. Create the file .storybook/preview-head.html
and add this:
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
Bootstrap’s CSS will be loaded from a CDN inside of Storybook. Just restart Storybook and profit!
Enriching the leaves
OK, we have a very simple list of items but there are still two important functionalities missing:
- Active list items.
- Clickable items so that they are interactive.
Let’s jump straight into the red test:
it('renders active when active prop is truthy', () => {
const wrapper = shallow(<ListGroupItem active>item 1</ListGroupItem>);
expect(wrapper)
.toContainExactlyOneMatchingElement('li.list-group-item.active');
});
And implement it to get a green test:
type ListGroupItemProps = {
children: string,
active?: boolean
};
export function ListGroupItem (props: ListGroupItemProps) {
const classes = "list-group-item" + (props.active ? " active" : "")
return <li className={classes}>{props.children}</li>;
}
Finally, a new story needs to be added so we can see the final result:
.add('with active items', () => (
<ListGroup>
<ListGroupItem active>item 1</ListGroupItem>
<ListGroupItem active>item 2</ListGroupItem>
</ListGroup>
));
For the clicking, the specification defines two options: either <a>
<button>
<button>
describe('when onClick prop is defined', () => {
it('renders as an action button', () => {
const wrapper = shallow(
<ListGroupItem onClick={() => {}}>
item 1
</ListGroupItem>
);
expect(wrapper).toContainExactlyOneMatchingElement(
'button[type="button"].list-group-item.list-group-item-action'
);
});
it('renders as an active action button when active prop is truthy', () => {
const wrapper = shallow(
<ListGroupItem active onClick={() => {}}>
item 1
</ListGroupItem>
);
expect(wrapper).toContainExactlyOneMatchingElement(
'button[type="button"].list-group-item.list-group-item-action.active'
);
});
it('calls the given callback when clicked', () => {
const onClickSpy = jest.fn();
const wrapper = shallow(
<ListGroupItem onClick={onClickSpy}>item 1</ListGroupItem>
);
wrapper.simulate("click");
expect(onClickSpy).toHaveBeenCalled();
});
});
To make the tests pass/green:
type ListGroupItemProps = {
children: string,
active?: boolean,
onClick?: Function
};
export function ListGroupItem (props: ListGroupItemProps) {
let classes = "list-group-item" + (props.active ? " active" : "");
if(props.onClick) {
classes += " list-group-item-action";
return (
<button type="button" className={classes} onClick={props.onClick}>
{props.children}
</button>
);
} else {
return <li className={classes}>{props.children}</li>;
}
}
The last step is to verify it in the browser with a story:
import { action } from '@storybook/addon-actions';
.add('with actionable items', () => (
<ListGroup>
<ListGroupItem active onClick={action("Item 1 clicked!")}>
action item 1
</ListGroupItem>
<ListGroupItem onClick={action("Item 2 clicked!")}>
action item 2
</ListGroupItem>
</ListGroup>
));
There you go: our React implementation of the Bootstrap’s list group component is finally complete!
But what do we do with it? The answer lies in the app’s requirements: selecting an item when it’s clicked, multiple or single selections, deselecting…
In the next post, we’ll implement the behavior with some state and do a deeper discussion on how to test components in a way that makes us confident.
Cheers!