I have this component:
export const CityForm: FC = () => {
const { state: widgetState } = useContext(WidgetContext);
const { dispatch: geocodingDispatch } = useContext(GeocodingContext);
const [city, setCity] = useState<string>('');
const debouncedCity = useDebouncedValue<string>(city, 800);
const fetchCallback = useCallback(() => {
getLocationCoords(geocodingDispatch)(widgetState.appid, debouncedCity);
}, [debouncedCity]);
useEffect(() => {
if (debouncedCity && widgetState.appid) {
fetchCallback();
} else {
clearLocationCoords(geocodingDispatch)();
}
return () => {
clearLocationCoords(geocodingDispatch)();
}
}, [debouncedCity]);
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
setCity(e.target.value);
};
return (
<Input
data-testid={TEST_ID.CITY_FORM}
type={'text'}
value={city}
onChange={handleOnChange}
placeholder={"Type your City's name..."}
/>
);
};
I'm trying to write a test to check if getLocationCoords function will be called after user input.
this is the test:
it('should call getLocationCoords action after 800 miliseconds', async () => {
const dispatch = jest.fn(() => null);
const testCity = 'city';
const getLocationCoords = jest.fn(() => null);
jest.spyOn(useDebouncedValueModule, 'useDebouncedValue')
.mockImplementation((value) => value);
const component = render(
<ThemeProvider theme={lightTheme}>
<WidgetContext.Provider value={{ state: { appid: 'test' } as IWidgetState, dispatch: dispatch }}>
<GeocodingContext.Provider value={{ state: {} as IGeocodingState, dispatch: dispatch }}>
<CityForm />
</GeocodingContext.Provider>
</WidgetContext.Provider>
</ThemeProvider >
);
const input = await component.findByTestId(TEST_ID.CITY_FORM);
expect(input).toHaveValue('');
await userEvent.type(input, testCity);
expect(input).toHaveValue(testCity);
expect(getLocationCoords).toHaveBeenCalledTimes(1);
});
expect(getLocationCoords).toHaveBeenCalledTimes(1); Received number of calls: 0.
I cannot figure out why this happens. As the component works properly during manual testing.
Since the useDebouncedValue is mocked and returns the value instantaneously the debouncedCity within the component should update and trigger the useEffect
I had to add a spy on getLocationCoords like so:
it('should call getLocationCoords action with testCity as param after after input', async () => {
jest.spyOn(useDebouncedValueModule, 'useDebouncedValue')
.mockImplementation((value) => value);
const getLocationCoordsSpy = jest.spyOn(geocodingActions, 'getLocationCoords');
const component = render(
<ThemeProvider theme={lightTheme}>
<WidgetContext.Provider value={{ state: { appid: 'test' } as IWidgetState, dispatch: dispatch }}>
<GeocodingContext.Provider value={{ state: {} as IGeocodingState, dispatch }}>
<CityForm />
</GeocodingContext.Provider>
</WidgetContext.Provider>
</ThemeProvider >
);
const input = await component.findByTestId(TEST_ID.CITY_FORM);
expect(input).toHaveValue('');
act(() => {
fireEvent.change(input, { target: { value: testCity } });
});
expect(input).toHaveValue(testCity);
expect(getLocationCoordsSpy).toHaveBeenCalledTimes(1);
Related
The bounty expires in 7 days. Answers to this question are eligible for a +500 reputation bounty.
Ludwig is looking for an answer from a reputable source.
I have a context provider in my app:
export const FormContext = createContext<IFormContext | null>(null);
function FormProvider({ caseNumber, children, ...props }: PropsWithChildren<IFormProviderContextProps>) {
const {
data: { caseNumber, taxDocuments, roles },
api,
} = useApiData();
const [error, setError] = useState<string>(null);
const [searchParams, setSearchParams] = useSearchParams();
const activeStep = searchParams.get("step");
const setActiveStep = useCallback((x: number) => {
searchParams.delete("steg");
setSearchParams([...searchParams.entries(), ["step", Object.keys(STEPS).find((k) => STEPS[k] === x)]]);
}, []);
useEffect(() => {
const abortController = new AbortController();
if (case) api.getPersons(case, abortController.signal).catch((error) => setError(error.message));
return () => {
abortController.abort();
};
}, [case]);
useEffect(() => {
const abortController = new AbortController();
if (activeStep === Stepper.INCOME) {
api.getTaxDocuments(abortController.signal).catch((error) => setError(error.message));
}
return () => {
abortController.abort();
};
}, [activeStep]);
useEffect(() => {
const abortController = new AbortController();
api.getCase(caseNumber, abortController.signal).catch((error) => setError(error.message));
}
return () => {
abortController.abort();
};
}, []);
return (
<FormContex.Provider value={{ taxDocuments, case, roles, activeStep, setActiveStep, error, ...props }}>
{children}
</FormContex.Provider>
);
}
I am using this FormProvider as a wrapper for my FormPage:
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/:caseNumber" element={<FormWrapper />} />
<Route path="/" element={<div>Hello world</div>} />
</Routes>
</BrowserRouter>
</React.StrictMode>
function FormWrapper() {
const { caseNumber } = useParams<{ caseNumber?: string }>();
return (
<FormProvider caseNumber={caseNumber}>
<FormPage />
</FormProvider>
);
}
In my FormPage I display components based on the activeStep that I get from FromProvider
export default function FormWrapper({ activeStep, ...props }: FormWrapperProps) {
const renderForm = useMemo(() => {
switch (activeStep) {
case Stepper.TIMELINE:
return <Timeline {...props} />;
case Stepper.INCOME:
return <Income {...props} />;
case Stepper.RESIDENCY:
return <Residency {...props} />;
case Stepper.SUMMARY:
return <Summary {...props} />;
default:
return <Timeline {...props} />;
}
}, [activeStep]);
return <Suspense fallback={<Loader size="3xlarge" title="loading..." />}>{renderForm}</Suspense>;
}
What I would like to do is to implement an abort controller if component gets unmounted to stop the fetch request and state update. I have tried that with implementing it inside useEffect functions of the FormProvider. But, that is repetitive and would like to make some kind of function or a hook that would set the abort controller to every request. I am not sure how to do that with the current setup, where I have my api calls defined in useApiData() hook which looks like this:
export const useApiData = () => {
const [case, setCase] = useState<CaseDto>(null);
const [taxDocuments, setTaxDocuments] = useState<TaxDocumentsResponse[]>([]);
const [roles, setRoles] = useState<IRoleUi[]>([]);
const getCase = async (caseNumber: string, signal?: AbortSignal) => {
const case = await CASE_API.case.findMetadataForCase(caseNumber, { signal });
setCase(case.data);
};
const getPersons = async (case: CaseDto, signal?: AbortSignal) => {
const personPromises = case.roles.map((role) =>
PERSON_API.information.getPersonPost(
{ id: role.id },
{ signal }
)
);
const [...persons] = await Promise.all([...personPromises]);
const roles = persons.map((person) => {
const role = case.roles.find((role) => role.id === person.data.id);
if (!role) throw new Error(PERSON_NOT_FOUND);
return { ...role, ...person.data };
});
setRoles(roles);
};
const getTaxDocuments = async (signal?: AbortSignal) => {
const taxDocumentsDtoPromises = [getFullYear() - 1, getFullYear() - 2, getFullYear() - 3].map((year) =>
TAX_API.integration.getTaxDocument(
{
year: year.toString(),
filter: "",
personId: "123",
},
{ signal }
)
);
const [taxDocument1, taxDocument2, taxDocument3] = await Promise.all([...taxDocumentsDtoPromises]);
setTaxDocuments([taxDocument1.data, taxDocument2.data, taxDocument3.data]);
};
const api = {
getCase,
getPersons,
getTaxDocuments,
};
const data = {
case,
roles,
taxDocuments,
};
return { data, api };
}
As I said I would like to be able to call api without having to define abort controller in every useEffect hook, but I am not sure how to achieve some like this for example:
apiWithAbortController.getCase(caseNumber).catch((error) => setError(error.message))}
I have tried with using a custom hook like this:
export const useAbortController = () => {
const abortControllerRef = useRef<AbortController>();
useEffect(() => {
return () => abortControllerRef.current?.abort();
}, []);
const getSignal = useCallback(() => {
if (!abortControllerRef.current) {
abortControllerRef.current = new AbortController();
}
return abortControllerRef.current.signal;
}, []);
return getSignal;
};
That I was using like this in my useApiData:
const signalAbort = useAbortController();
const getCase = async (caseNumber: string) => {
const case = await CASE_API.case.findMetadataForCase(caseNumber, { signal: signalAbort() });
setCase(case.data);
};
But, that didn't work, with that setup none of the fetch calls were made.
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.
What would be the correct way to test that a component has updated its parent context?
Say from the example below, after MsgSender has been clicked, how can I verify that MsgReader has been updated?
import React from 'react'
import { render, act, fireEvent } from '#testing-library/react'
const MsgReader = React.createContext()
const MsgWriter = React.createContext()
const MsgProvider = ({ init, children }) => {
const [state, setState] = React.useState(init)
return (
<MsgReader.Provider value={state}>
<MsgWriter.Provider value={setState}>{children}</MsgWriter.Provider>
</MsgReader.Provider>
)
}
const MsgSender = ({ value }) => {
const writer = React.useContext(MsgWriter)
return (
<button type="button" onClick={() => writer(value)}>
Increment
</button>
)
}
describe('Test <MsgSender> component', () => {
it('click updates context', async () => {
const { getByRole } = render(
<MsgProvider init={1}>
<MsgSender value={2} />
</MsgProvider>,
)
const button = getByRole('button')
await act(async () => fireEvent.click(button))
// -> expect(???).toBe(2)
})
})
The cleanest way I've managed to come up with is to manually set the *.Providers, but I'm wondering if this is perhaps the wrong way to go about it.
it('click updates context with overrides', async () => {
let state = 1
const setState = (value) => {
state = value
}
const { getByRole } = render(
<MsgReader.Provider value={state}>
<MsgWriter.Provider value={setState}>
<MsgSender value={2} />
</MsgWriter.Provider>
</MsgReader.Provider>,
)
const button = getByRole('button')
expect(state).toBe(1)
await act(async () => fireEvent.click(button))
expect(state).toBe(2)
})
You need to create a customRender which gives you the ability to assert the state like this:
function customRender(ui, { init, ...options }) {
const [state, setState] = React.useState(init);
function wrapper({ children }) {
return (
<MsgReader.Provider value={state}>
<MsgWriter.Provider value={setState}>{children}</MsgWriter.Provider>
</MsgReader.Provider>
);
}
return {
...render(ui, { wrapper, ...options }),
state,
};
}
describe("Test <MsgSender> component", () => {
it("click updates context", async () => {
const { getByRole, state } = customRender(<MsgSender value={2} />);
const button = getByRole("button");
await act(async () => fireEvent.click(button));
expect(state).toBe(2)
});
});
i have defined context like below for setting states and is like below,
interface DialogsCtxState {
isDialogOpen: boolean;
setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
itemVisible: boolean;
setItemVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
const initialState: DialogsCtxState = {
isDialogOpen: false,
setIsDialogOpen: () => {},
itemVisible: false,
setItemVisible: () => {},
};
export const DialogsContext = React.createContext<
DialogsCtxState
>(initialState);
export const DialogsContextProvider: React.FC = ({ children }) => {
const [isDialogOpen, setIsDialogOpen] = React.useState<boolean>(
false
);
const [itemsVisible, setItemsVisible] = React.useState<boolean>(
false
);
return (
<DialogsContext.Provider
value={{
isDialogOpen,
setIsDialogOpen,
itemVisible,
setItemVisible,
}}
>
{children}
</DialogsContext.Provider>
);
};
And i use this context in two components Uploadbutton and userbutton like below,
function UploadButton() {
const {isDialogOpen, setIsDialogOpen, itemVisible, setItemVisible} =
React.useContext(DialogContext);
const onUpload = () => {
itemVisible && setItemVisible(false);
setIsDialogOpen(isDialogOpen => !isDialogOpen);
}
return (
<Button onClick={onUpload}/>
);
}
function UserButton() {
const {isDialogOpen, setIsDialogOpen, itemVisible, setItemVisible} =
React.useContext(DialogContext);
const onAdd = () => {
isDialogOpen && setIsDialogOpen(false);
setItemVisible(prev => !prev);
}
return (
<Button onClick={onAdd}/>
);
}
the above snippet works fine. but i want to move the code withing onUpload and onAdd methods to DialogContext file which evaluates to something like below,
const onAdd = () => {
function1(); //where thiscontains the code in onAdd before snippet.
}
const onUpload = () => {
function2();//where this contains code in onUpload snippet before
}
what i have tried,
in file that contains DialogContext i tried something like below,
export const function1 = () => {
const {isDialogOpen, setIsDialogOpen, itemVisible, setItemVisible} =
React.useContext(DialogContext); //error here
isDialogOpen && setIsDialogOpen(false);
setItemVisible(prev => !prev);
}
export const function2 = () => {
const {isDialogOpen, setIsDialogOpen, itemVisible, setItemVisible} =
React.useContext(DialogContext); //error here
itemVisible && setItemVisible(false);
setIsDialogOpen(isDialogOpen => !isDialogOpen);
}
But i get the error react.usecontext is used in a function which is neither a react function component or custom react hook.
how can i fix this. could someone help me fix this. thanks.
It looks like the only use of the values returned from the context is to create the onUpload and onAdd functions. It will be a better approach to create the functions in the DialogsContextProvider component to pass them as value. Example
// context
interface DialogsCtxState {
onUpload: () => void;
onAdd: () => void;
};
const initialState: DialogsCtxState = {
onUpload: () => {},
onAdd: () => {}
};
export const DialogsContext = React.createContext<
DialogsCtxState
>(initialState);
The DialogsContextProvider component
// context provider
export const DialogsContextProvider: React.FC = ({ children }) => {
const [isDialogOpen, setIsDialogOpen] = React.useState<boolean>(
false
);
const [itemsVisible, setItemsVisible] = React.useState<boolean>(
false
);
// onUpload function
const onUpload = useCallback(() => {
itemsVisible && setItemsVisible(false);
setIsDialogOpen((isDialogOpen) => !isDialogOpen);
}, [itemsVisible]);
// onAdd function
const onAdd = useCallback(() => {
isDialogOpen && setIsDialogOpen(false);
setItemsVisible((prev) => !prev);
}, [isDialogOpen]);
return (
<DialogsContext.Provider value={{ onAdd, onUpload}}>
{children}
</DialogsContext.Provider>
);
};
This is how it can be used in the UploadButton and UserButton components,
const UserButton: React.FC = () => {
const { onAdd } = React.useContext(DialogsContext)
// rest of the logic
}
const UploadButton: React.FC = () => {
const { onUpload } = React.useContext(DialogsContext)
// rest of the logic
}
Note There are multiple typos in your code so if we ignore that the error occurred when you took the custom function approach because useContext can only be used in functional components and custom hooks. To solve the issue you have to take a custom hook approach. For example
export const useOnUpload = () => {
const { isDialogOpen, setIsDialogOpen, itemsVisible, setItemsVisible,
} = React.useContext(DialogsContext);
const onUpload = useCallback(() => {
itemsVisible && setItemsVisible(false);
setIsDialogOpen((isDialogOpen) => !isDialogOpen);
}, [itemsVisible, setIsDialogOpen, setItemsVisible]);
return onUpload;
};
// usage
function UploadButton() {
const onUpload = useOnUpload();
// rest of the logic
}
// similarly you can create the onUseAdd hook
I have this stateless React component:
...
const Providers = ({ onSelectFeedProvider, ... }) => {
const handleSelectFeedProvider = value => e => {
e.preventDefault();
onSelectFeedProvider({ target: { value } });
};
return {
<Row onClick={handleSelectFeedProvider(1)}>
...
</Row>
}
}
And the test:
import Row from 'components/Common/Row';
import Providers from './index';
jest.mock('components/Common/Row', () => 'Row');
let onSelectFeedProviderSpy = jest.fn();
let onSelectProviderSpy = jest.fn();
const initialProps = {
feedProvider: 0,
onSelectFeedProvider: () => onSelectFeedProviderSpy(),
selectedProvider: undefined,
onSelectProvider: () => onSelectProviderSpy()
};
const mockComponent = props => {
const finalProps = { ...initialProps, ...props };
return <Providers {...finalProps} />;
};
it('should call correctly', () => {
const wrapper = shallow(mockComponent());
wrapper.find(Row).simulate('click', 'what do I have to do here');
expect(onSelect).toHaveBeenCalledTimes(1);
});
How can I do to call the method correctly and pass the coverage? I think have tried all the possibilities. Any thoughts?
You don't have many options in this, one approach is to have onSelect injectable
const Component = ({onSelect}) => {
const handleSelect = value => e => {
e.preventDefault()
onSelect && onSelect({ target: { value } })
}
return <Row onClick={handleSelect(1)} />
}
Test
it('should call correctly', () => {
const spy = jest.fn()
const wrapper = shallow(mockComponent({onSelectProvider: spy}));
wrapper.find(Row).simulate('click', 'what do I have to do here');
expect(spy).toHaveBeenCalledTimes(1);
});