Implementing "Complete Task" Feature

You will practice more with unit tests, and learn a few techniques that are useful when testing React apps.

Where we left off

In the last lesson, we decided that our TODO app needs to be able to mark tasks as “completed” by clicking on them. The integration test that tests for this behaviour is currently failing, and we will begin fixing it now.

Task component

Stop and think about the behaviour we are going to implement. It assigns some of the responsibility (reacting to user input, e.g., mouse click) to individual tasks. Right now, we only have the TaskList component, which is responsible for displaying all of the tasks. To adhere to the single responsibility principle, we will create another component: Task. It is responsible for displaying a single task and reacting to user interaction for this task.

As usual, we will first write failing unit tests, and fix them with implementation. We will need to test (at least) three aspects of this component:

  1. It must display the task.
  2. It must communicate its completion (completed CSS class).
  3. It must react to user interaction.

Create a file Task.test.js in src/components with these placeholders:

import React from 'react'

import Task from './Task';
import {render, screen, fireEvent} from '@testing-library/react'


describe('<Task />', () => {
    it('renders the task', () => {
    });
});

To check if Task renders the task we ask it to, we need to think about the way we inject the task into the component. Right now, we store the tasks as an array of strings, where each element of the array represents a task. Since we will be adding the completion property to tasks, a new data structure is called for. Without any unnecessary complication, I suggest using a JS object like this:

{
  label: 'Buy milk',
  completed: false
}

This should be pretty self-explanatory. Every task is now a JS object, with the label key being the task content, and the completed boolean representing the task completion. To make our life easier when testing, create these two placeholder tasks in the describe block of the test:

const completedTask = {
    label: 'Do this',
    completed: true
};
const uncompletedTask = {
    label: 'Do that',
    completed: false
};

Test task display

Now, testing for the task content becomes a piece of cake. We will assume that the Task component will accept a prop task, which is the JS object. Then we will look for this text in the rendered result:

it('renders the task', () => {
    render(<Task task={completedTask}/>);
    expect(screen.getByText(completedTask.label)).toBeInTheDocument();
});

Fix task display

This test will fail due to a number of reasons. The first being that the Task component is not yet created. Let’s fix that by creating a Task.js file under src/components with this content:

import React from 'react';

const Task = ({task: {label}}) => (
    <li>{label}</li>
);

export default Task;

This simple component does two things for now: take in the task prop, and display the label wrapped by ul. Though the code is simple, it fixes the failing unit test, thus marking another iteration of Test-Driven-Development. Now, it is time to write another test.

Test task completion

As you recall, we want to show whether the task is completed or not by using the CSS class completed. We already created the completedTask and uncompletedTask objects, so all that’s left to do is render both, and assert on the CSS class:

it('assigns completed class', () => {
    render(<Task task={completedTask} />);
    expect(screen.getByText(completedTask.label)).toHaveClass('completed');

    render(<Task task={uncompletedTask} />);
    expect(screen.getByText(uncompletedTask.label)).not.toHaveClass('completed');
});

The toHaveClass matcher comes from the jest-dom library, which is included by CRA. You can see other useful functions from jest-dom here

Fix task completion

To fix this test, you will need to:

  1. Extract the completed key from the task.
  2. Assign a CSS class based on the completed key.

This code will do exactly that:

const Task = ({task: {label, completed}}) => (
    <li className={completed ? 'completed' : null}>{label}</li>
);

Exercise

Now it is time for your second hands-on exercise. We are almost done developing the Task component, but it still misses one important feature. We must be notified every time a user clicks on it, so the controlling component will mark tasks as completed. To be more precise, we want the Task component to accept a callback function via prop onToggle, and call this function every time the user clicks on a task. Use this integrated environment to write the test, and then fix the test by writing the implementation. You will see one of the correct solutions in the next lesson.

Get hands-on with 1200+ tech skills courses.