Basically, the Main task is that I am having a state
const [isShow,setIsShow] = useState(false);
I just want to target the isShow state so I can get the current value of its state,
so I can test whether my component is rendered or not.
Main.js
import React, { useState } from 'react'
export default function Main({
name
}) {
const [state, setState] = useState(0);
const [isShow, setIsShow] = useState(false);
return (
<>
{
isShow && <button name='click' onClick={() => setState(state + 1)}>Click me</button>
}
</>
)
}
Main.test.js
import { fireEvent, render, screen } from "#testing-library/react";
import Main from "./Main";
describe("state", () => {
test("state update or not", () => {
render(<Main />);
const btn = screen.getByRole('button', {
name: "Click me",
hidden: true
});
const span = screen.queryByTestId("state");
expect(span).toHaveTextContent(0);
fireEvent.click(btn);
expect(span).toHaveTextContent(1);
});
it("component visibe or not", () => {
render(<Main />);
})
})
Related
I have a component that renders either a sign in comp or an iframe based on a variable that is set to true or not. The variable will be true if the endpoint(using useParams hook) state is "signIn".
I'm having trouble trying to set or test this in Jest. I've tried setting a variable in jest const isSignIn = true and using props but it's still just showing the iframe comp.
The JSX
const myComp = () => {
const { target } = useParams();
const navigate = useNavigate();
const [endpoint, setEndpoint] = useState(null);
const isSignIn = endpoint === "signIn";
useEffect(() => {
if (Object.values(targetEndpoints).includes(`${BaseURL}/${target}`)) {
setEndpoint(`${BaseURL}/${target}`);
} else {
navigate(`${SignInURL}`);
}
}, [target, navigate]);
console.log({ isSignIn });
return (
<section className="container">
{isSignIn ? (
<SignIn />
) : (
<>
<div className="child__container">
<iframe
className="iframe"
src={`${iframe_URL}/${iframeEndpoints[endpoint]}`}
title="my_iframe"
></iframe>
</div>
</>
)}
</section>
);
};
The Test
import React from "react";
import * as ReactRouterDom from "react-router-dom";
import MyComp from ".";
import SignIn from "#Src/routes/SignIn/SignIn";
import { mount } from "enzyme";
describe("<MyComp/>", () => {
const mockSetState = jest.fn();
jest.mock("react", () => ({
useState: (initial) => [initial, mockSetState],
}));
const comp = mount(<MyComp />);
beforeEach(() => {
jest.spyOn(ReactRouterDom, "useParams").mockImplementation(() => {
return { target: "signIn" };
});
});
it("Should render SignIn comp", () => {
//Below just shows the iframe and not the sign in comp
console.log(comp.debug())
});
});
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.
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've created a custom Hook that detects if a click was done outside of a component:
import { useEffect, useState } from 'react';
const useOutsideClick = (ref) => {
const [clickOutside, setClickOutside] = useState(false);
useEffect(() => {
const handleClick = (e) => {
ref.current?.contains(e.target)
? setClickOutside(false)
: setClickOutside(true);
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [ref]);
return clickOutside;
};
export default useOutsideClick;
I'm using the Hook on a main component. After the user clicks outside the component, the Hook needs to reset to its initial state (outsideClick = false):
const App = () => {
const [activeComponent, setActiveComponent] = useState(null);
const dropDownRef = useRef();
const outsideClick = useOutsideClick(dropDownRef);
useEffect( () => {
if(outsideClick){
setActiveComponent('WhatAreYouWorkingOn');
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// At this point, outsideClick needs to be false again
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
}, [outsideClick, setActiveComponent]);
return (
<div className = 'DropDown' ref = {dropDownRef}/>
);
}
export default App;
How can I reset useOutsideClick to its initial state?
I nav component then will toggle state in a sidebar as well as open and close a menu and then trying to get this pass in code coverage. When I log inside my test my state keeps showing up as undefined. Not sure how to tackle this one here.
Component.js:
const Navigation = (props) => {
const {
classes,
...navProps
} = props;
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return (
<Fragment>
<Button
onClick={toggleMenuOpen}
/>
<SideMenu
toggleSidebar={toggleSidebar}
>
<Menu
onClose={toggleMenuClose}
>
</SideMenu>
</Fragment>
);
};
export default Navigation;
Test.js:
import { renderHook, act } from '#testing-library/react-hooks';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => Navigation({ ...newProps }));
expect(result.current.sidebarOpen).toBeFalsy();
});
Author of react-hooks-testing-library here.
react-hooks-testing-library is not for testing components and interrogating the internal hook state to assert their values, but rather for testing custom react hooks and interacting withe the result of your hook to ensure it behaves how you expect. For example, if you wanted to extract a useMenuToggle hook that looked something like:
export function useMenuToggle() {
const [anchorEl, setanchorEl] = useState(null);
const [sidebarOpen, setsidebarOpen] = useState(false);
const toggleSidebar = () => {
setsidebarOpen(!sidebarOpen);
};
const toggleMenuClose = () => {
setanchorEl(null);
};
const toggleMenuOpen = (event) => {
setanchorEl(event.currentTarget);
};
return {
sidebarOpen,
toggleSidebar,
toggleMenuClose,
toggleMenuOpen
}
}
Then you could test it with renderHook:
import { renderHook, act } from '#testing-library/react-hooks';
// Hooks
import { useMenuToggle } from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { result } = renderHook(() => useMenuToggle());
expect(result.current.sidebarOpen).toBeFalsy();
act(() => {
result.current.toggleSidebar()
})
expect(result.current.sidebarOpen).toBeTruthy();
});
Generally though, when a hook is only used by a single component and/or in a single context, we recommend you simply test the component and allow the hook to be tested through it.
For testing your Navigation component, you should take a look at react-testing-library instead.
import React from 'react';
import { render } from '#testing-library/react';
// Components
import Navigation from './navigation';
test('sidebar should be closed by default', () => {
const newProps = {
valid: true,
classes: {}
};
const { getByText } = render(<Navigation {...newProps} />);
// the rest of the test
});