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())
});
});
Related
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 />);
})
})
I have two function components with useState in two different files in my project. I want to display the url on my FaceRecognition component if I set fetchSuccess to true.
const ImageLinkForm = () => {
const [url, setUrl] = useState("");
const [fetchSuccess, setFetchSuccess] = useState(false);
const onInputChange = (event) => {
// I get the url and fetchSuccess is true
};
return (
<div>
// I return a form that allowed me to make the fetch call
</div>
);
};
export default ImageLinkForm;
const FaceRecognition = () => {
return (
<div>
{/* if fetchSuccess */}
<img src=url />
</div>
);
};
export default FaceRecognition;
This really depends on how these components are hierarchically related but one easy-ish option is to use the context API
// context/image.js
import { createContext, useState } from "react";
export const ImageContext = createContext({ fetchSuccess: false });
export const ImageContextProvider = ({ children }) => {
const [fetchSuccess, setFetchSuccess] = useState(false);
const setSuccessful = () => {
setFetchSuccess(true);
};
return (
<ImageContext.Provider value={{ fetchSuccess, setSuccessful }}>
{children}
</ImageContext.Provider>
);
};
Your components can then use the context to read the value...
import { useContext } from "react";
import { ImageContext } from "path/to/context/image";
const FaceRecognition = () => {
const { fetchSuccess } = useContext(ImageContext);
return <div>{fetchSuccess && <img src="url" />}</div>;
};
and write the value...
import { useContext, useState } from "react";
import { ImageContext } from "path/to/context/image";
const ImageLinkForm = () => {
const [url, setUrl] = useState("");
const { setSuccessful } = useContext(ImageContext);
const onInputChange = (event) => {
// I get the url and fetchSuccess is true
setSuccessful();
};
return (
<div>{/* I return a form that allowed me to make the fetch call */}</div>
);
};
The only thing you need to do is wrap both these components somewhere in the hierarchy with the provider
import { ImageContextProvider } from "path/to/context/image";
const SomeParent = () => (
<ImageContextProvider>
<ImageLinkForm />
<FaceRecognition />
</ImageContextProvider>
);
I am working through a react app using v17. I have a component that adds an expense. The functionality in the component works as expected in the GUI but when I try to test it using jest/enzyme it throws an error of TypeError: Cannot read properties of undefined (reading 'find'). In the GUI it finds the expense I am trying to edit without issue. Am I not testing it correctly when trying to match a snapshot?
Edit Expense Component
import React from "react";
import { useParams } from "react-router-dom";
import { connect } from "react-redux";
import { ExpenseForm } from "./ExpenseForm";
import { editExpense, removeExpense } from "../actions/expenses";
//React router
import { useNavigate } from "react-router-dom";
export const EditExpensePage = (props) => {
const navigate = useNavigate();
const { expenseID } = useParams();
const foundExpense = props.expenses.find(
(expense) => expense.id === expenseID
);
return (
<div>
<h1>Edit Expense</h1>
<ExpenseForm
expense={foundExpense}
onSubmit={(expense) => {
// //Edit expense action expects 2 params (id, updates)
props.editExpense(expenseID, expense);
// //Redirect to dashboard
navigate("/");
}}
/>
<button
onClick={(e) => {
e.preventDefault();
props.removeExpense(expenseID);
navigate("/");
}}
>
Remove Expense
</button>
</div>
);
};
const mapStateToProps = (state, props) => ({
expenses: state.expenses
});
const mapDispatchToProps = (dispatch) => ({
editExpense: (id, expense) => dispatch(editExpense(id, expense)),
removeExpense: (id) => dispatch(removeExpense(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(EditExpensePage);
Current Test
import React from "react";
import { shallow } from "enzyme";
import { EditExpensePage } from "../../components/EditExpensePage";
import { testExpenses } from "../fixtures/expenses";
let history, editExpense, removeExpense, wrapper;
//Mock for use navigate
const mockedUsedNavigate = jest.fn();
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockedUsedNavigate
}));
const setup = (props) => {
const component = shallow(
<EditExpensePage
{...props}
expense={editExpense}
history={history}
removeExpense={removeExpense}
/>
);
return {
component: component
};
};
describe("EditForm component", () => {
beforeEach(() => {
setup();
});
test("should render EditExpensePage", () => {
expect(wrapper).toMatchSnapshot();
});
});
Updated editExpense value in test
const setup = (props) => {
//editExpense = testExpenses[1]; //Same error
editExpense = jest.fn(); //Same error
let removeExpense = jest.fn();
let history = jest.fn();
const component = shallow(
<EditExpensePage
{...props}
expense={editExpense}
history={history}
removeExpense={removeExpense}
/>
);
return {
component: component
};
};
Updated Test File
import React from "react";
import { shallow } from "enzyme";
import { EditExpensePage } from "../../components/EditExpensePage";
import { testExpenses } from "../fixtures/expenses";
let editExpense, expenseID, removeExpense, wrapper;
//Mock for use navigate
const mockedUsedNavigate = jest.fn();
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockedUsedNavigate
}));
const setup = (props) => {
expenseID = 1;
editExpense = [testExpenses.find((expense) => expense.id === expenseID)];
console.log(editExpense);
//Output from this console log
// [
// {
// id: 1,
// description: 'Wifi payment',
// note: 'Paid wifi',
// amount: 10400,
// createdAt: 13046400000
// }
// ]
const component = shallow(
<EditExpensePage
{...props}
expense={editExpense}
removeExpense={removeExpense}
/>
);
return {
component: component
};
};
describe("EditForm component", () => {
beforeEach(() => {
setup();
});
test("should render EditExpensePage", () => {
expect(wrapper).toMatchSnapshot();
});
});
Updated your code
import React from "react";
import { shallow } from "enzyme";
import { EditExpensePage } from "../../components/EditExpensePage";
import { testExpenses } from "../fixtures/expenses";
let history, editExpense, removeExpense, wrapper;
//Mock for use navigate
const mockedUsedNavigate = jest.fn();
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockedUsedNavigate
}));
const setup = (props) => {
editExpense = testExpenses; //it should be an array like this [{ id: 1 }]
const component = shallow(
<EditExpensePage
{...props}
expenses={editExpense}
history={history}
removeExpense={removeExpense}
/>
);
return {
component: component
};
};
describe("EditForm component", () => {
beforeEach(() => {
setup();
});
test("should render EditExpensePage", () => {
expect(wrapper).toMatchSnapshot();
});
});
Your EditExpensePage is calling props.expenses, but in your test cases, you never set it up.
You only introduce it here
let history, editExpense, removeExpense, wrapper;
but you haven't set the value for editExpense which means it's undefined.
That's why undefined.find throws an error.
I'd suggest you set a mocked value for editExpense.
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 want to check condition based on the boolean value
export const Button:React.FunctionComponent<ButtonProps>
const [loading, setLoading] = React.useState(false)
return(
<container
color={props.color},
border={5}
>
{loading ? <itemlist /> : props.title}
/>
)
I want to test if loading is true then render "<itemList/"> if not just show some title
I want to add some test cases for this
I tried this but i guess this seems to be wrong approach
test('check if loading is true', () => {
const isloadedProp = {
loading: false
}
const output = mount(<Button {...ButtonProps} />)
expect(output.find(container).find(itemlist)).toBeFalsy()
expect(output.find(container).title).toBe('ABC')
any suggestion or answers
You can try jest.spyOn() to gain control of React.useState call.
const useStateSpy = jest.spyOn(React, 'useState');
const setLoadingMock = jest.fn();
let isLoadingState = true;
useStateSpy.mockImplementation(() => [isLoadingState, setLoadingMock]);
test('check if loading is true', () => {
isLoadingState = true;
// [loading, setLoading] = React.useState() will result in
// loading = true, setLoading = setLoadingMock
// your test code
});
test('check if loading is false', () => {
isLoadingState = false;
// [loading, setLoading] = React.useState() will result in
// loading = false, setLoading = setLoading Mock
// your test code
});
UPDATED: Rewrite to using just props and not state
As discussed, here is a solution to Button component using isLoading prop instead of useState.
Button.js
import React from 'react';
import Container from '....'; // insert path to Container component here
import ItemList from '....'; // insert path to ItemList component here
const Button = props => {
return (
<Container
color={props.color}
border={5}
>
{props.isLoading ? <Itemlist /> : props.title}
</Container>
);
};
export default Button;
Please mind that component names should always start with upper-case letter for React to distinguish between HTML tag and components.
Button.test.js
import React from 'react';
import { mount } from 'enzyme';
import Button from './Button';
const ButtonProps = {
color: 'red',
title: 'ABC',
};
describe('when isLoading === true', () => {
let output;
beforeAll(() => {
output = mount(<Button {...ButtonProps} isLoading />);
});
test('ItemList is rendered', () => {
expect(output.find('Itemlist')).toHaveLength(1);
});
test('title is not rendered', () => {
expect(output.find('Container').text()).not.toContain('ABC');
});
});
describe('when isLoading === false', () => {
let output;
beforeAll(() => {
output = mount(<Button {...ButtonProps} isLoading={false} />);
});
test('ItemList is not rendered', () => {
expect(output.find('Itemlist')).toHaveLength(0);
});
test('title is rendered', () => {
expect(output.find('Container').text()).toContain('ABC');
});
});