I have an issue with mocking / testing interactions with socket.io-client in my react application. I have tried methods such as mocking, libraries such as socket.io-mock and articles such as medium article. However, it did not came close to even emitting an event for my web application to consume.
socket_config.ts
import io from 'socket.io-client';
import config from '../utils/config';
const ws = io(config.socket_endpoint);
export default ws;
ScanPaperDocument.tsx
import React, { useState, useEffect } from 'react';
import ws from '../../socket';
const ScanPaperDocument: React.FC = () => {
const [loading, setLoading] = useState<boolean>(true);
const [scanning, setScanning] = useState<boolean>(true);
useEffect(() => {
ws.on('text_socket', (data: any) => {
// Process the data, do some state change
// Assuming that data is alright
setLoading(false);
setScanning(true);
});
ws.on('image_socket', (data: any) => {
// Process the data, do some state change
});
}, []);
return (
<>
{loading && scanning && <p>Scanning. Please wait.</p>}
{!loading && scanning && <p>Here is your data.</p>}
{/* And many other loading & scanning combinations */}
</>
);
};
export default ScanPaperDocument;
Here is a trashy mock implementation that I attempted to do
ScanPaperDocument.test.tsx
import React from 'react';
import ScanPaperDocument from './ScanPaperDocument';
import { act, render, waitFor } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
jest.mock('socket.io-client', () => {
return jest.fn().mockImplementation(() => {
return {
on: jest.fn(),
emit: jest.fn(),
};
});
});
describe(() => {
it('is able to consume the incoming websocket event', () => {
const { getByText } = render(<ScanPaperDocument />);
// Not sure how do I 'emit' an event from the 'server' for my frontend to consume
expect(getByText(/here is your data/i)).toBeTruthy();
})
})
As for the medium article that I have attempted, I received the following error by simply calling serverSocket.emit('text_socket', {data: ''})
TypeError: Cannot read property 'forEach' of undefined
2 |
3 | const emit = (event, ...args) => {
> 4 | EVENTS[event].forEach((func) => func(...args));
| ^
5 | };
6 |
7 | const socket = {
at Object.emit (src/socket/mock-socket.io-client.js:4:17)
Related
I have an issue with react won't update ui, I am trying to update the number of people connected in the same room there's no issue in the backend, my issue is on the front because I saw that the events are reaching the client through chrome dev tools.
as shown below the event is indeed reaching the client.
import React, { useContext, useEffect, useState, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { SocketContext } from '../context/socket';
type Props = {};
export default function Game({}: Props) {
const socket = useContext(SocketContext);
const { id } = useParams();
const [playerCount, setPlayerCount] = useState(0);
const updatePlayerCount = (...args: string[]) => {
console.log(args);
setPlayerCount(args.length);
};
useEffect(() => {
socket.emit('join_room', id);
socket.on('game_result', gameHandler);
socket.on('player_count', updatePlayerCount);
return () => {
socket.off('game_result');
socket.off('player_count');
};
}, []);
const gameHandler = (...args: any) => {
console.log(args);
};
return (
<div>
Game {id}
<div>{playerCount}</div>
</div>
);
}
checking the console I do see my console.log firing...
however the first join event does work cause I don't see 0 I see 1 instead. playerCount = 0 initially
You can try defining both your event handlers inside your useEffect() like this
useEffect(() => {
const updatePlayerCount = (...args: string[]) => {
console.log(args);
setPlayerCount(args.length);
};
const gameHandler = (...args: any) => {
console.log(args);
};
socket.emit('join_room', id);
socket.on('game_result', gameHandler);
socket.on('player_count', updatePlayerCount);
return () => {
socket.off('game_result');
socket.off('player_count');
};
}, [id]);
apparently I didn't pay attention that the data emitted was an array embedded inside an array
a simple fix was
setPlayerCount(args[0].length);
I am trying to test rendered data on my page that is coming from an api. The api generates one of three random objects, in this case they are weather and contain the following types:
forcast: string;
max: number;
min: number;
description: string;
I'm new to testing and typescript and am wondering how I can find say the forecast on a page if it is going to be randomised each call.
here is my code thus far:
Weather.tsx
import axios from 'axios';
import { useEffect, useState } from 'react';
import { IWeather } from '../interfaces/IWeather';
const Weather = () => {
const [minTemp, setMinTemp] = useState<IWeather[]>([]);
const [maxTemp, setMaxTemp] = useState<IWeather[]>([]);
const [forcast, setForcast] = useState([]);
const fetchWeatherData = async () => {
const response = await axios.get('http://mock-api-call/weather/get-weather');
setMinTemp(response.data.result.weather.min);
setMaxTemp(response.data.result.weather.max);
setForcast(response.data.result.weather.forcast);
};
useEffect(() => {
fetchWeatherData();
}, []);
return (
<div className="container">
<p className="forcast">{forcast}</p>
<p className="temperature">Temperature {`${minTemp} to ${maxTemp}`}</p>
</div>
);
};
export default Weather;
And this is a basic version of say testing for all the forecast elements. I have tried adding || and searching for multiple strings but unfortunately is not working
Weather.test.tsx
import { render, screen } from '#testing-library/react';
import Weather from '../Weather';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('http://mock-api-call/weather/get-weather', (req, res, ctx) => {
return res(ctx.json({}));
}),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('Weather', () => {
test('should render forcast for day', async () => {
render(<Weather />);
const forecast = await screen.findAllByText(/sunny/i || /overcast/i || /snowing/i);
expect(forecast).toBeInTheDocument();
});
});
I am also receiving this TypeError
TypeError: Cannot read properties of undefined (reading 'weather')
10 | const fetchWeatherData = async () => {
11 | const response = await axios.get('http://mock-api-call/weather/get-weather');
> 12 | setMinTemp(response.data.result.weather.min);
| ^
13 | setMaxTemp(response.data.result.weather.max);
14 | setForcast(response.data.result.weather.forcast);
15 | };
For clarity the data is rendering onto the page.
I'm having the following problem with my react + nextJS project...
The component is something like this:
import React, { FC, useCallback, useEffect, useState } from 'react';
import InputMask, { Props } from 'react-input-mask';
import {
getPayersDetails,
PayerCompany,
PayerContact,
PayerDocuments,
} from 'services';
import { Formik } from 'formik';
import { Field, Loading, Page, Tooltip } from 'components';
import { Button, IconButton, Typography } from '#mui/material';
import { TextField, TextFieldProps } from '#material-ui/core';
import { SvgSelfCheckout } from 'images';
import {
FaEdit,
FaFileInvoiceDollar,
FaUserCheck,
FaUserAltSlash,
} from 'react-icons/fa';
import theme from 'styles/theme';
import * as S from './styles';
import { useRouter } from 'next/router';
import { PAYER_HOME } from 'src/routes';
const PayersDetails: FC = () => {
const [payerCompany, setPayerCompany] = useState<PayerCompany[]>([]);
const [payerContact, setPayerContact] = useState<PayerContact[]>([]);
const [payerDocument, setPayerDocument] = useState<PayerDocuments[]>([]);
const [loading, setLoading] = useState(true);
const [isActivePayer, setIsActivePayer] = useState(false);
const router = useRouter();
const getPayerDetails = useCallback(async (payerId: number) => {
setLoading(true);
const payerDetails = await getPayersDetails(payerId);
setPayerCompany(payerDetails.payerCompany);
setPayerDocument(payerDetails.payerDocument);
setPayerContact(payerDetails.payerContact);
setLoading(false);
}, []);
useEffect(() => {
if (!router.isReady) {
return;
}
const payerId = router.query.payerId as string;
try {
const safePayerId = parseInt(payerId);
getPayerDetails(safePayerId);
} catch (e) {
router.push(PAYER_HOME);
return;
}
}, [getPayerDetails, router]);
const contact = payerContact.length > 0 ? payerContact[0] : null;
const mobilePhone = payerContact
.filter(contact => contact.contactType.contactTypeName === 'mobile')
.map(contact => contact.value)[0];
return (
<Page
title="Detalhes do pagador"
pageTitle="Detalhes do pagador"
pageSubtitle="Dados pessoais"
pageSubitleColor={`${theme.palette.primary.light}`}
>
{loading ? (
<Loading show={loading} />
) : (
<Formik
enableReinitialize={true}
initialValues={{
name: contact?.contactName,
email: contact?.value,
}}
onSubmit={() => console.log('onSumit')}
>
....
)}
</Formik>
)}
</Page>
);
};
export default PayersDetails;
And I'm trying to test it with the following code:
import { render, screen, fireEvent } from '#testing-library/react';
import { getCompany } from 'services/companies';
import { getPayersDetails } from 'services/payers';
import PayersImport from '.';
import { useRouter } from 'next/router';
import userEvent from '#testing-library/user-event';
jest.mock('services/payers', () => ({
__esModule: true, // this property makes it work
default: 'mockedDefaultExport',
getPayersDetails: jest.fn(),
}));
jest.mock('next/router', () => ({
useRouter: jest.fn().mockImplementation(() => ({
route: '/',
pathname: '',
query: '',
asPath: '',
})),
}));
describe('payers details layout', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
describe('when rendering', () => {
let getPayersDetailsMock;
beforeEach(() => {
getPayersDetailsMock = {
...
};
getPayersDetails.mockResolvedValue(getPayersDetailsMock);
useRouter.mockImplementation(() => ({
route: '/',
pathname: '',
isReady: true,
query: { payerId: 1 },
asPath: '',
}));
render(<PayersImport />);
});
it('Calls details api with the correct id', () => {
expect(getPayersDetails).toHaveBeenCalledWith(1);
});
});
});
The issue is:
The component load ok when we go to a browser, but when I run it on jest I get the following error:
console.error
Warning: An update to PayersDetails inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at PayersDetails (/home/thiago/finnet/repos/welcome/apps/lunapay-front/src/layouts/Payers/PayersDetails/index.tsx:29:43)
43 | setPayerContact(payerDetails.payerContact);
44 |
> 45 | setLoading(false);
| ^
46 | }, []);
47 |
48 | useEffect(() => {
I get it is only a warning and it wouldn't be a problem for me, but the issue is that it goes into a infinite loop trying to render again.
What am I doing wrong??
I solved the issue, but it was not the best solution.
Basically the render was being triggered by the <Loading /> component being rendered again.
What I did was just adding a new property telling me if it was already fetched and canceling the loop.
const [payerDetailsResponse, setPayerDetailsResponse] =
useState<PayerDetailsResponse | null>(null);
const [payerCompany, setPayerCompany] = useState<PayerCompany[]>([]);
const [payerContact, setPayerContact] = useState<PayerContact[]>([]);
const [payerDocument, setPayerDocument] = useState<PayerDocuments[]>([]);
const [loading, setLoading] = useState(true);
const [isActivePayer, setIsActivePayer] = useState(false);
const router = useRouter();
const getPayerDetails = useCallback(async (payerId: number) => {
setLoading(true);
const payerDetails = await getPayersDetails(payerId);
setPayerDetailsResponse(payerDetails);
setPayerCompany(payerDetails.payerCompany);
setPayerDocument(payerDetails.payerDocument);
setPayerContact(payerDetails.payerContact);
setLoading(false);
}, []);
useEffect(() => {
if (payerDetailsResponse) {
return;
}
if (!router.isReady) {
return;
}
const payerId = router.query.payerId as string;
try {
const safePayerId = parseInt(payerId);
getPayerDetails(safePayerId);
} catch (e) {
router.push(PAYER_HOME);
return;
}
}, [router, getPayerDetails]);
Sorry not to be able to help more, but that was a solution for my problem :)
Sometimes jest fall into an infinite loop with useEffect, if that is your case, you can make a mock of it
import React from 'react'
const mockUseEffect = jest.fn()
jest.spyOn(React, 'useEffect').mockImplementation(mockUseEffect)
and try with that and/or adapt to your needs
I have the following component, where I create ref on nav to close the menu on click outside of nav:
import { useState, useEffect, useRef, } from 'react';
const Header = () => {
const [menuOpen, setMenuOpen] = useState(false);
const navRef = useRef(null);
const hideMenu = () => setMenuOpen(false);
const handleClick = event => {
if (menuOpen && !navRef.current.contains(event.target)) {
hideMenu();
}
};
useEffect(() => {
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
});
return (
<header className="header">
<nav className="header-nav" ref={navRef}>
...
</nav>
</header>
);
};
export default Header;
And this is the unit test:
import React from 'react';
import '#testing-library/jest-dom';
import { cleanup, fireEvent } from '#testing-library/react';
import renderer from 'react-test-renderer';
import Header from './Header';
const { act } = renderer;
afterEach(cleanup);
describe('Header', () => {
test('should open and close mobile menu', () => {
const headerComponent = renderer.create(<Header />);
const headerRoot = headerComponent.root;
const navContainer = headerRoot.findByType('nav');
act(() => {
// open menu
navContainer.children[0].props.onClick(new MouseEvent('click'));
});
act(() => {
// click on document
fireEvent(document, new MouseEvent('click'));
});
headerTree = headerComponent.toJSON();
expect(headerTree).toMatchSnapshot();
});
});
The test run results in the following error:
TypeError: Cannot read property 'contains' of null
26 |
27 | const handleClick = (event) => {
> 28 | if (menuOpen && !navRef.current.contains(event.target)) {
| ^
29 | hideMenu();
30 | }
31 | };
I have tried to mock ref.currrent but it's still null:
jest.spyOn(React, 'useRef').mockReturnValue({
current: navContainer,
});
Please advice how I can organize the test to be able to test this
P.S. I have found this answer but it doesn't suit me as I don't wanna pass ref as a prop: https://stackoverflow.com/a/59207195/3132457
In create-react-app, I'm trying to simple test with jest but I'm getting this error : TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined.
A part of my component AppBarUser.js
/...
const AppBarUser = () => {
const classes = useStyles();
const [, formDispatch] = useContext(FormContext);
const [t] = useContext(TranslationContext);
const [openDrawer, setDrawer] = useState(false);
const [userInfos, setData] = useState({});
useEffect(() => {
const fetchData = async () => {
try {
const result = await axiosGET(`${domain}/users/profile?id_user=${decode().id}`);
setData(result[0]);
formDispatch({ type: 'SET_SQUADMEMBERS', squadMembers: [{ value: result[0].id, label: result[0].label, isFixed: true }] })
} catch (error) {
console.log(error)
}
};
fetchData();
}, []);
/...
export default AppBarUser;
initialized like this in App.js:
import TranslationContext from './contexts/translationContext';
import FormContext from './contexts/formContext';
import formReducer, { formInitialState } from './reducers/formReducer';
/...
const App = () => {
const [formState, formDispatch] = useReducer(formReducer, formInitialState);
const [t, setLocale, locale] = useTranslation();
return(
<TranslationContext.Provider value={[t, setLocale, locale]} >
<FormContext.Provider value={[formState, formDispatch]} >
<HomeComponent />
</FormContext.Provider>
</TranslationContext.Provider>
)
/...
}
/...
App
|_ HomeComponent
|_ AppBarComponent
|_ AppBarUser
AppBarUser.test.js
import React from 'react';
import { shallow } from 'enzyme';
import AppBarUser from './AppBarUser';
it('AppBarUser should render properly', () => {
shallow(<AppBarUser />)
});
Here is the result :
TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined
19 |
20 |
> 21 | const AppBarUser = () => {
| ^
22 |
23 | const classes = useStyles();
24 |
at _iterableToArrayLimit (node_modules/babel-preset-react-app/node_modules/#babel/runtime/helpers/iterableToArrayLimit.js:8:22)
at _slicedToArray (node_modules/babel-preset-react-app/node_modules/#babel/runtime/helpers/slicedToArray.js:8:33)
at AppBarUser (src/components/AppBarUser.jsx:21:26)
at ReactShallowRenderer.render (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:758:32)
at render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:636:55)
at fn (node_modules/enzyme-adapter-utils/src/Utils.js:99:18)
at Object.render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:636:20)
at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:265:22)
at shallow (node_modules/enzyme/build/shallow.js:21:10)
at Object.<anonymous>.it (src/components/AppBarUser.test.js:6:5)
When I remove in AppBarUser.js const [, formDispatch] = useContext(FormContext); const [t] = useContext(TranslationContext); and all associated variables, the test passes.
I'm a beginner with testing in jest, please could someone help me ?
Try wrapping AppBarUser in the context providers it is expecting to receive contexts from. The hooks are receiving undefined values for the context.
import React from 'react';
import { shallow } from 'enzyme';
import AppBarUser from './AppBarUser';
import TranslationContext from './contexts/translationContext';
import FormContext from './contexts/formContext';
it('AppBarUser should render properly', () => {
shallow(
<TranslationContext.Provider value={[/* Whatever context mocks needed */]} >
<FormContext.Provider value={[/* Whatever context mocks needed */]} >
<AppBarUser />
</FormContext.Provider>
</TranslationContext.Provider>
);
});
Depending on the test you may also need to do a full mount instead of a shallow one.