redux-toolkit
redux-toolkit copied to clipboard
Can't test my code with Mock Service Worker (typecannot read properties of null (reading 'url'))
I'm trying to test my react-native app, but the test always fails. The error is {status: 'FETCH_ERROR', error: 'typecannot read properties of null (reading 'url')'} (see in the code below). I imported whatwg-fetch at the very beginning of the jest.setup.js file and added it to the setupFilesAfterEnv array. What I'm doing wrong?
The test:
// Establish API mocking before all tests.
beforeAll(() =>
server.listen({
onUnhandledRequest: 'warn',
})
);
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished.
afterAll(() => server.close());
// ...
it('should continue on button press', () => {
const tree = renderWithProviders(<OTPModal {...props} />);
const pinInput = tree.root.findByType(PinInput);
act(() => {
pinInput.props.onPinChange('1234');
});
const button = tree.root.findByType(Button);
act(() => {
button.props.onPress();
});
expect(props.route.params.onSuccess).toHaveBeenCalled();
});
The component:
import React from 'react';
import { StyleSheet } from 'react-native';
import { useTranslation } from 'react-i18next';
import { RootStackScreenProps } from '@/Navigators/types';
import { AuthScreen } from '@/Components/Screen/AuthScreen';
import { AppScreen } from '@/Components/Screen/AppScreen';
import { PinInput } from '@/Components/Inputs/PinInput';
import { Typography } from '@/Components/Typography';
import { LinkQuestion } from '@/Components/LinkQuestion';
import { Button } from '@/Components/Buttons/Button';
import { Spacer } from '@/Components/Spacer';
import { styling } from '@/Theme/Styles/GlobalStyles';
import { COLORS } from '@/Theme/Colors';
import { scaling } from '@/Utils/scaling';
import { OtpType, ValidateOtpResponse } from '@/Types/CustomerService';
import { useOTP } from '@/Hooks/otp';
export type OtpModalProps = {
otpType: OtpType;
phoneNumber: string;
loginId: string;
isBeforeAuth: boolean;
onSuccess: (response: ValidateOtpResponse) => void;
};
export const OTPModal = ({ route }: RootStackScreenProps<'OTPModal'>) => {
const { otpType, phoneNumber, loginId, isBeforeAuth, onSuccess } = route.params;
const { t } = useTranslation();
const { otp, setOTP, handleContinue, resendCode, isTimerShowed, timeLeft } = useOTP({
otpType,
phoneNumber,
loginId,
onSuccess,
});
const renderBody = () => (
<>
<PinInput pin={otp} onPinChange={setOTP} autoFocus containerStyle={styles.container} />
{otp.length === 4 ? (
<Button title={t('globals.continue')} onPress={handleContinue} />
) : (
<Spacer useScaling height={68} />
)}
{isTimerShowed ? (
<Typography variant="regular" color={COLORS.NEUTRAL_500} style={styling.textCenter}>
{`${timeLeft}s`}
</Typography>
) : (
<LinkQuestion
textContent={t('otpVerification.linkQuestion.textContent')}
linkText={t('otpVerification.linkQuestion.linkText')}
onPress={resendCode}
/>
)}
</>
);
return isBeforeAuth ? (
<AuthScreen
header={{
title: t('otpVerification.title'),
text:
otpType === OtpType.PASSWORD_RESET
? t('otpVerification.textResetPassword')
: t('otpVerification.text', { phoneNumber }),
}}
>
{renderBody()}
</AuthScreen>
) : (
<AppScreen
header={{
title: t('otpVerification.title'),
}}
>
<Typography variant="regular" color={COLORS.NEUTRAL_500} style={styling.componentMargin}>
{t('otpVerification.text', { phoneNumber })}
</Typography>
{renderBody()}
</AppScreen>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: scaling.vs(42),
},
});
The useOTP hook:
import { useEffect, useState } from 'react';
import { GLOBALS } from '@/Constants/Globals';
import { OtpType, ValidateOtpResponse } from '@/Types/CustomerService';
import { DEVICE } from '@/Constants/Device';
import { useAppSelector } from './redux';
import { getInstitutionCD } from '@/Store/Profile/selectors';
import { useNavigation } from '@react-navigation/native';
import {
useInitiateOtpMutation,
useResendRegOtpMutation,
useValidateOtpMutation,
useResetPasswordInitiateMutation,
} from '@/Services/modules/customer';
type OtpConfig = {
otpType: OtpType;
phoneNumber: string;
loginId: string;
onSuccess: (response: ValidateOtpResponse) => void;
};
export const useOTP = ({ otpType, phoneNumber, loginId, onSuccess }: OtpConfig) => {
const institutionCD = useAppSelector(getInstitutionCD);
const navigation = useNavigation();
const [initiateOTP] = useInitiateOtpMutation();
const [validateOtp] = useValidateOtpMutation();
const [resendRegOtp] = useResendRegOtpMutation();
const [resetPasswordInitiate] = useResetPasswordInitiateMutation();
const [otp, setOTP] = useState('');
const [timeLeft, setTimeLeft] = useState(GLOBALS.OTP_TIMEOUT);
const [isTimerShowed, setIsTimerShowed] = useState(true);
useEffect(() => {
if (!timeLeft) {
setIsTimerShowed(false);
return;
}
const intervalId = setInterval(() => {
setTimeLeft(timeLeft - 1);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, [timeLeft]);
useEffect(() => {
// In these 2 cases OTP is generated on the screen before OTP
if (otpType !== OtpType.REGISTRATION && otpType !== OtpType.PASSWORD_RESET) {
initiateOTP({
deviceId: DEVICE.UUID,
institutionCD,
phoneNumber,
otpType,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resendCode = async () => {
setOTP('');
switch (otpType) {
case OtpType.REGISTRATION:
await resendRegOtp({ deviceId: DEVICE.UUID, institutionCD });
break;
case OtpType.PASSWORD_RESET:
await resetPasswordInitiate({ deviceId: DEVICE.UUID, institutionCD, userId: loginId });
break;
default:
await initiateOTP({
deviceId: DEVICE.UUID,
institutionCD,
phoneNumber,
otpType,
});
break;
}
setTimeLeft(GLOBALS.OTP_TIMEOUT);
setIsTimerShowed(true);
};
const handleContinue = () => {
validateOtp({
otpType,
otp,
loginId,
deviceId: DEVICE.UUID,
institutionCD,
})
.unwrap()
.then((response) => {
navigation.canGoBack() && navigation.goBack();
onSuccess?.(response);
})
.catch((e) => {
console.log(e); // >> 'FETCH_ERROR': 'typecannot read properties of null (reading 'url')'
});
};
return { otp, setOTP, handleContinue, resendCode, isTimerShowed, timeLeft };
};
The endpoint:
validateOtp: build.mutation<Types.ValidateOtpResponse, Types.ValidateOtpData>({
query: (body) => ({
url: ENDPOINTS.VALIDATE_OTP,
method: 'POST',
body,
}),
}),
The server:
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
The handler:
import { ENDPOINTS } from '@/Constants/Endpoints';
import { rest } from 'msw';
import { API_URL } from 'react-native-dotenv';
export const handlers = [
rest.post(`${API_URL}${ENDPOINTS.VALIDATE_OTP}`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
code: 0,
description: 'Operation successful',
data: '3aLQE6r3UO4VXTJCU10=',
})
);
}),
];