How can i access the history object inside of this promise call?

Viewed 430

In the React component, A button submission takes the values of text fields on the page and passes them into mockFetch. Then, If the promise of mockFetch is successful, It causes a history.push('/newaccount') to fire.

I set up my test to click on the button and then attempt to detect the history.push('/newaccount') call with what i passed into it, but my mock history.push doesn't get called. Does anyone know what i can do to get this test to pass?

EDIT: It turns out replacing the current jest.mock with:

    const history = createMemoryHistory();
    const pushSpy = await jest.spyOn(history, "push");

allows me to call the mocked history when the history.push is OUTSIDE of the mockFetch function... but not when it is inside of it. That's what i'm trying to figure out now.

Main app routing:

function App() {
  const classes = useStyles();

  return (
    <div className="App">
      <Banner />
      <div id="mainSection" className={classes.root}>
        <ErrorBoundary>
          <Paper>
            <Router history={history}>
              <Switch>
                <Route path="/newaccount">
                  <NewAccountPage />
                </Route>
                <Route path="/disqualify">
                  <DisqualificationPage />
                </Route>
                <Route path="/">
                  <LandingPage />
                </Route>
              </Switch>
            </Router>
          </Paper>
        </ErrorBoundary>
      </div>
    </div>
  );
}

export default App;

Component: (omitted some redundant fields)

import React, { useCallback, useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useHistory } from "react-router-dom";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import TextFieldStyled from "../TextFieldStyled/TextFieldStyled.js";
import * as actionTypes from "../../redux/actions/rootActions.js";
import {
  checkAllErrors,
} from "../../validators/validators.js";
import mockFetch from "../../fetchCall/fetchCall";
import "./LandingPage.css";

const LandingPage = () => {
  const dispatch = useDispatch();
  const history = useHistory();

  const {
    changeCarPrice,
    changeErrorMessage,
    resetState,
  } = actionTypes;

  useEffect(() => {
    return () => {
      dispatch({ type: resetState });
    };
  }, [dispatch, resetState]);

  const { carPrice } = useSelector(
    (state) => state
  );

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!checkAllErrors(allErrors)) {
      // Call the API
      mockFetch(carPrice)
        .then((response) => {
          history.push("/newaccount");
        })
        .catch((error) => {
          dispatch({ type: changeErrorMessage, payload: error });
          history.push("/disqualify");
        });
    }
  };

  const [allErrors, setAllErrors] = useState({
    carValueError: false,
  });

  return (
    <div id="landingPage">
      <Grid container>
        <Grid item xs={2} />
        <Grid item xs={8}>
          <form onSubmit={handleSubmit}>
            <Grid container id="internalLandingPageForm" spacing={4}>
              {/* Text Fields */}
              <Grid item xs={6}>
                <TextFieldStyled
                  info={"Enter Car Price ($):"}
                  value={carPrice}
                  adornment={"$"}
                  type="number"
                  label="required"
                  required
                  error={allErrors.carValueError}
                  id="carPriceField"
                  helperText={
                    allErrors.carValueError &&
                    "Please enter a value below 1,000,000 dollars"
                  }
                  passbackFunction={(e) => handleChange(e, changeCarPrice)}
                />
              </Grid>
            </Grid>
            <Grid container id="internalLandingPageFormButton" spacing={4}>
              <Grid item xs={4} />
              <Grid item xs={3}>
                <Button
                  variant="contained"
                  color="primary"
                  type="submit"
                  id="applyNowButton"
                  title="applyNowButton"
                  onSubmit={handleSubmit}
                >
                  Apply Now
                </Button>
              </Grid>
              <Grid item xs={5} />
            </Grid>
          </form>
        </Grid>
        <Grid item xs={2} />
      </Grid>
    </div>
  );
};

export default LandingPage;

Test:

const wrapWithRedux = (component) => {
  return <Provider store={store}>{component}</Provider>;
};

  it("simulates a successful submission form process", () => {
    const mockHistoryPush = jest.fn();
    jest.mock("react-router-dom", () => ({
      ...jest.requireActual("react-router-dom"),
      useHistory: () => ({
        push: mockHistoryPush,
      }),
    }));

    render(
      wrapWithRedux(
        <Router history={history}>
          <Switch>
            <Route path="/newaccount">
              <NewAccountPage />
            </Route>
            <Route path="/">
              <LandingPage />
            </Route>
          </Switch>
        </Router>
      )
    );

    const carPriceField= screen.getByTestId("carPriceField");
    fireEvent.change(carPriceField, { target: { value: "5000" } });

    const buttonSubmission= screen.getByTitle("buttonSubmission");
    fireEvent.click(buttonSubmission);


    expect(mockHistoryPush).toHaveBeenCalledWith('/newaccount');
  });

2 Answers

so after looking further into mocking history, i found this from How to mock useHistory hook in jest?

The answer seems to be to do the code below and then the pushSpy will be called

    const history = createMemoryHistory();
    const pushSpy = jest.spyOn(history, "push");

    render(
      wrapWithRedux(
        <Router history={history}>
          <Switch>
            <Route path="/newaccount">
              <NewAccountPage />
            </Route>
            <Route path="/">
              <LandingPage />
            </Route>
          </Switch>
        </Router>
      )
    );

I also had to wrap the expect line using waitFor from the react testing library to get the history.push to call the mock inside of the mockFetch function like so:

await waitFor(() => expect(pushSpy).toHaveBeenCalledWith("/newaccount"));

With these two modifications, the test now passes.

Rather than mock a bunch of things and test implementation, I would test the behavior.

Here are some ideas to maybe help writing for behavior over implementation details.

Refer to React Router testing docs

React Router has a guide on testing navigation based on actions.

Hijack render() to wrap with Redux

Rather than writing a Redux wrapper for every test suite, you could hijack the react-testing-library's render() function to get a clean state or seed the state. Redux has docs here.

Don't mock fetch()

A button submission takes the values of text fields on the page and passes them into mockFetch

I would use an HTTP interceptor to stub a response. That way you get the async behavior and you bind your tests to the backend vs binding it to the tool. Say you don't like fetch(), you'll be stuck with it until you migrate everything. I made a blog post on the subject Testing components that make API calls.

Here's your code example with some edits:

  it("creates a new account", () => { // <-- more descriptive behavior
    // Stub the server response
    nock(`${yoursite}`)
      .post('/account/new') // <-- or whatever your backend is
      .reply(200);

    render(
        <MemoryRouter initialEntries={["/"]}> // <-- Start where your forms are at
          <Switch>
            <Route path="/newaccount">
              <NewAccountPage />
            </Route>
            <Route path="/">
              <LandingPage />
            </Route>
          </Switch>
        </MemoryRouter>
    );

    const carPriceField= screen.getByTestId("carPriceField");
    fireEvent.change(carPriceField, { target: { value: "5000" } });

    const buttonSubmission= screen.getByTitle("buttonSubmission");
    fireEvent.click(buttonSubmission);


    expect(document.body.textContent).toBe('New Account Page'); // <-- Tests route change
  });
Related