How to test if button was clicked? - javascript

I'm working with React Testing Library. The problem I have is that fireEvent doesn't trigger, and the error throws, is following:
Expected number of calls: >= 1
Received number of calls: 0
For this particular test do I need to work with #testing-library/react-hooks? Do I need to render my Product component wrapped on context provider where the function is imported from?
Product.tsx component
import React, { FunctionComponent } from "react";
import { useProducts } from "./ContextWrapper";
import { Button } from "#mui/material";
import { articleProps } from "./types";
const Product: FunctionComponent<{ article: articleProps }> = ({
article,
}) => {
const { handleAddToCart } = useProducts(); // this is from global context
return (
<Button
aria-label="AddToCart"
onClick={() => handleAddToCart(article)}
>
Add to cart
</Button>
);
};
export default Product;
Product.test.tsx
import React from "react";
import { fireEvent, render, screen } from "#testing-library/react";
import Product from "./Product";
import { act, renderHook } from "#testing-library/react-hooks";
const article = {
name: "samsung",
variantName: "phone",
categories: [],
};
const renderProduct = () => {
return render(
<ContextProvider>
<Product article={article} />
</ContextProvider>
);
};
//first test just tests if button exists with provided content and works fine
test("renders product", () => {
render(<Product article={article} />);
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
expect(AddToCartbutton).toHaveTextContent("Add to cart");
});
// this test throws the error described above
test("test addToCart button", () => {
renderProduct();
const onAddToCart = jest.fn();
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
fireEvent.click(AddToCartbutton);
expect(onAddToCart).toHaveBeenCalled();
});
ContextWrapper.tsx
import React, {createContext, useContext, ReactNode, useState} from "react";
import { prodProps } from "../types";
type ProductContextProps = {
productData: prodProps;
handleAddToCart: (clickedItem: productPropsWithAmount) => void;
};
const ProductContextDefaultValues: ProductContextProps = {
productData: null as any,
handleAddToCart:null as any;
};
type Props = {
children: ReactNode;
};
const ProductContext = createContext<ProductContextProps>(
ProductContextDefaultValues
);
export function useProducts() {
return useContext(ProductContext);
}
const ContextWrapper = ({ children }: Props) => {
const { data } = useGraphQlData();
const [productData, setProductData] = useState<Category>(data);
const [cartItems, setCartItems] = useState<prodProps[]>([]);
useEffect(() => {
if (data) {
setProductData(data);
}
}, [data]);
const handleAddToCart = (clickedItem: prodProps) => {
setCartItems((prev: prodProps[]) => {
return [...prev, { ...clickedItem }];
});
};
return (
<ProductContext.Provider
value={{
productData,
handleAddToCart,
}}
>
{children}
</ProductContext.Provider>
);
};
export default ContextWrapper;

I advise you to create Button Component and test it separately like this:
const onClickButton = jest.fn();
await render(<Button onClick={onClickButton} />);
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
await fireEvent.click(AddToCartbutton);
expect(onClickButton).toHaveBeenCalled();

One way to accomplish this is to mock ContextWrapper, as your test is specifically referring to Product component.
So, you could do something like this into your test:
import * as ContextWrapper from '--- PATH TO ContextWrapper ---';
test('test addToCart button', () => {
/// mockFunction
const onAddToCart = jest.fn();
jest.spyOn(ContextWrapper, 'useProducts').mockImplementationOnce(() => ({
productData: {
example_prodProp1: 'initialValue1',
example_prodProp2: 'initialValue2',
},
// Set mockFunction to handleAddToCart of useProducts
handleAddToCart: onAddToCart,
}));
render(<Product article={article} />);
const AddToCartbutton = screen.getByRole('button', { name: /AddToCart/i });
fireEvent.click(AddToCartbutton);
expect(onAddToCart).toHaveBeenCalledWith({
prodProp1: 'samsung',
prodProp2: 'phone',
});
});
In this line jest.spyOn(ContextWrapper, 'useProducts').mockImplementation we are mocking useProducts return value and setting the handleAddToCart function to your mockFunction and that's how you can check if it has been called.
* This test is strictly focused on Product component and you just want to garantee that the component calls the handleAddToCart function from ContextWrapper.
For test how handleAddToCart should work, you can create a specific test for the ContextWrapper.

Related

Mock antd useBreakpoint hook

I want to test the modal component, but there is an error with defining the cancel button,
it renders only if it's not mobile.
isMobile is a variable that is a boolean value from hook - useBreakpoint (ant design library hook).
I don't know how to mock that value, or how to click that button.
Note: if I remove the isMobile check, the button clicks well:)
import React from 'react'
import {Grid, Typography} from 'antd'
import {Button} from '#/components/Button'
import {Modal} from '#/components/Modal'
import translations from './translations'
import {ConfirmationModalProps} from './props'
const {Text} = Typography
const {useBreakpoint} = Grid
export const ConfirmationModal = ({visible, onClose, children}: ConfirmationModalProps) => {
const screens = useBreakpoint()
const isMobile = screens.xs
return (
<Modal
title={translations().chargeConfirmation}
visible={visible}
onOk={onClose}
onCancel={onClose}
footer={[
!isMobile && (
<Button role={'cancel-button'} type={'ghost'} key={'cancel'} onClick={onClose}>
{ translations().cancel }
</Button>
),
<Button type={'primary'} key={'charge'} onClick={onClose}>
{ translations().confirm }
</Button>
]}
>
<Text>{translations().confirmationText(children)}</Text>
</Modal>
)
}
describe('ConfirmationModal', () => {
it('should should the children and close button', async () => {
const onClose = jest.fn()
jest.mock('antd/es/grid/hooks/useBreakpoint', () => ({
xs: false
}))
render(<ConfirmationModal onClose={onClose} visible={true}>100</ConfirmationModal>)
const child = screen.getByText('Are you sure you want to charge 100')
expect(child).toBeTruthy()
expect(screen.queryByTestId('cancel')).toBeDefined()
await waitFor(() => screen.queryByTestId('cancel'))
fireEvent.click(screen.queryByRole('cancel-button'))
expect(onClose).toHaveBeenCalledTimes(1)
})
})
Errors are:
Error: Unable to fire a "click" event - please provide a DOM element.
Unable to find an accessible element with the role "cancel-button"
Depending on queryByRole or getByRole selector.
What is wrong?
Let's take a look at the source code of the useBreakpoint hook.
import { useEffect, useRef } from 'react';
import useForceUpdate from '../../_util/hooks/useForceUpdate';
import type { ScreenMap } from '../../_util/responsiveObserve';
import ResponsiveObserve from '../../_util/responsiveObserve';
function useBreakpoint(refreshOnChange: boolean = true): ScreenMap {
const screensRef = useRef<ScreenMap>({});
const forceUpdate = useForceUpdate();
useEffect(() => {
const token = ResponsiveObserve.subscribe(supportScreens => {
screensRef.current = supportScreens;
if (refreshOnChange) {
forceUpdate();
}
});
return () => ResponsiveObserve.unsubscribe(token);
}, []);
return screensRef.current;
}
export default useBreakpoint;
It uses ResponsiveObserve.subscribe() to get the supportScreens, it calls ResponsiveObserve.register(), the .register() method use window.matchMedia() underly. jestjs use JSDOM(a DOM implementation) as its test environment, but JSDOM does not implement window.matchMedia() yet. So we need to mock it, see Mocking methods which are not implemented in JSDOM
E.g.
import { render } from '#testing-library/react';
import React from 'react';
import { Grid } from 'antd';
const { useBreakpoint } = Grid;
describe('72021761', () => {
test('should pass', () => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(
(query) =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(max-width: 575px)' });
},
removeListener: jest.fn(),
matches: query === '(max-width: 575px)',
} as any)
),
});
let screensVar;
function Demo() {
const screens = useBreakpoint();
screensVar = screens;
return <div />;
}
render(<Demo />);
expect(screensVar).toEqual({
xs: true,
sm: false,
md: false,
lg: false,
xl: false,
xxl: false,
});
});
});

Update a component after useState value updates

Having a monaco-editor inside a React component:
<Editor defaultValue={defaultValue} defaultLanguage='python' onChange={onChangeCode} />
The defaultValue, the default code inside of the editor, is sent via props to the component:
const MyComponent = ({
originalCode
}: MyComponentProps) => {
const [defaultValue, setDefaultValue] = useState(originalCode);
When the user edits the code, onChange={onChangeCode} is called:
const onChangeCode = (input: string | undefined) => {
if (input) {
setCode(input);
}
};
My question is, how to reset the code to the original one when the user clicks on Cancel?
Initially it was like:
const handleCancel = () => {
onChangeCode(defaultValue);
};
but it didn't work, probably because useState is asynchronous, any ideas how to fix this?
Here is the whole component for more context:
import Editor from '#monaco-editor/react';
import { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, HeaderWithButtons } from '../shared/ui-components';
import { ICalculationEngine } from '../../../lib/constants/types';
import { usePostScript } from '../../../lib/hooks/use-post-script';
import { scriptPayload } from '../../../mocks/scriptPayload';
import { editorDefaultValue } from '../../../utils/utils';
export interface ScriptDefinitionProps {
realInputDetails: Array<ICalculationEngine['RealInputDetails']>;
realOutputDetails: ICalculationEngine['RealInputDetails'];
originalCode: string;
scriptLibId: string;
data: ICalculationEngine['ScriptPayload'];
}
const ScriptDefinition = ({
realInputDetails,
realOutputDetails,
originalCode
}: ScriptDefinitionProps) => {
const [defaultValue, setDefaultValue] = useState(originalCode);
const [code, setCode] = useState(defaultValue);
const { handleSubmit } = useForm({});
const { mutate: postScript } = usePostScript();
const handleSubmitClick = handleSubmit(() => {
postScript(scriptPayload);
});
const handleCancel = () => {
onChangeCode(defaultValue);
};
const onChangeCode = (input: string | undefined) => {
if (input) {
setCode(input);
}
};
useEffect(() => {
setDefaultValue(editorDefaultValue(realInputDetails, realOutputDetails));
}, [realInputDetails, realOutputDetails, originalCode]);
return (
<div>
<HeaderWithButtons>
<div>
<Button title='cancel' onClick={handleCancel} />
<Button title='save' onClick={handleSubmitClick} />
</div>
</HeaderWithButtons>
<Editor defaultValue={defaultValue} defaultLanguage='python' onChange={onChangeCode} />
</div>
);
};
export default ScriptDefinition;
If you need the ability to change the value externally, you'll need to use the Editor as a controlled component by passing the value prop (sandbox):
For example:
const defaultValue = "// let's write some broken code 😈";
function App() {
const [value, setValue] = useState(defaultValue);
const handleCancel = () => {
setValue(defaultValue);
};
return (
<>
<button title="cancel" onClick={handleCancel}>
Cancel
</button>
<Editor
value={value}
onChange={setValue}
height="90vh"
defaultLanguage="javascript"
/>
</>
);
}

Problem when maping array - React with Redux Toolkit

I have the following problem: I use a fatch API to get all the products I have registered in my database (mongodb), then I store the result in a slice called products-slice which has an array as its initial state empty. Until then everything is in order. As I need information the time the homepage is loaded, I use the useEffect hook to fetch the products I have registered. Then I pass this array as props to a component, and make a map. The problem is that when the component loads, the information is not local.
código do backend
module.exports.fetchProduct = async (req, res) => {
try {
const products = await Product.find({});
if (products) {
res.json(products);
}
} catch (error) {
console.log(error);
}
};
productsActions.js
export const fetchProducts = () => {
return async (dispatch) => {
try {
const response = await fetch("http://localhost:xxxx/xxxxxx");
const data = await response.json();
let loadedProducts = [];
for (const key in data) {
loadedProducts.push({
id: data[key]._id,
productName: data[key].productName,
price: data[key].price,
imageUrl: data[key].imageUrl,
});
}
dispatch(setProducts(loadedProducts));
} catch (error) {
console.log(error);
}
};
};
home.jsx
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Container } from "react-bootstrap";
import {fetchProducts} from '../../store/actions/productsActions';
import Hero from "../hero/Hero";
import Footer from "../footer/Footer";
import DisplayProductsList from "../displayProduct/DisplayProductsList";
export default function Home() {
const productsInfo = useSelector((state) => state.products.products);
console.log(productsInfo);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
return (
<>
<Hero />
<DisplayProductsList products={productsInfo} />
<Container fluid>
<Footer></Footer>
</Container>
</>
);
}
product-slice.js
const initialState = {
products: [],
};
const productSlice = createSlice({
name: "product",
initialState,
reducers: {
setProducts(state, action) {
state.products.push(action.payload);
},
},
});
export const { setProducts } = productSlice.actions;
export default productSlice.reducer;
component where I'm mapping
export default function DisplayProductsList(props) {
console.log(props);
return (
props.products.map((product) => (
<DisplayProducts
key={product.id}
imageUrl={product.imageUrl}
name={product.productName}
price={product.price}
/>
))
);
}
console.log output in the above component
enter image description here

TypeError: updateElement is not a function

I am trying to update an element from an array by adding an object as a property like shown in this picture
When a user clicks on a single node button, a modal appears the user fills the form and then it is addes as a property for this node.
But for some reason I get this type error that says that the updateElement is not a function.
BTW, I am using Redux & react-flow-renderer libraries.
Reducer
import * as types from '../actions/types';
const initialState = {
elements: []
};
const flow = (state = initialState, action) => {
switch (action.type) {
case types.UPDATE_ELEMENT:
return {
...state,
elements: state.elements.map((e) => {
if (e.id === action.payload.id) {
e = {
...e,
options: action.payload.options,
};
}
return e;
}),
};
default:
return state;
}
};
export default flow;
Action
import { UPDATE_ELEMENT } from './types';
export const updateElement = (data) => (dispatch) => {
dispatch({
type: UPDATE_ELEMENT,
payload: data,
});
};
Node modal
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { updateElement } from '../../../../redux/actions/flow';
const VPCNodeModal = (props, { updateElement }) => {
const [formData, setFormData] = useState({
instance: '',
});
// options
const { instance } = formData;
const onFormChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmitForm = () => {
const update = {
id: selectedElement.id,
options: formData,
};
updateElement(update);
};
return (
<>
<Modal {...props}>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmitForm();
}}
>
<label>
<span> Instance name:</span>
<input
type='text'
name='instance'
value={instance}
onChange={onFormChange}
/>
</label>
<button type='submit'>Submit</button>
</form>
</Modal>
</>
);
};
VPCNodeModal.propTypes = {
updateElement: PropTypes.func.isRequired
};
export default connect(null, { updateElement })(VPCNodeModal);
Issue is while receiving the props.
change
const VPCNodeModal = (props, { updateElement }) => {
to
const VPCNodeModal = (props) => {
const { updateElement } = props;
updateElement is a props was passes in VPCNodeModal. So you should update like this with spread operator
const VPCNodeModal = ({ updateElement, ...props }) => {

React test a component with saga

Hllo Guys, I'm having a bit trouble with testing my component
The problem is that I would like to test my React Native Component that uses saga to fetch data from server.
The Problem is that I do know what I'm supposed to do, I think I should mock my API calls in my test file but I do not know how :/
The component file is really simple, when mounted it dispatches action to fetch list on vehicles, and then it shows them in UI. And until that is fetched it shows loading text
Bellow are my current setup of components & test file.
Here is a screen component that fetches initial data on screen load
Screen Component
import React, { useContext, useEffect, useState } from 'react';
import { Platform, FlatList, View, ActivityIndicator, Text } from 'react-native';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { vehiclesActions } from '_store/vehicles';
export const MainScreen = ({ navigation }) => {
/**
* Redux selectors and dispatch
*/
const {
loading = true,
vehicles = [],
loadMore = false
} = useSelector((state) => state.vehicles);
/**
* Initial effect, fetches all vehicles
*/
useEffect(() => {
dispatch(
vehiclesActions.vehicleGet({
page: 1,
})
);
}, []);
const renderCard = () => {
return (<View><Text>Test</Text></View>)
}
if (loading) {
return (<View><Text>App Loading </Text></View>
}
return (
<View style={styles.wrapper}>
<View
style={
Platform.OS === 'ios' ? { marginTop: 30 } : { marginTop: 0, flex: 1 }
}
>
{!loading && (
<View style={Platform.OS === 'ios' ? {} : { flex: 1 }}>
<FlatList
testID={'flat-list'}
data={vehicles}
renderItem={renderCard}
/>
</View>
)}
</View>
</View>
);
};
MainScreen.propTypes = {
navigation: PropTypes.object
};
export default MainScreen;
My Vehicles Saga:
const api = {
vehicles: {
getVehicles: (page) => {
return api.get(`/vehicles/list?page=${page}`, {});
},
}
function* getVehicles(action) {
try {
const { page } = action.payload;
const { data } = yield call(api.vehicles.getVehicles, page);
yield put({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload: data });
} catch (err) {
yield call(errorHandler, err);
yield put({ type: vehiclesConstants.VEHICLE_GET_FAIL });
}
}
export function* vehiclesSaga() {
yield takeLatest(vehiclesConstants.VEHICLE_GET_REQUEST, getVehicles);
}
Actions:
export const vehiclesActions = {
vehicleGet: payload => ({ type: vehiclesConstants.VEHICLE_GET_REQUEST, payload }),
vehicleGetSuccess: payload => ({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload }),
vehicleGetFail: error => ({ type: vehiclesConstants.VEHICLE_GET_FAIL, error }),
}
Reducer
import { vehiclesConstants } from "./constants";
const initialState = {
vehicles: [],
loading: true,
};
export const vehiclesReducer = (state = initialState, action) => {
switch (action.type) {
case vehiclesConstants.VEHICLE_GET_REQUEST:
return {
...state,
loading: true,
};
case vehiclesConstants.VEHICLE_GET_SUCCESS:
return {
...state,
loading: false,
vehicles: action.payload,
};
}
}
My Test File
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
Like I see above I cannot find element with testId flat-list, since component AppScreen it always show loading text, is there any way I could mock that API call and make this to work ?
Jest allows you to mock any module using jest.mock.
You have to write an alternative to axios.get like this
const vehiclesData = [
// ... put default data here
]
const delay = (ms, value) =>
new Promise(res => setTimeout(() => res(value), ms))
const mockAxiosGet = async (path) => {
let result = null
if (path.includes('vehicles/list') {
const query = new URLSearchParams(path.replace(/^[^?]+\?/, ''))
const page = + query.get('page')
const pageSize = 10
const offset = (page - 1)*pageSize
result = vehiclesData.slice(offset, offset + pageSize)
}
return delay(
// simulate 100-500ms latency
Math.floor(100 + Math.random()*400),
{ data: result }
)
}
Then modify the test file as
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import axios from 'axios'
// enable jest mock on 'axios' module
jest.mock('axios')
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
before(() => {
// mock axios implementation
axios.get.mockImplementation(mockAxiosGet)
})
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
For your use case, read more at Mocking Implementations

Categories

Resources