esamattis / redux-hooks

⚓ React Hooks implementation for Redux
http://npm.im/@epeli/redux-hooks
MIT License
96 stars 4 forks source link

Component does not receive updates in Jest unit test using react-test-renderer #4

Closed sykesd closed 5 years ago

sykesd commented 5 years ago

After finding a work around for my type error in #3 I am now attempting to test a component written using react-hooks and it appears that the component does not receive the updates.

The basic setup is: TypeScript 3.3.3, react-hooks 0.5.0, react 16.8.1, jest 23.x, redux 4.0.1, immer-reducer 0.6.0

What doesn't work, is a component like this:

import { useActionCreators } from "@epeli/redux-hooks";
import React from "react";

import { ActionCreators } from "./state";
import { useSearchState } from "./store";

export const Search: React.SFC<{}> = () => {
  const { loading, query } = useSearchState((state) => state);
  const actions = useActionCreators(ActionCreators);

  return (
    <div>
            <h2>{query}</h2>
            <button onClick={actions.loadProcedures}>Load</button>
            <p>{loading ? "Loading" : ""}</p>
    </div>
  );
};

My test case looks like:

describe("", () => {
    it("should update loading on click using hooks", async (done) => {
        const store = createSearchStore();

        const component = create(
            <HooksProvider store={store}>
                <HookedSearch />
            </HooksProvider>,
        );

        const rootInstance = component.root;
        const text = rootInstance.findByType("p");
        expect(text.children[0]).toBe("");

        const button = rootInstance.findByType("button");
        button.props.onClick();

        // Timeout just there to see if it wasn't some sort of async update/timing problem
        setTimeout(() => {
            expect(store.getState().loading).toBe(true);
            expect(text.children[0]).toBe("Loading");  // <-- this fails!!
            done();
        }, 200);
    });
});

An almost identical test, using a more traditional component that uses react-redux 6.0.0 works correctly.

My suspicion is that something about the way react-hooks sets up its subscription to the store does not work correctly when running under the react-test-renderer

Is this a known problem?

Is there something else I need to be doing to get the update to work in tests?

The version of the component that does work looks like this:

import React from "react";
import { connect } from "react-redux";

import { ActionCreators, ProcedureSearchState } from "./state";

interface SearchProps {
    loading: boolean;
    query: string;

    loadProcedures: typeof ActionCreators.loadProcedures;
}

const Search: React.SFC<SearchProps> = (props) => {
    return (
        <div>
            <h2>{props.query}</h2>
            <button onClick={props.loadProcedures}>Load</button>
            <p>{props.loading ? "Loading" : ""}</p>
        </div>
    );
};

const mapStateToProps = (state: ProcedureSearchState) => ({
    loading: state.loading,
    query: state.query,
});

const mapDispatchToProps = { loadProcedures: ActionCreators.loadProcedures };

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(Search);

The only difference in the test is to use the react-redux <Provider> component instead of <HooksProvider>.

esamattis commented 5 years ago

I think you are missing the act() wrap from button.props.onClick()?

act(() => {
  button.props.onClick();
});

https://reactjs.org/docs/hooks-faq.html#how-to-test-components-that-use-hooks

You might need to import it from the react-testing-library

import {act} from "react-testing-library";

I did some wrappers for this in the useSelector tests:

https://github.com/epeli/redux-hooks/blob/8c76fa3b2c3ef442e0b87798436a57706cd7333a/__tests__/use-selector.test.tsx

sykesd commented 5 years ago

Doh! I had actually read that document on testing hooks, but then forgot about it completely.

Just for future reference, I was able to get it working with react-test-renderer, it comes with its own act() implementation. But it was necessary to wrap the call to create() in act() as well:

import { act, create } from "react-test-renderer";
...
  let component: ReactTestRender;
  act(() => {
    component = create(
      <HooksProvider store={store}>
        <Search />
      </HooksProvider>
    );
  });

  ...
  act(() => {
    button.props.onClick();
  });

Thanks again.