React, TypeScript, and TDD Part 3
Tags: react, typescript, testing
Published: 2021-06-28 17:44:07

React component development is fun, but it breaks your flow heading over to the browser to poke around. What's a more joyful routine? Staying in a test in your IDE.

That's what this series of posts is about. I'm showing my [React+TypeScript+TDD](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/) tutorial in the [WebStorm Guide](https://www.jetbrains.com/webstorm/guide/), which includes videos+text+code. The previous two articles covered [Part 1](https://dev.to/pauleveritt/react-typescript-and-tdd-1ne7) and [Part 2](https://dev.to/pauleveritt/react-typescript-and-tdd-part-2-44fm).

Let's wrap up this series by taking a look at the last two steps in the tutorial: [Rich Events and Testing](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/rich_events_and_testing/) and [Presentation and Container Components](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/presentation_components/).

# Rich Events and Testing

Our `Counter` doesn't track any count. We're going to add event handling to a stateful class component by first writing tests during development. First, let's get things set back up.

## Getting Setup

From the end of Part 2, we have a `Counter` component in a file `Counter.tsx`:

```typescript
import React, {Component} from "react";

export type CounterProps = {
    label?: string;
    start?: number;
};
const initialState = {count: 0};
export type CounterState = Readonly<typeof initialState>;

export class Counter extends Component<CounterProps, CounterState> {
    readonly state: CounterState = initialState;

    componentDidMount() {
        if (this.props.start) {
            this.setState({
                count: this.props.start,
            });
        }
    }

    render() {
        const {label = "Count"} = this.props;
        return (
            <div>
                <div data-testid="counter-label">{label}</div>
                <div data-testid="counter">
                    {this.state.count}
                </div>
            </div>
        );
    }
}
```

Side-by-side in our IDE, we have the tests for that component in `Counter.test.tsx`:

```typescript
import React from "react";
import {render} from "@testing-library/react";
import {Counter} from "./Counter";

test("should render a label and counter", () => {
    const {getByTestId} = render(<Counter/>);
    const label = getByTestId("counter-label");
    expect(label).toBeInTheDocument();
    const counter = getByTestId("counter");
    expect(counter).toBeInTheDocument();
});

test("should render a counter with custom label", () => {
    const {getByTestId} = render(<Counter label={`Current`}/>);
    const label = getByTestId("counter-label");
    expect(label).toBeInTheDocument();
});

test("should start at zero", () => {
    const {getByTestId} = render(<Counter/>);
    const counter = getByTestId("counter");
    expect(counter).toHaveTextContent("0");
});

test("should start at another value", () => {
    const {getByTestId} = render(<Counter start={10}/>);
    const counter = getByTestId("counter");
    expect(counter).toHaveTextContent("10");
});
```

With this in place, our tests pass:

![01](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ozdoywz49h9o7d9sisbn.png)


## Failing Click Test

Let's start with a failing test that clicks on the count and checks if the number is updated:

```typescript
import { render, fireEvent } from "@testing-library/react";
// ...

test("should increment the count by one", () => {
  const { getByRole } = render(<Counter />);
  const counter = getByRole("counter");
  expect(counter).toHaveTextContent("0");
  fireEvent.click(counter)
  expect(counter).toHaveTextContent("1");
});
```

`fireEvent`, what's that? It's the big idea in this tutorial step. You can pretend to click, or dispatch other DOM events, even without a real browser or "mouse". Jest uses the browser-like JSDOM environment entirely inside NodeJS to fire the event.

This new test fails: the number didn't increment. Which is good!

## onClick Handler

The component doesn't handle clicks. Let's head to `Counter.tsx` and add a click handler on the counter, pointed at a method-like arrow function "field":

```typescript
    incrementCounter = (event: React.MouseEvent<HTMLElement>) => {
        const inc: number = event.shiftKey ? 10 : 1;
        this.setState({count: this.state.count + inc});
    }

    render() {
        const {label = "Count"} = this.props;
        return (
            <div>
                <div data-testid="counter-label">{label}</div>
                <div data-testid="counter" onClick={this.incrementCounter}>
                    {this.state.count}
                </div>
            </div>
        );
    }
```

With `onClick={this.incrementCounter}` we bind to an arrow function, which helps solve the classic "which `this` is `this`?" problem. The `incrementCounter` arrow function uses some good typing on the argument, which can help us spot errors in the logic of the handler.

## Allow Event Modifiers

Let's add one more feature: if you click with the Shift key pressed, you increase the count by 10. To help on testing, we'll install the `user-event` library:

```console
$ npm install @testing-library/user-event @testing-library/dom --save-dev
```

...then import it at the top of `Counter.test.tsx`:

```typescript
import userEvent from "@testing-library/user-event";
```

The event modifier code is already written above -- we just need a test:

```typescript
test("should increment the count by ten", () => {
    const {getByTestId} = render(<Counter/>);
    const counter = getByTestId("counter");
    expect(counter).toHaveTextContent("0");
    userEvent.click(counter, { shiftKey: true });
    expect(counter).toHaveTextContent("1");
});
```

In this test, we changed from fireEvent in testing-library to userEvent in user-event. The click passes in some information saying shiftKey was "pressed".

The test passes! 

# Presentation and Container Components

Our `Counter` component has a lot going on inside. React encourages presentation components which have their state and some logic passed in by container components. Let's do so, and along the way, convert the back to a functional component.

As a reminder, this is covered in depth, with a video, in the [Guide tutorial step](https://www.jetbrains.com/webstorm/guide/tutorials/react_typescript_tdd/presentation_components/).

## Counter State

Let's start with a test. We want to pass the state into component as a prop, thus allowing a starting point for the count. In the `should render a label and counter` first test, when we change to `<Counter count={0}/>`, the TypeScript compiler yells at us:

![02](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg5szat58lyqy8wl00pr.png)

That makes sense: it isn't in the type information as a valid prop. Change the second test to also ask for starting count:

```typescript
test("should render a label and counter", () => {
    const {getByTestId} = render(<Counter count={0}/>);
    const label = getByTestId("counter-label");
    expect(label).toBeInTheDocument();
    const counter = getByTestId("counter");
    expect(counter).toBeInTheDocument();
});

test("should render a counter with custom label", () => {
    const {getByTestId} = render(<Counter label={`Current`} count={0}/>);
    const label = getByTestId("counter-label");
    expect(label).toBeInTheDocument();
});
```

Back in `Counter.tsx`, let's convert to a dumb, presentational component:

```typescript
import React from "react";

export type CounterProps = {
    label?: string;
    count: number;
};

export const Counter = ({label = "Count", count}: CounterProps) => {
    return (
        <div>
            <div data-testid="counter-label">{label}</div>
            <div data-testid="counter"
                // onClick={handleClick}
            >
                {count}
            </div>
            {count}
        </div>
    );
};
```

It's pretty similar, but the `count` value is passed in, rather than being component state. We also have commented out the star of the show: a callable that increments the counter.

## Passing In a Function

We'll tackle that now. But in a bit of a curveball way: we'll pass the `handleClick` callable *into* this dumb component. The parent will manage the logic.

Let's model the type information for this prop:

```typescript
export type CounterProps = {
    label?: string;
    count: number;
    onCounterIncrease: (event: React.MouseEvent<HTMLElement>) => void;
};
```

Immediately, though, TypeScript gets mad in our first two tests: we're missing a mandatory prop. We fix it by creating mock function and passing it into these two tests:

```typescript
test("should render a label and counter", () => {
    const handler = jest.fn();
    const {getByTestId} = render(<Counter count={0} onCounterIncrease={handler}/>);
    const label = getByTestId("counter-label");
    expect(label).toBeInTheDocument();
    const counter = getByTestId("counter");
    expect(counter).toBeInTheDocument();
});

test("should render a counter with custom label", () => {
    const handler = jest.fn();
    const {getByTestId} = render(<Counter label={`Current`} count={0} onCounterIncrease={handler}/>);
    const label = getByTestId("counter-label");
    expect(label).toBeInTheDocument();
});
```

For our third test -- tracking the click event -- we change the handler to see if it was called:

```typescript

test("should call the incrementer function", () => {
    const handler = jest.fn();
    const { getByTestId } = render(
        <Counter count={0} onCounterIncrease={handler} />
    );
    const counter = getByTestId("counter");
    fireEvent.click(counter);
    expect(handler).toBeCalledTimes(1);
});
```

The last section of the tutorial continues to cover more of the refactoring:

- Make the dumb component a little smarter by not requiring a callable prop
- Changing the parent component to track the updating of the state
- Writing tests to make sure the `App` uses the container and presentation components correctly

Along the way, the tutorial shows how to refactor the type information to correctly model the contract.

# Conclusion

And that's a wrap! In this 3 part series, we did a summary of this React+TS+TDD tutorial. We covered quite a bit, and the best part -- we didn't head over to a browser. We stayed in our tool, in the flow, and worked with confidence.