Fakultas Ilmu Komputer UI

Commit aa011df3 authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Revise choose plan logic and integrate with checkout API

parent 8757ce20
......@@ -26,7 +26,7 @@ test:
before_script:
- yarn install
script:
- yarn test
- yarn test --silent
artifacts:
paths:
- coverage
......
import { jest } from '@jest/globals';
import mockAsyncStorage from '@react-native-community/async-storage/jest/async-storage-mock';
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
jest.mock('@react-native-community/async-storage', () => mockAsyncStorage);
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);
import React from 'react';
import { act, create } from 'react-test-renderer';
import { render, fireEvent } from '@testing-library/react-native';
import BigButton from '.';
describe('BigButton', () => {
it('renders correctly when active', () => {
create(
render(
<BigButton title="ima button" onPress={() => console.log('cool')} />,
);
});
it('renders correctly when disabled', () => {
create(
render(
<BigButton
title="ima button"
onPress={() => console.log('cool')}
......@@ -21,7 +21,7 @@ describe('BigButton', () => {
});
it('renders correctly when given a testID', () => {
create(
render(
<BigButton
title="ima button"
onPress={() => console.log('cool')}
......@@ -33,17 +33,16 @@ describe('BigButton', () => {
it('executes onPress callback when button is pressed', async () => {
let isPressed = false;
const component = create(
const { getByText } = render(
<BigButton
title="ima button"
onPress={() => (isPressed = true)}
testID="button"
/>,
);
const instance = component.root;
const button = instance.findAllByType(BigButton)[0];
act(() => button.props.onPress());
const button = getByText(/ima button/i);
fireEvent.press(button);
expect(isPressed).toBeTruthy();
});
});
import React, { FC } from 'react';
import { Button } from 'react-native-elements';
import { styles } from './styles';
import { Props } from './types';
import { typographyStyles } from 'styles';
const BigButton: FC<Props> = ({ title, onPress, disabled }) => (
<Button
titleStyle={[typographyStyles.overlineBig, styles.titleStyle]}
disabledTitleStyle={styles.disabledStyle}
disabled={disabled}
buttonStyle={styles.containerStyle}
onPress={onPress}
title={title}
/>
);
export default BigButton;
import { StyleSheet } from 'react-native';
import { typographyStyles, colors } from 'styles';
import { colors } from 'styles';
export const styles = StyleSheet.create({
titleStyle: { ...typographyStyles.overlineBig, color: colors.textBlack },
titleStyle: { color: colors.textBlack },
containerStyle: {
padding: 12,
overflow: 'hidden',
......@@ -10,5 +10,4 @@ export const styles = StyleSheet.create({
backgroundColor: colors.buttonYellow,
},
disabledStyle: { color: 'white' },
disabledContainerStyle: { backgroundColor: 'pink' },
});
import React, { FC } from 'react';
import { View } from 'react-native';
import Button from 'react-native-button';
import { styles } from './styles';
import { Props } from './types';
const BigButton: FC<Props> = ({ title, onPress, disabled, testID }) => (
<View testID={testID}>
<Button
style={styles.titleStyle}
styleDisabled={styles.disabledStyle}
disabled={disabled}
containerStyle={styles.containerStyle}
disabledContainerStyle={styles.disabledContainerStyle}
onPress={onPress}>
{title}
</Button>
</View>
);
export default BigButton;
export { default as AutoImage } from './AutoImage';
export { default as BigButton } from './Button';
export { default as BigButton } from './BigButton';
export { default as CarouselPagination } from './CarouselPagination';
export { default as InfoCard } from './InfoCard';
export { default as Loader } from './Loader';
......
export default {
dietProfileId: 'DIET_PROFILE_ID',
selectedProgramId: 'PROGRAM_ID',
selectedNutritionistId: 'NUTRITIONIST_ID',
cartId: 'CART_ID',
};
import { DietelaProgram } from 'services/cart/models';
import {
ProgramRecommendations,
DietelaProgram,
} from 'services/dietelaQuiz/quizResult';
const prices = {
oneWeek: '239,900',
......@@ -9,7 +12,7 @@ const prices = {
export const dietPrograms = {
[DietelaProgram.TRIAL]: {
title: 'One Time Consulation (7 Hari)',
title: 'One Week Trial (7 Hari)',
price: prices.oneWeek,
},
[DietelaProgram.BALANCED_1]: {
......@@ -45,3 +48,8 @@ export const dietPrograms = {
price: prices.threeMonths,
},
};
export const defaultProgramRecommendations: ProgramRecommendations = {
priority_1: DietelaProgram.TRIAL,
priority_2: null,
};
......@@ -11,7 +11,7 @@ import {
BreakfastReponse,
PhysicalActivityResponse,
} from 'services/dietelaQuiz/quizResult';
import { DietelaProgram } from 'services/cart/models';
import { defaultProgramRecommendations } from 'constants/dietelaProgram';
const validFormValues: DietProfileRequest = {
name: 'Dietela',
......@@ -36,11 +36,6 @@ const validFormValues: DietProfileRequest = {
health_problem: [2, 3],
};
export const mockProgramRecommendations = {
priority_1: DietelaProgram.TRIAL,
priority_2: null,
};
export const mockQuizResult: DietProfileResponse = {
id: 1,
...validFormValues,
......@@ -81,6 +76,6 @@ export const mockQuizResult: DietProfileResponse = {
afternoon_snack: 254.51999999999998,
},
physical_activity_recommendation: PhysicalActivityResponse.LEVEL1_ACTIVITY,
program_recommendation: mockProgramRecommendations,
program_recommendation: defaultProgramRecommendations,
},
};
......@@ -4,9 +4,10 @@ import axios from 'axios';
import * as ROUTES from 'constants/routes';
import ChoosePlan from '.';
import { mockProgramRecommendations } from 'mocks/quizResult';
import { defaultProgramRecommendations } from 'constants/dietelaProgram';
jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('ChoosePlan', () => {
const nutritionists = [
......@@ -27,30 +28,103 @@ describe('ChoosePlan', () => {
data: nutritionists,
});
const mockAxios = axios as jest.Mocked<typeof axios>;
mockAxios.request.mockImplementation(retrieveNutritionistsApi);
it('provides default program recommendations if not passed', async () => {
mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi);
const { getAllByText } = render(<ChoosePlan />, ROUTES.choosePlan);
await waitFor(() => expect(mockAxios.request).toBeCalled());
const defaultProgram = getAllByText(/One Week Trial/i);
expect(defaultProgram).toBeTruthy();
});
it('redirects to cart page when user clicks Bayar button', async () => {
const { getByText } = render(<ChoosePlan />, ROUTES.choosePlan);
const createCartApi = () =>
Promise.resolve({
status: 200,
data: {
id: 1,
program: { id: 1 },
nutritionist: nutritionists[0],
},
});
mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi);
const { getByText, queryByText } = render(
<ChoosePlan />,
ROUTES.choosePlan,
defaultProgramRecommendations,
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
const selectProgramButton = getByText(/Pilih One Week Trial/i);
expect(selectProgramButton).toBeTruthy();
fireEvent.press(selectProgramButton);
const nextButton = getByText(/Lanjut/i);
expect(nextButton).toBeTruthy();
fireEvent.press(nextButton);
const selectNutritionistButton = getByText(/Pilih Wendy/i);
expect(selectNutritionistButton).toBeTruthy();
fireEvent.press(selectNutritionistButton);
mockAxios.request.mockImplementationOnce(createCartApi);
const payButton = getByText(/Bayar/i);
expect(payButton).toBeTruthy();
await waitFor(() => fireEvent.press(payButton));
const cartPage = getByText(/Coming Soon/i);
const cartPage = queryByText(/Coming Soon/i);
expect(cartPage).toBeTruthy();
});
it('does not redirect to cart page when submit fails', async () => {
const createCartApi = () =>
Promise.reject({
status: 400,
data: 'error',
});
mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi);
const { getByText, queryByText } = render(
<ChoosePlan />,
ROUTES.choosePlan,
defaultProgramRecommendations,
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
const selectProgramButton = getByText(/Pilih One Week Trial/i);
expect(selectProgramButton).toBeTruthy();
fireEvent.press(selectProgramButton);
const nextButton = getByText(/Lanjut/i);
expect(nextButton).toBeTruthy();
fireEvent.press(nextButton);
const selectNutritionistButton = getByText(/Pilih Wendy/i);
expect(selectNutritionistButton).toBeTruthy();
fireEvent.press(selectNutritionistButton);
mockAxios.request.mockImplementationOnce(createCartApi);
const payButton = getByText(/Bayar/i);
expect(payButton).toBeTruthy();
await waitFor(() => fireEvent.press(payButton));
const cartPage = queryByText(/Coming Soon/i);
expect(cartPage).toBeFalsy();
});
it('redirects to program detail page when user clicks Baca selengkapnya button for program', async () => {
mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi);
const { getAllByText, getByText } = render(
<ChoosePlan />,
ROUTES.choosePlan,
mockProgramRecommendations,
defaultProgramRecommendations,
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
......@@ -63,17 +137,26 @@ describe('ChoosePlan', () => {
});
it('redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist', async () => {
mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi);
const { getAllByText, getByText } = render(
<ChoosePlan />,
ROUTES.choosePlan,
mockProgramRecommendations,
defaultProgramRecommendations,
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
const selectProgramButton = getByText(/Pilih One Week Trial/i);
expect(selectProgramButton).toBeTruthy();
fireEvent.press(selectProgramButton);
const nextButton = getByText(/Lanjut/i);
expect(nextButton).toBeTruthy();
fireEvent.press(nextButton);
const nutritionistPage = getByText(/Nutrisionis/i);
expect(nutritionistPage).toBeTruthy();
const readMoreButton = getAllByText(/Baca selengkapnya/i)[0];
expect(readMoreButton).toBeTruthy();
fireEvent.press(readMoreButton);
......
......@@ -2,14 +2,18 @@ import React, { FC, useState } from 'react';
import { ScrollView } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';
import { WizardContainer, Loader } from 'components/core';
import { WizardContainer, Loader, Toast } from 'components/core';
import CACHE_KEYS from 'constants/cacheKeys';
import * as ROUTES from 'constants/routes';
import { useForm, useApi } from 'hooks';
import { dietPrograms } from 'constants/dietelaProgram';
import { useApi } from 'hooks';
import {
dietPrograms,
defaultProgramRecommendations,
} from 'constants/dietelaProgram';
import { retrieveNutritionistsApi } from 'services/nutritionists';
import { Nutritionist } from 'services/nutritionists/models';
import { ProgramRecommendations } from 'services/dietelaQuiz/quizResult';
import { createCartApi } from 'services/payment';
import { layoutStyles } from 'styles';
import { setCache } from 'utils/cache';
......@@ -19,18 +23,41 @@ import { initialValues, getRecommendedPrograms } from './schema';
const ChoosePlan: FC = () => {
const navigation = useNavigation();
const route = useRoute();
const programs = route.params as ProgramRecommendations;
const programs = (route.params ??
defaultProgramRecommendations) as ProgramRecommendations;
const [currentStep, setCurrentStep] = useState(1);
const [currentPage, setCurrentPage] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const [values, setValues] = useState(initialValues);
const { handleSubmit, getFormFieldProps } = useForm({
initialValues,
onSubmit: (values) => {
setCache(CACHE_KEYS.selectedProgramId, values.program);
setCache(CACHE_KEYS.selectedNutritionistId, values.nutritionist);
navigation.navigate(ROUTES.cart);
},
});
const handleSubmit = async () => {
setIsSubmitting(true);
const response = await createCartApi(values);
setIsSubmitting(false);
if (response.success) {
await setCache(CACHE_KEYS.cartId, response.data?.id);
navigation.navigate(ROUTES.cart, response.data);
} else {
Toast.show({
type: 'error',
text1: 'Gagal menyimpan data',
text2: 'Terjadi kesalahan pada sisi kami. Silakan coba lagi',
});
}
};
const handleChange = (name: string, value: string) => {
setValues({
...values,
[name]: value,
});
};
const isCurrentPageError = (): boolean => {
const fieldName = currentPage === 1 ? 'program' : 'nutritionist';
return values[fieldName] === null;
};
const { isLoading, data: nutritionists = [] } = useApi(
retrieveNutritionistsApi,
......@@ -41,8 +68,12 @@ const ChoosePlan: FC = () => {
}
return (
<WizardContainer
currentStep={currentStep}
setCurrentStep={setCurrentStep}
currentStep={currentPage}
setCurrentStep={setCurrentPage}
onFinish={handleSubmit}
finishButtonLabel="Bayar"
isNextDisabled={isCurrentPageError()}
isLoading={isSubmitting}
components={[
<ScrollView contentContainerStyle={layoutStyles}>
<PricingList
......@@ -53,7 +84,8 @@ const ChoosePlan: FC = () => {
onReadMore: () =>
navigation.navigate(ROUTES.programDetail, { id: code }),
}))}
{...getFormFieldProps('program')}
value={values.program}
onChange={(v) => handleChange('program', v)}
/>
</ScrollView>,
<ScrollView contentContainerStyle={layoutStyles}>
......@@ -62,18 +94,19 @@ const ChoosePlan: FC = () => {
items={nutritionists.map((nutritionist: Nutritionist) => ({
title: nutritionist.full_name_and_degree,
value: nutritionist.id,
info: nutritionist.handled_age_group.split(','),
info: nutritionist.mastered_nutritional_problems
.split(';')
.slice(0, 3),
onReadMore: () =>
navigation.navigate(ROUTES.nutritionistDetail, {
id: nutritionist.id,
}),
}))}
{...getFormFieldProps('nutritionist')}
value={values.nutritionist}
onChange={(v) => handleChange('nutritionist', v)}
/>
</ScrollView>,
]}
onFinish={handleSubmit}
finishButtonLabel="Bayar"
/>
);
};
......
import { ProgramRecommendations } from 'services/dietelaQuiz/quizResult';
import { DietelaProgram } from 'services/cart/models';
import {
ProgramRecommendations,
DietelaProgram,
} from 'services/dietelaQuiz/quizResult';
import { CartRequest } from 'services/payment/models';
export const initialValues = {
program: '',
nutritionist: '',
export const initialValues: CartRequest = {
program: null,
nutritionist: null,
};
export const getRecommendedPrograms = (
......
......@@ -30,9 +30,9 @@ describe('InitialPage', () => {
test('has call-to-action button that navigates to Dietela Quiz', () => {
let props = createTestProps({});
const { queryByTestId } = render(<InitialPage {...props} />);
const { getByText } = render(<InitialPage {...props} />);
const button = queryByTestId('cta-button')!;
const button = getByText(/konsultasi sekarang/i);
fireEvent.press(button);
expect(button).toBeTruthy();
......
import React, { FC } from 'react';
import { View, Text, ImageBackground, Image } from 'react-native';
import { layoutStyles, typographyStyles } from '../../../styles';
import { layoutStyles, typographyStyles } from 'styles';
import { BigButton } from 'components/core';
import { styles } from './styles';
import { banner_girl_eating, logo_white_small } from 'assets/images';
import * as ROUTES from 'constants/routes';
import BigButton from 'components/core/Button';
const InitialPage: FC = ({ navigation }) => (
<ImageBackground
......@@ -30,7 +30,6 @@ const InitialPage: FC = ({ navigation }) => (
<BigButton
title="konsultasi sekarang"
onPress={() => navigation.navigate(ROUTES.allAccessQuestionnaire)}
testID="cta-button"
/>
</View>
</View>
......
import React from 'react';
import { render, fireEvent, waitFor } from 'utils/testing';
import { act, render, fireEvent, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import axios from 'axios';
......@@ -70,7 +70,7 @@ describe('AllAccessQuestionnaire', () => {
});
const maleChoice = getByText(/Pria/i);
fireEvent.press(maleChoice);
act(() => fireEvent.press(maleChoice));
allAccessQuestions.slice(1).forEach(({ choiceList }) => {
const nextButton = getByText(/Lanjut/i);
......@@ -106,7 +106,7 @@ describe('AllAccessQuestionnaire', () => {
});
const femaleChoice = getByText(/Wanita/i);
fireEvent.press(femaleChoice);
act(() => fireEvent.press(femaleChoice));
allAccessQuestions.forEach(({ choiceList }) => {
const nextButton = getByText(/Lanjut/i);
......
......@@ -43,7 +43,7 @@ const AllAccessQuestionnaire: FC = () => {
const response = await createDietProfileApi(convertPayload(values));
if (response.success) {
setCache(CACHE_KEYS.dietProfileId, response.data?.id);
await setCache(CACHE_KEYS.dietProfileId, response.data?.id);
navigation.navigate(ROUTES.dietelaQuizResult, response.data);
} else {
Toast.show({
......
......@@ -30,7 +30,7 @@ const DietelaQuizResult: FC = () => {
renderItem={({ item }: any) => item}
sliderWidth={Dimensions.get('window').width}
itemWidth={Dimensions.get('window').width}
onSnapToItem={(index) => setActiveSlide(index)}
onSnapToItem={setActiveSlide}
/>
<CarouselPagination index={activeSlide} length={9} />
</View>
......