John K Doe
John K Doe

Reputation: 21

React/Jest - mock fetch and wait for componentDidMount to re-render

I'm playing around with react and jest and I've came to the following situation where I simply cannot figure it out how should I do it.

Todo.js

import React from 'react';
import PropTypes from 'prop-types';
import TodoItem from './TodoItem';
import {fetchTodoItems} from '../helpers/todo';

class Todo extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            todos: [],
            error: false,
            loading: true
        };

        this.updateMockState = this.updateMockState.bind(this);
    }

    updateMockState() {
        this.setState({
            todos: [{ id: 8101, text: "Cook fresh food", status: "completed" }],
            loading: false
        });
    }

    componentDidMount() {
        // TODO: add error handling

        fetchTodoItems().then(response => {
            this.setState({
                todos: response.data,
                loading: false
            })
        }, error => {});
    }

    render() {
        let content;

        // TODO: add error handling

        if (this.state.loading) {
            return (
                <div>
                    <div>
                        <button id="update-data" onClick={this.updateMockState}>Update State</button>
                    </div>
                    <p>Todos are being fetched...</p>
                </div>
            );
        }

        return (
            content ||
            <div>
                <div>
                    <button id="update-data" onClick={this.updateMockState}>Update State</button>
                </div>
                <p><b>{this.state.todos.length}</b> todos fetched so far</p>
                {this.state.todos.map(
                    (todo) => <TodoItem key={todo.id} id={todo.id} status={todo.status} text={todo.text} />)}
            </div>
        );
    }
}

Todo.proptypes = {
    timeout: PropTypes.number
};

export default Todo;

Todo.test.js

import React from 'react';
import ReactDOM from 'react-dom';
import renderer from 'react-test-renderer';
import { mount, shallow, render } from 'enzyme';
import Todo from '../components/Todo';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import toJson from 'enzyme-to-json';

// TODO: remove sinon from NPM packages

Enzyme.configure({ adapter: new Adapter() });

const todos = { data: [{
    "id": 4404,
    "status": "active",
    "text": "Shopping List"
}, {
    "id": 7162,
    "status": "completed",
    "text": "Time Registration"
}]};

describe('Todo', () => {
    it('calls mock API', async () => {
        fetch = jest.fn().mockReturnValue(Promise.resolve(todos));
        const spy = jest.spyOn(Todo.prototype, 'componentDidMount');

        var component = mount(<Todo timeout={2000} />);
        component.instance().componentDidMount();
        expect(spy).toHaveBeenCalled();

        expect(toJson(component)).toMatchSnapshot();
    });
});

As you can see Todo component is a simple component that inside componentDidMount calls an API, get the response and displays. While waiting for the api call, some info is shown... There's also a button for dummy state update but that's not important for now.

fetchTodoItems (file is todo.js)

export const fetchTodoItems = () => {
    return fetch("data/todo.json").then(res => res.json());
};

My problem is the following:

On 1st I'm supposed to see no todo items, but on 2nd I should see my todo items.

Here is TodoItem

import React from 'react';
import PropTypes from 'prop-types';

const TodoItem = (props) => {
    let htmlClass = [];
    if (props.status === 'completed') {
        htmlClass.push("todo-completed");
    }

    return (
        <ul>
            <p className={htmlClass.join(" ")}>
                <small>[#{props.id}]</small> {props.text} <i>({props.status})</i>
            </p>
        </ul>
    );
}

TodoItem.proptypes = {
    id: PropTypes.number.required,
    text: PropTypes.string.required,
    status: PropTypes.string.required
};

export default TodoItem;

So far I've tried the following:

Any help?

EDIT:

If I remove API call from componentDidMount and use only setState, then snapshot testing shows todos items (without 1st case where it should say that todos are being fetched...) Why?? Shouldn't be the same?

If then only the promises are the root cause, how can I wait for all promises to finish?

Upvotes: 2

Views: 3438

Answers (1)

Jitesh
Jitesh

Reputation: 51

One way would be to wait for the last event(promise) if any in the queue to be resolved and then do a component instance update ?

const allOver = () => new Promise((resolve) => setImmediate(resolve));

describe('Todo', () => {
    it('calls mock API', async () => {
        fetch = jest.fn().mockReturnValue(Promise.resolve(todos));
        var component = mount(<Todo timeout={2000} />);
        await allOver();
        component.update();
        expect(toJson(component)).toMatchSnapshot();
    });

});

Upvotes: 1

Related Questions