Confident React App – Part 4

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 shows 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:

  1. Active list items.
  2. Clickable items so that they are interactive.

Let’s jump straight into the red test:

// src/components/listGroup.spec.js

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:

// src/components/listGroup.js

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:

// src/stories/index.js

.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 an <a> or <button>. Since we don’t need page navigation yet, the <button> is the right choice. Also, whenever an item is clicked, it must execute the given callback if any. The red tests:

// src/components/listGroup.spec.js

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:

// src/components/listGroup.js
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:

// src/stories/index.js

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!

Flattr this!