Jest/React/Redux thunks: Redux store dispatch not called in test

26 views Asked by At

I am using React together with Redux thunks to manage the global state in my application. I have a component that receives one of it's variables from the Redux store. This variable is used to trigger a useEffect which dispatches an API call.

I am writing some tests for this workflow. I want my tests to check that the useEffect is triggered when the Redux store variable is updated.

This is an example of my component:

import React, { useEffect, useMemo, useState } from "react";
import { useAppDispatch } from "../hooks/useAppDispatch";
import { useAppSelector } from "../hooks/useAppSelector";
import { fetchData, storeFilters } from "../slices/dataSlice";
import { filtersSelector } from "../selectors";

const MyComponent = () => {
  const dispatch = useAppDispatch();
  const { filters } = useAppSelector(filtersSelector);
  const [year, setYear] = useState((new Date()).getFullYear());
  const [period, setPeriod] = useState("all");

  useEffect(() => {
    console.log("Dispatching fetchData: ", filters);

    dispatch(
      fetchData({
        filters: filters
      })
    );
  }, [filters]);

  const changeYear = (e) => {
    console.log("Year was changed: ", e.target.value);

    setYear(parseInt(e.target.value));
  }

  const changePeriod = (e) => {
    console.log("Period was changed: ", e.target.value);

    setPeriod(e.target.value);
  }

  const applyFilters = () => {
    const newFilters = {
      year: year,
      period: period
    };

    console.log("Apply button was clicked, dispatching filters to store: ", newFilters);

    dispatch(
      storeFilters(newFilters)
    );
  }

  const years = useMemo(() => {
    const thisYear = (new Date()).getFullYear();
    const yearArr = [];

    for (let year = thisYear; year < thisYear + 11; year++) {
      yearArr.push(year);
    }

    return yearArr
  }, []);

  return (
    <div className="componentContainer s-componentContainer">
      <div className="filtersContainer s-filtersContainer">
        <select name="year" id="yearSelect s-yearSelect" onChange={changeYear}>
          {years.map(y => <option key={y} className="selectYearOption s-selectYearOption" value={y}>{y}</option>)}
        </select>

        <select name="period" id="periodSelect s-periodSelect" onChange={changePeriod}>
          <option className="selectPeriodOption s-selectPeriodOption" value="all">All</option>
          <option className="selectPeriodOption s-selectPeriodOption" value="winter">Winter</option>
          <option className="selectPeriodOption s-selectPeriodOption" value="spring">Spring</option>
          <option className="selectPeriodOption s-selectPeriodOption" value="summer">Summer</option>
          <option className="selectPeriodOption s-selectPeriodOption" value="autumn">Autumn</option>
        </select>

        <button onClick={applyFilters}>Apply Filters</button>
      </div>

      <div className="mainContainer s-mainContainer">
        {Object.keys(filters).map(objKey => <div key={objKey}>{objKey} value selected: {filters[objKey]}</div>)}
      </div>
    </div>
  );
};

export default MyComponent;

This works in a regular browser with the store updated as it should and the component and useEffect triggered as expected. But when running tests, this does not trigger correctly.

In a browser I can see that all console.log(...); are printed correctly and as expected. I can also see in the network tab that the API call is made when the page is loaded and when the store is updated.

The store and slice functions are written as follows:

import { createSlice } from "@reduxjs/toolkit";
import { thunkFactory } from "../utils/ThunkFactory";
import AppConstants from "../utils/AppConstants";

interface Filters {
  year: number;
  period: string;
}

interface Notification {
  key: string;
  type: string;
}

interface FiltersSliceState {
  loading: boolean;
  notification: Notification | null;
  filters: Filters;
}

export const initialState: FiltersSliceState = {
  loading: false,
  notification: null,
  filters: {
    year: (new Date()).getFullYear(),
    period: "all"
  }
};

export const fetchData = () => {
  console.log("Recieved fetch call, making actual call")

  return thunkFactory("fetchDataThunk", AppConstants.API_URL + "/data/all", "get");
};

const filtersSlice = createSlice({
  name: "filters",
  initialState: initialState,
  reducers: {
    storeValues: (state, { payload }) => {
      console.log("Received store filters call, storing filter values: ", payload);

      state.filters = payload;
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchData.pending, (state) => {
        state.loading = true;
        state.notification = null;
      })
      .addCase(fetchData.fulfilled, (state, action) => {
        console.log("Data received: ", action.payload);
        // Do something with action.payload
        state.loading = false;
        state.notification = null;
      })
      .addCase(fetchData.rejected, (state, action) => {
        state.loading = false;
        state.notification = { key: action.payload as string, type: "ERROR" };
      });
  }
});

export const { storeValues } = filtersSlice.actions;
export default filtersSlice.reducer;

As before this works as expected in the browser.

So I have written my test like this:

import React from "react";
import configureStore from "redux-mock-store";
import thunk from "redux-thunk";
import { act } from "react-dom/test-utils";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { Provider } from "react-redux";
import MyComponent from "../components/MyComponent";

export const filtersMock = {
  loading: false,
  notification: null,
  filters: {
    period: "all"
  }
};

const mockStore = configureStore([thunk]);

export const getMockStore = () => mockStore({ filters: filtersMock });

describe("MyComponent", () => {
  let store;

  const getRenderedComponent = () => {
    store = getMockStore();

    return render(
      <Provider store={store}>
        <MyComponent />
      </Provider>
    );
  };

  it("updating filters triggers new data fetch", async () => {
    const { baseElement } = getRenderedComponent();
    const selectField = baseElement.querySelector("#s-periodSelect");

    await waitFor(() => {
      act(() => {
        fireEvent.click(selectField);
      });
    });
  
    const options = baseElement.querySelectorAll(".s-selectOption");
  
    const selectOption = options[1];
    await waitFor(() => selectOption);
  
    await waitFor(() => {
      act(() => {
        fireEvent.click(selectOption);
      });
    });

    const applyButton = await screen.findByText(/Apply Filters/i);

    await waitFor(() => {
      act(() => {
        fireEvent.click(applyButton);
      });
    });
});

Comparing the flow with printed messages from the browser and the test, the following happens:

Action Browser result Jest testing result
Page loads OK OK
useEffect triggered first time (because of component load) OK OK
"Dispatching fetchData ..." message logged OK OK
Call to slice function to make API call OK OK
"Recieved fetch call ..." message logged OK OK
API call made OK Assume OK, not implemented in test
"Data received: ..." message logged OK Assume OK, not implemented in test
Period selected in the dropdown OK OK
"Period was changed: ..." message logged OK OK
"Apply Filters" button clicked OK OK
"Apply button was clicked ..." message logged OK OK, data looks OK (correctly changed period)
Call made to Redux store to update filters OK Unknown
"Received store filters call ..." message logged OK Not OK
Filters updated in store, broadcasting store change OK Not OK
Component useAppSelector received store change, updated filters value OK Not OK
useEffect triggered second time (because of filter variable changed) OK Not OK
"Dispatching fetchData ..." message logged OK Not OK
Call to slice function to make API call OK Not OK
"Recieved fetch call ..." message logged OK Not OK
API call made OK Assume not OK, not implemented in test

So my question here is: Why is the dispatch call to the redux store either not dispatched by the component or not received by the store?

0

There are 0 answers