Not working tests for moct store Mocha Enzyme - reactjs

I am trying to test if a button dispatches an action but instead of an action type I get []. The functionality works completely fine outside the test this is why I don't understand why the test fails.
My test:
const mockStore = configureStore();
const store = mockStore({});
describe('buttonUploadWorks', () => {
it('checks if the button for upload dispatches an action', () => {
const wrapper = shallow(
<Provider store = {store}>
<DropzoneComponent.WrappedComponent/>
</Provider>).dive();
const uploadButton = wrapper.find('button').at(0);
uploadButton.simulate('onClickUpload');
const expectedAction = UPLOAD_STARTING;
wrapper.setState({ accepted: ['abc'] });
const action = store.getActions();
expect(action).to.equal(expectedAction);
});
});
My actions:
export const uploadStarting = () => {
return {type: UPLOAD_STARTING};
};
export const uploadSuccess = uploadMessage => {
return {type: UPLOAD_SUCCESS, uploadMessage};
};
export const uploadFail = error => {
return {type: UPLOAD_FAIL, error};
};
export function tryUpload (file) {
const formData = new FormData();
formData.append('file', file);
return (dispatch) => {
dispatch(uploadStarting());
axios.post(filesEndpoint, formData).then(function (response) {
dispatch(uploadSuccess(response));
}).catch(function (error) {
dispatch(uploadFail(error));
});
};
};
And the button:
<button className='buttonUpload' onClick={() => { this.state.accepted.length > 0 ? this.onClickUpload().bind(this) : alert('No files presented'); }}>UPLOAD</button>
onClickUpload() {
this.props.dispatch(tryUpload(this.state.accepted[0]));
localStorage.clear();
this.setState({accepted: []});
this.props.history.push(searchPath);
}

That's happening because setState({accepted:[]}) triggers before this.props.dispatch(tryUpload(this.state.accepted[0])) you could fix it binding dispatch function inside a promise function and then calling the setState function.
JavaScript Promises

Related

How do I initialise state values and methods that uses useSyncExternalStore + Context in React?

Description
I'm creating a state management tool for a small project, using mainly useSyncExternalStore from React, inspired by this video from Jack Herrington https://www.youtube.com/watch?v=ZKlXqrcBx88&ab_channel=JackHerrington.
But, I'm running into a pattern that doesn't look right, which is having to use 2 providers, one to create the state, and the other to initialise it.
The gist of the problem:
I have a property sessionId coming from an HTTP request. Saving it in my store wasn't an issue.
However, once I have a sessionId then all of my POST requests done with notifyBackend should have this sessionId in the request body. And I was able to achieve this requirement using the pattern above, but I don't like it.
Any idea how to make it better ?
Code
CreateStore.jsx (Not important, just providing the code in case)
export default function createStore(initialState) {
function useStoreData(): {
const store = useRef(initialState);
const subscribers = useRef(new Set());
return {
get: useCallback(() => store.current, []),
set: useCallback((value) => {
store.current = { ...store.current, ...value };
subscribers.current.forEach((callback) => callback());
}, []),
subscribe: useCallback((callback) => {
subscribers.current.add(callback);
return () => subscribers.current.delete(callback);
}, []),
};
}
const StoreContext = createContext(null);
function StoreProvider({ children }) {
return (
<StoreContext.Provider value={useStoreData()}>
{children}
</StoreContext.Provider>
);
}
function useStore(selector) {
const store = useContext(StoreContext);
const state = useSyncExternalStore(
store.subscribe,
() => selector(store.get()),
() => selector(initialState),
);
// [value, appendToStore]
return [state, store.set];
}
return {
StoreProvider,
useStore,
};
}
Creating the state
export const { StoreProvider, useStore } = createStore({
sessionId: "INITIAL",
notifyBackend: () => { },
});
index.jsx
<Router>
<StoreProvider>
<InitialisationProvider>
<App />
</InitialisationProvider>
</StoreProvider>
</Router
InitialisationContext.jsx
const InitialisationContext = createContext({});
export const InitializationProvider = ({ children }) {
const [sessionId, appendToStore] = useStore(store => store.session);
const notifyBackend = async({ data }) => {
const _data = {
...data,
sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
appendToStore({ sessionId: result.sessionId });
} else if (result.otherProp) {
appendToStore({ otherProp: result.otherProp });
}
} catch (e) { }
};
useEffect(() => {
appendToStore({ notifyBackend });
}, [sessionId]);
return (
<InitialisationContext.Provider value={{}}>
{children}
</InitialisationContext.Provider>
);
}
I just tried out Zustand, and it's very similar to what I'm trying to achieve.
Feels like I'm trying to reinvent the wheel.
With Zustand:
main-store.js
import create from 'zustand';
export const useMainStore = create((set, get) => ({
sessionId: 'INITIAL',
otherProp: '',
notifyBackend: async ({ data }) => {
const _data = {
...data,
sessionId: get().sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
set({ sessionId: result.sessionId });
} else if (result.otherProp) {
set({ otherProp: result.otherProp });
}
} catch (e) { }
},
}));
SomeComponent.jsx
export const SomeComponent() {
const sessionId = useMainStore(state => state.sessionId);
const notifyBackend = useMainStore(state => state.notifyBackend);
useEffect(() => {
if (sessionId === 'INITIAL') {
notifyBackend();
}
}, [sessionId]);
return <h1>Foo</h1>
};
This answer focuses on OPs approach to createStore(). After reading the question a few more times, I think there are bigger issues. I'll try to get to these and then extend the answer.
Your approach is too complicated.
First, the store is no hook! It lives completely outside of react. useSyncExternalStore and the two methods subscribe and getSnapshot are what integrates the store into react.
And as the store lives outside of react, you don't need a Context at all.
Just do const whatever = useSyncExternalStore(myStore.subscribe, myStore.getSnapshot);
Here my version of minimal createStore() basically a global/shared useState()
export function createStore(initialValue) {
// subscription
const listeners = new Set();
const subscribe = (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
}
const dispatch = () => {
for (const callback of listeners) callback();
}
// value management
let value = typeof initialValue === "function" ?
initialValue() :
initialValue;
// this is what useStore() will return.
const getSnapshot = () => [value, setState];
// the same logic as in `setState(newValue)` or `setState(prev => newValue)`
const setState = (arg) => {
let prev = value;
value = typeof arg === "function" ? arg(prev) : arg;
if (value !== prev) dispatch(); // only notify listener on actual change.
}
// returning just a custom hook
return () => useSyncExternalStore(subscribe, getSnapshot);
}
And the usage
export const useMyCustomStore = createStore({});
// ...
const [value, setValue] = useMyCustomStore();

Jest-React hook Testing : How to call a setState inside a custom function using useEffect

I am trying to set mockPatient data and wanted to test if the 'sortByCaseFn ' function is called by the useEffect.
Here is my sourcecode:
Patient.tsx
const [patients, setPatients] = useState([]);
const [sortBy, setSortBy] = useState('events');
const [fetched, setFetched] = useState(false);
const dispatch = useAppDispatch();
const props = useAppSelector((state) => state.myPatientProps);
const getPatientData = (): void => {
dispatch(MyPatientActions.getMyPatientsData());
};
const sortByCaseFn = (sortBy, list) => {
let patientsToSort = [...list];
if (sortBy.includes('events'))
patientsToSort.sort(
sorter.byPropertiesOf(['-ActiveEventsCount', 'LastName'])
);
if (sortBy.includes('vae'))
patientsToSort.sort(sorter.byPropertiesOf(['-VaeStatus']));
console.log('patientsToSort---', patientsToSort);
setPatients(patientsToSort);
};
useEffect(() => {
if (!fetched) {
getPatientData();
}
}, []);
useEffect(() => {
console.log('setpatients called .. ', patients);
}, [patients]);
useEffect(() => {
const saved_sortby = localStorage.getItem('sortby');
if (saved_sortby) {
sortByCaseFn(saved_sortby, props.myPatientDetails);
} else sortByCaseFn('events', props.myPatientDetails);
setFetched(true);
}, [props.myPatientDetails]);
useEffect(() => {
sortByCaseFn(sortBy, patients);
}, [sortBy]);
return (
<> Render Patient List </> )
My Test Code :
Patients.test.tsx
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: jest.fn()
}));
export const setHookTestState = (newState: any) => {
const setStateMockFn = () => {};
return Object.keys(newState).reduce((acc, val) => {
acc = acc?.mockImplementationOnce(() => [newState[val], setStateMockFn]);
return acc;
}, jest.fn());
};
describe('My Patient Screen', () => {
const useSelectorMock = reactRedux.useSelector as jest.Mock<any>;
const useDispatchMock = reactRedux.useDispatch as jest.Mock<any>;
beforeEach(() => {
useSelectorMock.mockImplementation((selector) => selector(mockStore));
useDispatchMock.mockImplementation(() => () => {});
});
afterEach(() => {
useDispatchMock.mockClear();
useSelectorMock.mockClear();
});
const mockInitialState = {
myPatientDetails: vaeMock,
fetching: false,
failedMsg: '',
requestPayload: {}
};
const mockStore = {
counter: undefined,
menu: undefined,
selectPatientProps: undefined,
myPatientProps: mockInitialState
};
test('validate sorting by events', async (done) => {
React.useState = setHookTestState({
patients: vaeMock,
sortBy: 'vae',
fetched: 'false'
});
const {
getByText,
getByRole,
getByTestId,
getAllByTestId,
findAllByTestId,
queryByText,
container
} = render(<Mypatient />);
await waitFor(() => {
expect(getByText('Ander, Sam')).toBeDefined();
});
const list = getAllByTestId('patientname');
expect(within(list[0]).getByText('Sara, Jone')).toBeInTheDocument(); //Fails here as Sorting doesnt happen
console.log('....list ', list);
});
});
My Observations:
The 'vaeMock' data that I set in redux state 'mockInitialState' is successfully sent as props
The 'vaeMock' data that I set in component state using setHookTestState is also set successfully.
The lifecycle events happens like this -
a. setPatients() is called using the component state data.
b. using props that is sent , sortByCaseFn is called but setPatients is not called.
c. again using the component state , sortByCaseFn is called but setPatients is not set.
Without setting the component state variables runs into a TypeError: Undefined is not iterable.
All Iam trying to do is - send a mockData to a component that uses useDispatch, useEffects
and sort the data on the component mount and initialize to local state variable.

Unit Testing with Mocha,Enzyme dispatched functions in functional components which uses React Hooks + Redux

I am trying to test a dispatch from 'mapDispatchToProps' defined with a functional component which uses useEffect() hook and the function is called there.
export const MyComponent = (props) => {
useEffect(() => {
// Anything in here is fired on component mount.
props.registerInStore(props.id, false);
return () => {
// Anything in here is fired on component unmount.
props.resetInStore(props.id);
};
}, []);
const handleOnClick = () => {
props.toggle(props.id);
};
return (
<div >
{!props.isOpen ? (
<button
onClick={handleOnClick}>
Open
</button>
) : (
<button
onClick={handleOnClick}>
close
</button>
)}
</div>
);
};
const mapDispatchToProps = (dispatch) => ({
registerInStore(id, isOpen) {
dispatch(registerInStore(id, isOpen));
},
resetInStore(id) {
dispatch(resetInStore(id));
}
});
export default connect(null, mapDispatchToProps)(MyComponent);
In my unit tests with Mocha and enzyme i also want to test the dispatches inside 'mapDispatchToProps', what i did below does not seem to work :
describe('<MyComponent/>', () => {
let store = mockStore({
toggles: [
{
id: 10,
isOpen: true
}
]
}
});
const options = {
context: {store},
childContextTypes: {store: PropTypes.object.isRequired},
lifecycleExperimental: true
};
const setup = (inputProps = {}) => {
const props = {
id: 10,
isOpen: false,
registerInStore: expect.createSpy(),
resetInStore: expect.createSpy(),
toggle: expect.createSpy(),
...inputProps
};
const wrapper = mount(<MyComponent {...props} />, options);
return {
props,
wrapper
};
};
afterEach(() => {
expect.restoreSpies();
});
it('should dispatch', async () => {
const {wrapper}=setup();
await store.dispatch(wrapper.prop('registerInStore')(10,false));
/* i tried the commented way too instead of directly dispatching*/
// wrapper.prop('registerInStore')(10,false);
//await new Promise((resolve) => setTimeout(resolve, 50));
const expectedActions = [{type: 'REGISTER_IN_STORE', id: 10, isOpen: false}];
expect(store.getActions()).toEqual(expectedActions);
});
the store.getActions() is returning an empty array, i am new to React Hooks and testing, what am i doing wrong, any other solutions?.
Thanks in Advance.
worked by removing the spies e.g:-
const setup = (inputProps = {}) => {
const props = {
id: 10,
isOpen: false,
registerInStore:()=>null,
resetInStore: ()=>null,
toggle: ()=>null,
...inputProps
};
const wrapper = mount(<MyComponent {...props} />, options);
return {
props,
wrapper
};
};

How to cover lines in mapDispatchToProps with Jest?

My question is how do we cover these lines in jest?
export const mapDispatchToProps = dispatch => {
return {
submitClaimsForm: form => {
dispatch(submitClaimsForm(form));
}
};
};
In my component this is what the redux connected area looks like:
export function mapStateToProps(state) {
return {
formNonMember: state.form,
submissionSuccess: state.claimSubmission.submissionSuccess
};
}
export const mapDispatchToProps = dispatch => {
return {
submitClaimsForm: form => {
dispatch(submitClaimsForm(form));
}
};
};
let AdditionalDetailsFormConnect = reduxForm({
form: 'AdditionalDetails',
destroyOnUnmount: false
})(AdditionalDetailsForm);
export default connect(
mapStateToProps,
mapDispatchToProps
)(AdditionalDetailsFormConnect);
And this is how the dispatched action is used:
onSubmit() {
this.props.submitClaimsForm(this.props.formattedForm);
}
Next this is what the actual action looks like:
import {postClaimsForm} from '../shared/services/api';
export const Actions = {
SET_SUBMISSION_STATUS: 'SET_SUBMISSION_STATUS'
};
export const submitClaimsForm = form => dispatch => {
return postClaimsForm(form)
.then(res => {
// console.log('promise returned:', res);
return dispatch({
type: Actions.SET_SUBMISSION_STATUS,
submissionSuccess: true
});
})
.catch(error => {
// console.log('error returned:', error);
return dispatch({
type: Actions.SET_SUBMISSION_STATUS,
submissionSuccess: false
});
});
};
What I've tried so far:
it('mapDispatchToProps works as expected', () => {
const actionProps = mapDispatchToProps({
submitClaimsForm: jest.fn()
});
actionProps.submitClaimsForm();
expect(submitClaimsForm).toHaveBeenCalled();
});
But this errors and tells me that dispatch is undefined.
I also have this test, which passes, it tells me that submitClaimsForm has been called, but it just covers the lines for onSubmit:
it('onSubmit is called on submit', function() {
const spyOnSubmit = jest.spyOn(wrapper.instance(), 'onSubmit');
const mockHandleSubmit = jest.fn(wrapper.instance().onSubmit);
const submitClaimsForm = jest.fn(wrapper.instance().submitClaimsForm);
wrapper.setProps({
handleSubmit: mockHandleSubmit,
submitClaimsForm
});
wrapper.find('MyForm').simulate('submit');
expect(mockHandleSubmit).toHaveBeenCalled();
expect(spyOnSubmit).toHaveBeenCalled();
expect(submitClaimsForm).toHaveBeenCalled(); // <--
});
The reason your mapDispatchToProps works as expected test fails is because mapDispatchToProps expects a dispatch function to be passed in, not the map itself (that's what mapDispatchToProps returns).
This should work:
jest.mock('./actions');
import * as actions from './actions';
it('mapDispatchToProps calls the appropriate action', async () => {
// mock the 'dispatch' object
const dispatch = jest.fn();
const actionProps = mapDispatchToProps(dispatch);
const formData = { ... };
actionProps.submitClaimsForm(formData);
// verify the appropriate action was called
expect(actions.submitClaimsForm).toHaveBeenCalled(formData);
});

How to mock an object/function inside a module

I am having trouble mocking a function inside a module. (Not sure if this is possible.
So I have a module called myActions.ts
import * as Config from "../utils/Config";
const _config = Config.getConfig();
export const requestLoadDataA = (postLoadAction: Function = undefined) => {
return async function (dispatch, getState) {
const client = { url: `${_config.ApiUrl}getDataA` };
...
}
}
This module contains a Config.getConfig() and this is what I want to mock.
Config module looks like this:
export const getConfig = () => {
const app = document.getElementById("react-app");
if (app) {
const config = app.dataset.configuration;
return JSON.parse(config) as IConfiguration;
} else {
return undefined;
}
};
This is the jest test I have so far and it doesn't work:
describe("DATA_A Action Creator (Sync): Tests", () => {
afterEach(fetchMock.restore);
it("REQUEST and SUCCESS actions on successful loadData()", () => {
const dataA: any = require("../../__mockData__/dataA.json");
fetchMock.mock("/getDataA", {
status: 200,
body: dataA
});
const _config = { };
const spy = jest.spyOn(Config, "getConfig");
spy.mockReturnValue(_config);
const store = mockStore({
dataA: {
hasLoadedEntities: false,
isLoadingEntities: false
}
});
return store.dispatch(aActions.requestLoadDataA())
.then(() => {
const expectedActions = store.getActions();
expect(expectedActions.length).toEqual(2);
expect(expectedActions).toContainEqual({ type: ACTION_TYPES.LOAD_A_REQUESTED });
expect(expectedActions).toContainEqual({ type: ACTION_TYPES.LOAD_A_SUCCESS, data: resultData });
});
});
}
I get a "cannot read property 'ApiUrl' of undefined.
How can I mock the _config.ApiUrl object?
Not sure if I understood you.
you can mock your default import like this
import * as Config from "../utils/Config";
jest.mock("../utils/Config", () => ({
getConfig: () => ({ ApiUrl: 'yourMockApiUrl' })
}));

Resources