Fakultas Ilmu Komputer UI

Commit 2fd70922 authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Refactor custom test provider and improve coverage

parent ce76dfc4
import React from 'react';
import { withAuthRender, fireEvent, waitFor } from 'utils/testing';
import { render, fireEvent, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import LogoutButton from '.';
import { UserContext } from 'provider';
describe('LogoutButton', () => {
const initialUser = {
id: null,
email: '',
name: '',
role: null,
};
const userContextMock = {
user: initialUser,
firstAuthenticated: false,
isAuthenticated: true,
isUnpaidClient: false,
isPaidClient: false,
isNutritionist: false,
isAdmin: false,
isLoading: false,
isFirstLoading: false,
signup: jest.fn(),
login: jest.fn(),
loginWithGoogle: jest.fn(),
isUnpaidClient: true,
logout: jest.fn(),
};
it('renders nothing when user is not authenticated', () => {
render(<LogoutButton />, ROUTES.initial);
});
it('renders correctly', () => {
withAuthRender(<LogoutButton />, ROUTES.checkout);
render(<LogoutButton />, ROUTES.checkout, { userContext: userContextMock });
});
it('calls logout and redirects to initial page when clicked', async () => {
const { getByTestId } = withAuthRender(
<UserContext.Provider value={userContextMock}>
<LogoutButton />
</UserContext.Provider>,
const { getByTestId, queryByText } = render(
<LogoutButton />,
ROUTES.checkout,
{
userContext: userContextMock,
},
);
const logoutButton = getByTestId('logoutButton');
expect(logoutButton).toBeTruthy();
await waitFor(() => fireEvent.press(logoutButton));
expect(userContextMock.logout).toBeCalledTimes(1);
expect(queryByText(/konsultasi sekarang/i)).toBeTruthy();
});
});
......@@ -2,17 +2,28 @@ import React, { FC, useContext } from 'react';
import { UserContext } from 'provider';
import { Button, Icon } from 'react-native-elements';
import { StyleSheet, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import * as ROUTES from 'constants/routes';
const LogoutButton: FC<{
tintColor?: string | undefined;
tintColor?: string;
}> = () => {
const { logout, isAuthenticated } = useContext(UserContext);
const navigation = useNavigation();
const handlePress = async () => {
await logout();
navigation.reset({
index: 0,
routes: [{ name: ROUTES.initial }],
});
};
return isAuthenticated ? (
<Button
icon={<Icon name="logout" type="material" />}
buttonStyle={styles.button}
onPress={logout}
onPress={handlePress}
testID="logoutButton"
/>
) : (
......
......@@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { Text } from 'react-native-elements';
import { typographyStyles } from 'styles';
const BodyMedium: FC<{ text: String }> = ({ text }) => (
const BodyMedium: FC<{ text: string }> = ({ text }) => (
<Text style={typographyStyles.bodyMedium}>{text}</Text>
);
......
......@@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { Text } from 'react-native-elements';
import { typographyStyles } from 'styles';
const HeadingLarge: FC<{ text: String }> = ({ text }) => (
const HeadingLarge: FC<{ text: string }> = ({ text }) => (
<Text style={typographyStyles.headingLarge}>{text}</Text>
);
......
......@@ -87,3 +87,32 @@ export const privateNavigation: NavRoute[] = [
header: 'Profile',
},
];
export const testNavigation: NavRoute[] = [
...navigation,
...privateNavigation,
{
name: ROUTES.initial,
component: InitialPage,
},
{
name: ROUTES.registration,
component: ManualRegistrationPage,
header: 'Registrasi',
},
{
name: ROUTES.login,
component: LoginPage,
header: 'Login',
},
{
name: ROUTES.checkout,
component: Checkout,
header: 'Checkout',
},
{
name: ROUTES.paymentResult,
component: PaymentResult,
header: 'Pembayaran',
},
];
import { createContext, useCallback, useEffect, useState } from 'react';
import { createContext, useCallback, useState } from 'react';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { Toast } from 'components/core';
......@@ -19,12 +19,12 @@ import {
LoginResponse,
UserRole,
} from 'services/auth/models';
import { set401Callback, setAuthHeader, resetAuthHeader } from 'services/api';
import { setAuthHeader, resetAuthHeader } from 'services/api';
import { iUserContext } from './types';
import { TransactionStatus } from 'services/payment/models';
const initialUser = {
export const initialUser = {
id: null,
email: '',
name: '',
......@@ -40,6 +40,7 @@ export const UserContext = createContext<iUserContext>({
isAdmin: false,
isLoading: false,
isFirstLoading: false,
getUser: () => Promise.reject(),
signup: () => Promise.reject(),
login: () => Promise.reject(),
loginWithGoogle: () => Promise.reject(),
......@@ -156,17 +157,6 @@ export const useUserContext = (): iUserContext => {
setIsLoading(false);
};
useEffect(() => {
// TODO: save to .env
GoogleSignin.configure({
webClientId:
'813112248680-ulv0amtocut652j31qbpvubtclbd2c7o.apps.googleusercontent.com',
});
getUser();
set401Callback(logout);
}, [getUser, logout]);
return {
user,
isAuthenticated: user.id !== null,
......@@ -178,6 +168,7 @@ export const useUserContext = (): iUserContext => {
isAdmin: user.role === UserRole.ADMIN,
isLoading,
isFirstLoading,
getUser,
signup,
login,
loginWithGoogle,
......
......@@ -16,6 +16,7 @@ export interface iUserContext {
isAdmin: boolean;
isLoading: boolean;
isFirstLoading: boolean;
getUser: () => Promise<void>;
signup: (
data: RegistrationRequest,
) => ApiResponse<LoginResponse | LinkUserDataResponse>;
......
import React, { FC } from 'react';
import React, { FC, useEffect } from 'react';
import { UserContext, useUserContext } from './UserContext';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { set401Callback } from 'services/api';
const ContextProvider: FC = ({ children }) => {
const user = useUserContext();
const { getUser, logout, ...user } = useUserContext();
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
useEffect(() => {
// TODO: save to .env
GoogleSignin.configure({
webClientId:
'813112248680-ulv0amtocut652j31qbpvubtclbd2c7o.apps.googleusercontent.com',
});
getUser();
set401Callback(logout);
}, [getUser, logout]);
return (
<UserContext.Provider value={{ getUser, logout, ...user }}>
{children}
</UserContext.Provider>
);
};
export default ContextProvider;
......
......@@ -2,15 +2,9 @@ import React from 'react';
import { render, fireEvent, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import axios from 'axios';
import CACHE_KEYS from 'constants/cacheKeys';
import { setCache } from 'utils/cache';
import Login from '.';
import {
authResponse,
invalidLoginValues,
validLoginValues,
} from '__mocks__/auth';
import { authResponse, validLoginValues } from '__mocks__/auth';
import { textField } from './schema';
jest.mock('react-native-toast-message');
......@@ -18,18 +12,7 @@ jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('Login page', () => {
it('shows dietela cover loader when is loading', async () => {
const { queryByText } = render(<Login />, ROUTES.login);
await waitFor(() =>
expect(queryByText(/Lanjut dengan Google/i)).toBeFalsy(),
);
});
it('renders correctly if client has filled questionnaire and cart', async () => {
await setCache(CACHE_KEYS.cartId, 1);
await setCache(CACHE_KEYS.dietProfileId, 1);
const { queryByText } = render(<Login />, ROUTES.login);
await waitFor(() =>
......@@ -45,15 +28,11 @@ describe('Login page', () => {
});
mockAxios.request.mockImplementationOnce(loginApi);
const { getByPlaceholderText, queryByText, getByTestId } = render(
const { getByPlaceholderText, getByTestId } = render(
<Login />,
ROUTES.login,
);
await waitFor(() =>
expect(queryByText(/Lanjut dengan Google/i)).toBeTruthy(),
);
textField.map(({ name, placeholder }) => {
const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, validLoginValues[name]);
......@@ -63,7 +42,7 @@ describe('Login page', () => {
await waitFor(() => fireEvent.press(loginButton));
});
it('fails when field is invalid and submit success', async () => {
it('fails when field is valid and submit fails', async () => {
const loginApi = () =>
Promise.reject({
status: 400,
......@@ -78,13 +57,9 @@ describe('Login page', () => {
ROUTES.login,
);
await waitFor(() =>
expect(queryByText(/Lanjut dengan Google/i)).toBeTruthy(),
);
textField.map(({ name, placeholder }) => {
const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, invalidLoginValues[name]);
fireEvent.changeText(formField, validLoginValues[name]);
});
const loginButton = getByTestId('loginButton');
......
import React, { FC, useContext, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { UserContext } from 'provider';
import { useAuthEffect, useForm } from 'hooks';
import { useForm } from 'hooks';
import { GoogleLoginButton } from '../components';
import { BigButton, Toast, Loader } from 'components/core';
import { BigButton, Toast } from 'components/core';
import { fieldValidation, initialValues, textField } from './schema';
import { generateValidationSchema } from 'utils/form';
......@@ -45,11 +45,6 @@ const Login: FC = () => {
},
});
const isProcessing = useAuthEffect();
if (isProcessing) {
return <Loader />;
}
return (
<View style={layoutStyles}>
{textField.map(({ name, label, required, placeholder }, i) => (
......
......@@ -4,6 +4,8 @@ import * as ROUTES from 'constants/routes';
import axios from 'axios';
import ManualRegistrationPage from '.';
import CACHE_KEYS from 'constants/cacheKeys';
import { setCache } from 'utils/cache';
import { textField } from './schema';
import {
authResponse,
......@@ -16,7 +18,19 @@ jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('ManualRegistrationPage', () => {
it('renders correctly', () => {
it('shows dietela cover loader when is loading', async () => {
const { queryByText } = render(
<ManualRegistrationPage />,
ROUTES.registration,
);
await waitFor(() => expect(queryByText(/daftarkan akun/i)).toBeFalsy());
});
it('renders correctly', async () => {
await setCache(CACHE_KEYS.cartId, 1);
await setCache(CACHE_KEYS.dietProfileId, 1);
render(<ManualRegistrationPage />, ROUTES.registration);
});
......@@ -28,11 +42,13 @@ describe('ManualRegistrationPage', () => {
});
mockAxios.request.mockImplementationOnce(signupApi);
const { getByPlaceholderText, getByTestId } = render(
const { getByPlaceholderText, queryByText, getByTestId } = render(
<ManualRegistrationPage />,
ROUTES.registration,
);
await waitFor(() => expect(queryByText(/daftarkan akun/i)).toBeTruthy());
textField.map(({ name, placeholder }) => {
const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, validRegistrationValues[name]);
......@@ -57,6 +73,8 @@ describe('ManualRegistrationPage', () => {
ROUTES.registration,
);
await waitFor(() => expect(queryByText(/daftarkan akun/i)).toBeTruthy());
textField.map(({ name, placeholder }) => {
const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, validRegistrationValues[name]);
......@@ -91,6 +109,8 @@ describe('ManualRegistrationPage', () => {
ROUTES.registration,
);
await waitFor(() => expect(queryByText(/daftarkan akun/i)).toBeTruthy());
textField.map(({ name, placeholder }) => {
const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, invalidRegistrationValues[name]);
......@@ -108,6 +128,9 @@ describe('ManualRegistrationPage', () => {
<ManualRegistrationPage />,
ROUTES.registration,
);
await waitFor(() => expect(queryByText(/daftarkan akun/i)).toBeTruthy());
expect(queryByText(/Login disini/i)).toBeTruthy();
await waitFor(() => fireEvent.press(getByText(/Login disini/i)));
......
import React, { FC, useContext } from 'react';
import { useForm } from 'hooks';
import { useForm, useAuthEffect } from 'hooks';
import { ScrollView } from 'react-native-gesture-handler';
import { useNavigation } from '@react-navigation/core';
import { BigButton, Link, Toast } from 'components/core';
import { BigButton, Link, Toast, Loader } from 'components/core';
import { Section } from 'components/layout';
import { TextField } from 'components/form';
import { GoogleLoginButton } from '../components';
......@@ -50,6 +50,11 @@ const ManualRegistrationPage: FC = () => {
const signupWithGoogle = () => loginWithGoogle(false);
const isProcessing = useAuthEffect();
if (isProcessing) {
return <Loader />;
}
return (
<ScrollView contentContainerStyle={layoutStyles}>
{textField.map((fieldProps, i) => (
......
import React from 'react';
import { withAuthRender, waitFor, fireEvent } from 'utils/testing';
import { render, waitFor, fireEvent } from 'utils/testing';
import axios from 'axios';
import Checkout from '.';
......@@ -9,6 +9,7 @@ import CACHE_KEYS from 'constants/cacheKeys';
import { DietelaProgram } from 'services/dietelaQuiz/quizResult';
import { setCache } from 'utils/cache';
import { mockProgramRecommendations } from '__mocks__/quizResult';
import { Linking } from 'react-native';
jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
......@@ -37,14 +38,18 @@ describe('Checkout', () => {
},
});
const userContextMock = {
isAuthenticated: true,
isUnpaidClient: true,
};
it('redirects to program detail page when user clicks Baca selengkapnya button for program', async () => {
await setCache(CACHE_KEYS.cartId, 1);
mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getAllByText, getByText } = withAuthRender(
<Checkout />,
ROUTES.checkout,
);
const { getAllByText, getByText } = render(<Checkout />, ROUTES.checkout, {
userContext: userContextMock,
});
await waitFor(() => expect(mockAxios.request).toBeCalled());
const chosenProgram = getByText(/One Week Trial/i);
......@@ -61,10 +66,9 @@ describe('Checkout', () => {
it('redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist', async () => {
mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getAllByText, getByText } = withAuthRender(
<Checkout />,
ROUTES.checkout,
);
const { getAllByText, getByText } = render(<Checkout />, ROUTES.checkout, {
userContext: userContextMock,
});
await waitFor(() => expect(mockAxios.request).toBeCalled());
const chosenNutritionist = getByText(/Wendy/i);
......@@ -82,7 +86,9 @@ describe('Checkout', () => {
);
mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getByText } = withAuthRender(<Checkout />, ROUTES.checkout);
const { getByText } = render(<Checkout />, ROUTES.checkout, {
userContext: userContextMock,
});
await waitFor(() => expect(mockAxios.request).toBeCalled());
const changePlanButton = getByText(/ganti pilihan/i);
......@@ -93,6 +99,58 @@ describe('Checkout', () => {
expect(choosePlanPage).toBeTruthy();
});
it('call Linking open url when user clicks Bayar button and submit successful', async () => {
mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getByText } = render(<Checkout />, ROUTES.checkout, {
userContext: userContextMock,
});
await waitFor(() => expect(mockAxios.request).toBeCalled());
const payWithMidtransApi = () =>
Promise.resolve({
status: 200,
data: {
redirect_url: 'url',
},
});
mockAxios.request.mockImplementationOnce(payWithMidtransApi);
const spy = jest.spyOn(Linking, 'openURL');
const payButton = getByText(/bayar dengan midtrans/i);
expect(payButton).toBeTruthy();
await waitFor(() => fireEvent.press(payButton));
expect(spy).toHaveBeenCalled();
spy.mockReset();
});
it('does not call Linking open url when user clicks Bayar button but submit fails', async () => {
mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getByText } = render(<Checkout />, ROUTES.checkout, {
userContext: userContextMock,
});
await waitFor(() => expect(mockAxios.request).toBeCalled());
const payWithMidtransApi = () =>
Promise.reject({
status: 400,
response: {
data: 'error',
},
});
mockAxios.request.mockImplementationOnce(payWithMidtransApi);
const spy = jest.spyOn(Linking, 'openURL');
const payButton = getByText(/bayar dengan midtrans/i);
expect(payButton).toBeTruthy();
await waitFor(() => fireEvent.press(payButton));
expect(spy).not.toHaveBeenCalled();
spy.mockReset();
});
afterAll(() => {
jest.clearAllMocks();
});
......
......@@ -14,7 +14,7 @@ import { nutritionistDummy } from '__mocks__/cart';
jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('ChoosePlanPalingKeren', () => {
describe('ChoosePlan', () => {
const nutritionists = [nutritionistDummy];
const retrieveNutritionistsApi = () =>
Promise.resolve({
......@@ -32,7 +32,7 @@ describe('ChoosePlanPalingKeren', () => {
expect(defaultProgram).toBeTruthy();
});
it('redirects to cart page when user clicks Bayar button', async () => {
it('redirects to checkout page when authenticated user clicks Bayar button', async () => {
await setCache(
CACHE_KEYS.programRecommendations,
JSON.stringify(mockProgramRecommendations),
......@@ -52,6 +52,7 @@ describe('ChoosePlanPalingKeren', () => {
const { getByText, queryAllByText } = render(
<ChoosePlan />,
ROUTES.choosePlan,
{ userContext: { user: { id: 1 }, isAuthenticated: true } },
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
......@@ -77,6 +78,43 @@ describe('ChoosePlanPalingKeren', () => {
expect(checkoutPage).toBeTruthy();
});
it('redirects to registration page when non-authenticated user clicks Bayar button', async () => {
const createCartApi = () =>
Promise.resolve({