Skip to content
Snippets Groups Projects
Unverified Commit 3b671c75 authored by Emmad Usmani's avatar Emmad Usmani Committed by GitHub
Browse files

Improve typesafety for `useDispatch` (#29530)

* Re-export typesafe redux hooks

* rename `redux.unit.spec.js`

* add support for `dispatch.action` syntax

* use `createMockUser`

* replace author name in comment with PR number

* remove custom hooks in favor of using `extend-redux`

* revert changes to `renderWithProviders`
parent a27d958a
No related branches found
No related tags found
No related merge requests found
import type { ThunkDispatch, AnyAction } from "@reduxjs/toolkit";
import type { TypedUseSelectorHook } from "react-redux";
import {
useDispatch as useDispatchOriginal,
useSelector as useSelectorOriginal,
} from "react-redux";
import type { State } from "metabase-types/store";
export const useDispatch: () => ThunkDispatch<State, void, AnyAction> =
useDispatchOriginal;
export const useSelector: TypedUseSelectorHook<State> = useSelectorOriginal;
import React from "react";
import type { State } from "metabase-types/store";
import { renderWithProviders, screen } from "__support__/ui";
import { createMockUser } from "metabase-types/api/mocks";
import { useDispatch, useSelector } from "./hooks";
const DEFAULT_USER = createMockUser({ email: undefined });
const TEST_EMAIL = "test_email@metabase.test";
describe("useSelector", () => {
it("should allow access to redux store", () => {
const Component = () => {
const email = useSelector(state => state.currentUser?.email);
return <>{email || "No email found"}</>;
};
renderWithProviders(<Component />, {
storeInitialState: {
currentUser: { ...DEFAULT_USER, email: TEST_EMAIL },
},
});
expect(screen.getByText(TEST_EMAIL)).toBeInTheDocument();
expect(screen.queryByText("No email found")).not.toBeInTheDocument();
});
});
describe("useDispatch", () => {
describe("thunk", () => {
function setup({
thunk,
}: {
thunk: () => (dispatch: any, getState: () => State) => void;
}) {
const Component = () => {
const dispatch = useDispatch();
dispatch(thunk());
return <></>;
};
renderWithProviders(<Component />);
}
it("should provide a `dispatch` method that can dispatch a thunk", () => {
const funcInThunk = jest.fn();
setup({ thunk: () => (dispatch: any, getState: any) => funcInThunk() });
expect(funcInThunk).toHaveBeenCalled();
});
it("should properly dispatch thunks that use `getState`", () => {
const foundEmailState = jest.fn();
const didNotFindEmailState = jest.fn();
setup({
thunk: () => (dispatch: any, getState: () => State) => {
const email = getState().currentUser?.email;
email ? foundEmailState() : didNotFindEmailState();
},
});
expect(foundEmailState).toHaveBeenCalled();
expect(didNotFindEmailState).not.toHaveBeenCalled();
});
it("should properly dispatch thunks that use `dispatch`", () => {
const funcInNestedThunk = jest.fn();
const nestedThunk = () => (dispatch: any, getState: any) =>
funcInNestedThunk();
setup({
thunk: () => (dispatch: (thunk: any) => void, getState: any) =>
dispatch(nestedThunk()),
});
expect(funcInNestedThunk).toHaveBeenCalled();
});
});
});
export * from "./utils";
export * from "./hooks";
import { fetchData, updateData, mergeEntities } from "metabase/lib/redux";
import { delay } from "metabase/lib/promise";
import { fetchData, updateData, mergeEntities } from "./utils";
describe("Metadata", () => {
const getDefaultArgs = ({
existingData = "data",
......
......@@ -6,6 +6,7 @@ import _ from "underscore";
import { createMemoryHistory, History } from "history";
import { Router } from "react-router";
import { routerReducer, routerMiddleware } from "react-router-redux";
import type { Reducer } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import { ThemeProvider } from "@emotion/react";
import { DragDropContextProvider } from "react-dnd";
......@@ -23,6 +24,11 @@ import publicReducers from "metabase/reducers-public";
import { getStore } from "./entities-store";
type ReducerValue = ReducerObject | Reducer;
interface ReducerObject {
[slice: string]: ReducerValue;
}
export interface RenderWithProvidersOptions {
mode?: "default" | "public";
initialRoute?: string;
......@@ -30,6 +36,7 @@ export interface RenderWithProvidersOptions {
withSampleDatabase?: boolean;
withRouter?: boolean;
withDND?: boolean;
customReducers?: ReducerObject;
}
/**
......@@ -46,6 +53,7 @@ export function renderWithProviders(
withSampleDatabase,
withRouter = false,
withDND = false,
customReducers,
...options
}: RenderWithProvidersOptions = {},
) {
......@@ -64,11 +72,14 @@ export function renderWithProviders(
? createMemoryHistory({ entries: [initialRoute] })
: undefined;
const reducers = mode === "default" ? mainReducers : publicReducers;
let reducers = mode === "default" ? mainReducers : publicReducers;
if (withRouter) {
Object.assign(reducers, { routing: routerReducer });
}
if (customReducers) {
reducers = { ...reducers, ...customReducers };
}
const store = getStore(
reducers,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment