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)
});
});
Related
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);
I've created a common component and exported it, i need to call that component in action based on the result from API. If the api success that alert message component will call with a message as "updated successfully". error then show with an error message.
calling service method in action. is there any way we can do like this? is it possible to call a component in action
You have many options.
1. Redux
If you are a fan of Redux, or your project already use Redux, you might want to do it like this.
First declare the slice, provider and hook
const CommonAlertSlice = createSlice({
name: 'CommonAlert',
initialState : {
error: undefined
},
reducers: {
setError(state, action: PayloadAction<string>) {
state.error = action.payload;
},
clearError(state) {
state.error = undefined;
},
}
});
export const CommonAlertProvider: React.FC = ({children}) => {
const error = useSelector(state => state['CommonAlert'].error);
const dispatch = useDispatch();
return <>
<MyAlert
visible={error !== undefined}
body={error} onDismiss={() =>
dispatch(CommonAlertSlice.actions.clearError())} />
{children}
</>
}
export const useCommonAlert = () => {
const dispatch = useDispatch();
return {
setError: (error: string) => dispatch(CommonAlertSlice.actions.setError(error)),
}
}
And then use it like this.
const App: React.FC = () => {
return <CommonAlertProvider>
<YourComponent />
</CommonAlertProvider>
}
const YourComponent: React.FC = () => {
const { setError } = useCommonAlert();
useEffect(() => {
callYourApi()
.then(...)
.catch(err => {
setError(err.message);
});
});
return <> ... </>
}
2. React Context
If you like the built-in React Context, you can make it more simpler like this.
const CommonAlertContext = createContext({
setError: (error: string) => {}
});
export const CommonAlertProvider: React.FC = ({children}) => {
const [error, setError] = useState<string>();
return <CommonAlertContext.Provider value={{
setError
}}>
<MyAlert
visible={error !== undefined}
body={error} onDismiss={() => setError(undefined)} />
{children}
</CommonAlertContext.Provider>
}
export const useCommonAlert = () => useContext(CommonAlertContext);
And then use it the exact same way as in the Redux example.
3. A Hook Providing a Render Method
This option is the simplest.
export const useAlert = () => {
const [error, setError] = useState<string>();
return {
setError,
renderAlert: () => {
return <MyAlert
visible={error !== undefined}
body={error} onDismiss={() => setError(undefined)} />
}
}
}
Use it.
const YourComponent: React.FC = () => {
const { setError, renderAlert } = useAlert();
useEffect(() => {
callYourApi()
.then(...)
.catch(err => {
setError(err.message);
});
});
return <>
{renderAlert()}
...
</>
}
I saw the similar solution in Antd library, it was implemented like that
codesandbox link
App.js
import "./styles.css";
import alert from "./alert";
export default function App() {
const handleClick = () => {
alert();
};
return (
<div className="App">
<button onClick={handleClick}>Show alert</button>
</div>
);
}
alert function
import ReactDOM from "react-dom";
import { rootElement } from ".";
import Modal from "./Modal";
export default function alert() {
const modalEl = document.createElement("div");
rootElement.appendChild(modalEl);
function destroy() {
rootElement.removeChild(modalEl);
}
function render() {
ReactDOM.render(<Modal destroy={destroy} />, modalEl);
}
render();
}
Your modal component
import { useEffect } from "react";
export default function Modal({ destroy }) {
useEffect(() => {
return () => {
destroy();
};
}, [destroy]);
return (
<div>
Your alert <button onClick={destroy}>Close</button>
</div>
);
}
You can't call a Component in action, but you can use state for call a Component in render, using conditional rendering or state of Alert Component such as isShow.
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 contexts/RoomContext.tsx:
import { useState, createContext } from 'react';
const RoomContext = createContext([{}, () => {}]);
const RoomProvider = (props) => {
const [roomState, setRoomState] = useState({ meetingSession: null, meetingResponse: {}, attendeeResponse: {} })
return <RoomContext.Provider value={[roomState, setRoomState]}>
{props.children}
</RoomContext.Provider>
}
export { RoomContext, RoomProvider }
Then in my component, RoomPage.tsx, I have:
const RoomPageComponent = (props) => {
const router = useRouter()
const [roomState, setRoomState] = useContext(RoomContext);
useEffect(() => {
const createRoom = async () => {
const roomRes = await axios.post('http://localhost:3001/live')
console.log('roomRes', roomRes)
setRoomState(state => ({ ...state, ...roomRes.data }))
}
if (router.query?.id) {
createRoom()
}
}, [router])
return <RoomPageWeb {...props} />
}
export default function RoomPage(props) {
return (
<RoomProvider>
<RoomPageComponent {...props} />
</RoomProvider>
)
}
But I get a complaint about the setRoomState:
This expression is not callable.
Type '{}' has no call signatures.
The issue here is that you are trying to use RoomContext in a component(RoomPage) which doesn't have RoomContext.Provider, higher up in the hierarchy since it is rendered within the component.
The solution here to wrap RoomPage with RoomProvider
import { RoomProvider, RoomContext } from '../../contexts/RoomContext'
function RoomPage(props) {
const [roomState, setRoomState] = useContext(RoomContext);
useEffect(() => {
const createRoom = async () => {
const roomRes = await axios.post('http://localhost:3001/live')
console.log('roomRes', roomRes)
setRoomState(state => ({...state, ...roomRes.data}))
}
...
return (
<RoomPageWeb {...props} />
)
export default (props) => (
<RoomProvider><RoomPage {...props} /></RoomProvider>
)
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);
});