Fakultas Ilmu Komputer UI

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

Add routes param mock for testing and configure new CI stages

parent 1339c97a
......@@ -64,3 +64,4 @@ buck-out/
coverage/
.husky
.eslintcache
image: reactnativecommunity/react-native-android
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
......@@ -8,21 +6,47 @@ cache:
- ios/
stages:
- lint
- test
- pbi-sonar-scanner
- sonar-scanner
- build
lint-test:
stage: test
lint:
image: node:slim
stage: lint
before_script:
- yarn install
script:
- yarn lint
test:
image: node:slim
stage: test
before_script:
- yarn install
script:
- yarn test --coverage --watchAll=false --verbose --collectCoverageFrom="src/**/*.tsx"
artifacts:
paths:
- coverage
pbi-sonar-scanner:
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: ['']
stage: pbi-sonar-scanner
script:
- sonar-scanner
-Dsonar.host.url=$SONARQUBE_URL
-Dsonar.login=$SONARQUBE_TOKEN
-Dsonar.branch.name=$CI_COMMIT_REF_NAME
-Dsonar.branch.target=staging
-Dsonar.projectKey=$SONARQUBE_PROJECT_KEY
except:
- staging
- master
sonar-scanner:
image:
name: sonarsource/sonar-scanner-cli:latest
......@@ -34,15 +58,19 @@ sonar-scanner:
-Dsonar.login=$SONARQUBE_TOKEN
-Dsonar.branch.name=$CI_COMMIT_REF_NAME
-Dsonar.projectKey=$SONARQUBE_PROJECT_KEY
only:
- staging
- master
android:
image: reactnativecommunity/react-native-android
stage: build
before_script:
- yarn install
- export ANDROID_SDK_ROOT=/usr/lib/android-sdk
script:
- cd android
- chmod +x gradlew && ./gradlew clean && ./gradlew assembleRelease
- chmod +x gradlew && ./gradlew assembleRelease
- cd .. && cp android/app/build/outputs/apk/release/app-release.apk $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
artifacts:
name: '$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME'
......
......@@ -59,10 +59,9 @@
"@types/react": "^16"
},
"lint-staged": {
"./src/*.{ts,tsx}": [
"yarn run lint",
"yarn run prettify"
],
"*.{ts,tsx}": "eslint --cache --fix"
"*.{ts,tsx}": [
"yarn lint",
"yarn prettify"
]
}
}
......@@ -39,10 +39,10 @@ describe('WizardContainer component', () => {
});
it('shows finish button in the last component', () => {
const { findByText } = render(
const { queryByText } = render(
<WizardContainer {...props} currentStep={components.length} />,
);
const finishButton = findByText(/Selesai/i);
const finishButton = queryByText(/Selesai/i);
expect(finishButton).toBeTruthy();
});
});
import {
DietProfileResponse,
DietProfileRequest,
} from 'services/dietelaQuiz/models';
import {
BodyMassConstants,
VegetableAndFruitSufficiencyResponse,
SugarSaltFatProblemResponse,
LargeMealDietRecommendation,
SnacksDietRecommendation,
BreakfastReponse,
PhysicalActivityResponse,
} from 'services/dietelaQuiz/quizResult';
import { DietelaProgram } from 'services/cart/models';
const validFormValues: DietProfileRequest = {
name: 'Dietela',
email: 'dietela@gmail.com',
age: 20,
weight: 60,
height: 172,
gender: 2,
special_condition: 1,
body_activity: 1,
vegetables_in_one_day: 1,
fruits_in_one_day: 1,
fried_food_in_one_day: 1,
sweet_snacks_in_one_day: 1,
sweet_drinks_in_one_day: 1,
packaged_food_in_one_day: 1,
large_meal_in_one_day: 1,
snacks_in_one_day: 1,
breakfast_type: 1,
current_condition: 1,
problem_to_solve: 1,
health_problem: [2, 3],
};
export const mockProgramRecommendations = {
priority_1: DietelaProgram.TRIAL,
priority_2: null,
};
export const mockQuizResult: DietProfileResponse = {
id: 1,
...validFormValues,
quiz_result: {
diet_profile: 1,
age: 20,
weight: 60,
height: 172,
gender: 2,
body_mass_index: 20,
nutrition_status: BodyMassConstants.NORMAL,
ideal_weight_range: {
max: 68.0432,
min: 56.209599999999995,
},
daily_energy_needs: 1696.8,
daily_nutrition_needs: {
fat_needs: 56.559999999999995,
fiber_needs: 25,
protein_needs: 55.146,
carbohydrate_needs: 241.79399999999998,
},
vegetable_and_fruit_sufficiency:
VegetableAndFruitSufficiencyResponse.LACKING,
vegetable_and_fruit_diet_recommendation:
'Yah.. asupan buah & sayur kamu masih kurang nih. Yuk, cari solusi agar kamu bisa konsumsi setidaknya 2 porsi sayur & 3 porsi buah atau sebaliknya per hari. Pokoknya kalau di total kamu perlu 5 porsi buah & sayur per hari.',
sugar_salt_fat_problem: SugarSaltFatProblemResponse.CONTROLLED,
sugar_salt_fat_diet_recommendation:
'Selamat! Kamu sudah memenuhi salah satu langkah untuk menuju hidup sehat. Pertahankan pemilihan jenis makanan yang seperti ini ya. Upayakan agar jumlah gorengan, cemilan manis, makan manis, makanan kemasan, dan makanan cepat saji yang kamu makan bisa terkontrol jumlahnya.',
large_meal_diet_recommendation: LargeMealDietRecommendation.ONCE_A_DAY,
snacks_diet_recommendation: SnacksDietRecommendation.NO_SNACK,
breakfast_recommendation: BreakfastReponse.NO_BREAKFAST,
energy_needed_per_dine: {
lunch: 509.03999999999996,
dinner: 509.03999999999996,
breakfast: 169.68,
morning_snack: 254.51999999999998,
afternoon_snack: 254.51999999999998,
},
physical_activity_recommendation: PhysicalActivityResponse.LEVEL1_ACTIVITY,
program_recommendation: mockProgramRecommendations,
},
};
......@@ -4,6 +4,7 @@ import axios from 'axios';
import * as ROUTES from 'constants/routes';
import ChoosePlan from '.';
import { mockProgramRecommendations } from 'mocks/quizResult';
jest.mock('axios');
......@@ -46,21 +47,26 @@ describe('ChoosePlan', () => {
});
it('redirects to program detail page when user clicks Baca selengkapnya button for program', async () => {
const { queryAllByText } = render(<ChoosePlan />, ROUTES.choosePlan);
const { getAllByText, getByText } = render(
<ChoosePlan />,
ROUTES.choosePlan,
mockProgramRecommendations,
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
const readMoreButton = queryAllByText(/Baca selengkapnya/i)[0];
expect(readMoreButton).toBeFalsy();
// fireEvent.press(readMoreButton);
const readMoreButton = getAllByText(/Baca selengkapnya/i)[0];
expect(readMoreButton).toBeTruthy();
fireEvent.press(readMoreButton);
// const programDetailPage = queryByText(/Coming Soon/i);
// expect(programDetailPage).toBeFalsy();
const programDetailPage = getByText(/Coming Soon/i);
expect(programDetailPage).toBeTruthy();
});
it('redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist', async () => {
const { getAllByText, getByText } = render(
<ChoosePlan />,
ROUTES.choosePlan,
mockProgramRecommendations,
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
......
import React, { FC, useState } from 'react';
import { ScrollView } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import { useNavigation, useRoute } from '@react-navigation/native';
import { WizardContainer, Loader } from 'components/core';
......@@ -10,11 +9,12 @@ import { useForm, useApi } from 'hooks';
import { dietPrograms } from 'constants/dietelaProgram';
import { retrieveNutritionistsApi } from 'services/nutritionists';
import { Nutritionist } from 'services/nutritionists/models';
import { ProgramRecommendations } from 'services/dietelaQuiz/quizResult';
import { layoutStyles } from 'styles';
import { setCache } from 'utils/cache';
import { PricingList } from './components';
import { initialValues, getRecommendedPrograms } from './schema';
import { ProgramRecommendations } from 'services/dietelaQuiz/quizResult';
const ChoosePlan: FC = () => {
const navigation = useNavigation();
......@@ -25,15 +25,9 @@ const ChoosePlan: FC = () => {
const { handleSubmit, getFormFieldProps } = useForm({
initialValues,
onSubmit: async (values) => {
await AsyncStorage.setItem(
CACHE_KEYS.selectedProgramId,
`${values.program}`,
);
await AsyncStorage.setItem(
CACHE_KEYS.selectedNutritionistId,
`${values.nutritionist}`,
);
onSubmit: (values) => {
setCache(CACHE_KEYS.selectedProgramId, values.program);
setCache(CACHE_KEYS.selectedNutritionistId, values.nutritionist);
navigation.navigate(ROUTES.cart);
},
});
......
......@@ -5,6 +5,7 @@ import axios from 'axios';
import AllAccessQuestionnaire from '.';
import { allAccessQuestions, textFields } from './schema';
import { mockQuizResult } from 'mocks/quizResult';
jest.mock('react-native-toast-message');
jest.mock('axios');
......@@ -14,9 +15,9 @@ describe('AllAccessQuestionnaire', () => {
const validFormValues: { [_: string]: any } = {
name: 'Dietela',
email: 'dietela@gmail.com',
age: '29',
weight: '82',
height: '178',
age: '20',
weight: '60',
height: '172',
gender: 1,
special_condition: 1,
body_activity: 1,
......@@ -31,7 +32,7 @@ describe('AllAccessQuestionnaire', () => {
breakfast_type: 1,
current_condition: 1,
problem_to_solve: 1,
health_problem: [1],
health_problem: [2, 3],
};
it('shows biodata form and all required errors after submit with empty form values', async () => {
......@@ -91,44 +92,41 @@ describe('AllAccessQuestionnaire', () => {
expect(nextPage).toBeTruthy();
});
// it('redirects to quiz result page if all form values are valid and submit success', async () => {
// const createDietProfileApi = () =>
// Promise.resolve({
// status: 201,
// data: {
// id: 1,
// ...validFormValues,
// },
// });
// mockAxios.request.mockImplementationOnce(createDietProfileApi);
// const { getByText, getByPlaceholderText } = render(
// <AllAccessQuestionnaire />,
// ROUTES.allAccessQuestionnaire,
// );
// textFields.forEach(({ name, placeholder }) => {
// const formField = getByPlaceholderText(placeholder as string);
// fireEvent.changeText(formField, validFormValues[name]);
// });
// const maleChoice = getByText(/Pria/i);
// fireEvent.press(maleChoice);
// allAccessQuestions.forEach(({ choiceList }) => {
// const nextButton = getByText(/Lanjut/i);
// fireEvent.press(nextButton);
// const firstChoice = getByText(choiceList[0]);
// fireEvent.press(firstChoice);
// });
// const submitButton = getByText('Selesai');
// await waitFor(() => fireEvent.press(submitButton));
// const quizResultPage = getByText(/Quiz Result/i);
// expect(quizResultPage).toBeTruthy();
// });
it('redirects to quiz result page if all form values are valid and submit success', async () => {
const createDietProfileApi = () =>
Promise.resolve({
status: 201,
data: mockQuizResult,
});
mockAxios.request.mockImplementationOnce(createDietProfileApi);
const { getByText, getByPlaceholderText } = render(
<AllAccessQuestionnaire />,
ROUTES.allAccessQuestionnaire,
);
textFields.forEach(({ name, placeholder }) => {
const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, validFormValues[name]);
});
const maleChoice = getByText(/Pria/i);
fireEvent.press(maleChoice);
allAccessQuestions.forEach(({ choiceList }) => {
const nextButton = getByText(/Lanjut/i);
fireEvent.press(nextButton);
const firstChoice = getByText(choiceList[0]);
fireEvent.press(firstChoice);
});
const submitButton = getByText('Selesai');
await waitFor(() => fireEvent.press(submitButton));
const quizResultPage = getByText(/Quiz Result/i);
expect(quizResultPage).toBeTruthy();
});
it('does not redirect to quiz result page if all form values are valid but submit fails', async () => {
const createDietProfileApi = () =>
......
import React, { FC, useState } from 'react';
import { View } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import { useNavigation } from '@react-navigation/native';
import { WizardContainer, Toast } from 'components/core';
......@@ -21,6 +20,7 @@ import {
convertPayload,
} from './schema';
import { generateValidationSchema } from 'utils/form';
import { setCache } from 'utils/cache';
const AllAccessQuestionnaire: FC = () => {
const navigation = useNavigation();
......@@ -41,10 +41,7 @@ const AllAccessQuestionnaire: FC = () => {
const response = await createDietProfileApi(convertPayload(values));
if (response.success) {
await AsyncStorage.setItem(
CACHE_KEYS.dietProfileId,
`${response.data?.id}`,
);
setCache(CACHE_KEYS.dietProfileId, response.data?.id);
navigation.navigate(ROUTES.dietelaQuizResult, response.data);
} else {
Toast.show({
......
......@@ -5,13 +5,13 @@ import Carousel from 'react-native-snap-carousel';
import { CarouselPagination } from 'components/core';
import { ResultPage, pages } from './pages';
import { styles } from './styles';
import { useRoute } from '@react-navigation/core';
import { useRoute } from '@react-navigation/native';
import { DietProfileResponse } from 'services/dietelaQuiz/models';
const DietelaQuizResult: FC = () => {
const [activeSlide, setActiveSlide] = useState(0);
const route = useRoute();
const resultData: DietProfileResponse = route.params;
const resultData = route.params as DietProfileResponse;
return (
<View style={styles.view}>
......@@ -19,14 +19,15 @@ const DietelaQuizResult: FC = () => {
data={pages.map((page, idx) =>
idx === 8 ? (
<ResultPage
key={idx}
content={page(resultData)}
cta={resultData.quiz_result.program_recommendation}
/>
) : (
<ResultPage content={page(resultData)} />
<ResultPage content={page(resultData)} key={idx} />
),
)}
renderItem={({ item }) => item}
renderItem={({ item }: any) => item}
sliderWidth={Dimensions.get('window').width}
itemWidth={Dimensions.get('window').width}
onSnapToItem={(index) => setActiveSlide(index)}
......
......@@ -33,11 +33,11 @@ const ResultPage: FC<{
<Section key={i}>
<Text style={[typographyStyles.bodySmall]}>{section.header}</Text>
{section.content.statistics?.map((statRow) => (
<View style={styles.marginTop}>
{section.content.statistics?.map((statRow, ii) => (
<View style={styles.marginTop} key={`stat${ii}`}>
<Row>
{statRow.map((stat) => (
<Column>
{statRow.map((stat, iii) => (
<Column key={`statrow${iii}`}>
<Statistic
title={stat.label}
emote={stat.emote}
......
import { DietProfileResponse } from 'services/dietelaQuiz/models';
import { DietelaProgram } from 'services/cart/models';
export enum Status {
......@@ -174,72 +173,3 @@ export interface ProgramRecommendations {
priority_1: DietelaProgram;
priority_2: DietelaProgram | null;
}
export const exampleResult: DietProfileResponse = {
id: 1,
health_problem: [2, 3],
name: 'test2',
email: 'test2@test.com',
age: 20,
weight: 60,
height: 172,
gender: 2,
special_condition: 1,
body_activity: 1,
vegetables_in_one_day: 1,
fruits_in_one_day: 1,
fried_food_in_one_day: 1,
sweet_snacks_in_one_day: 1,
sweet_drinks_in_one_day: 1,
packaged_food_in_one_day: 1,
large_meal_in_one_day: 1,
snacks_in_one_day: 1,
breakfast_type: 1,
current_condition: 1,
problem_to_solve: 1,
quiz_result: {
diet_profile: 1,
age: 20,
weight: 60,
height: 172,
gender: 2,
body_mass_index: 20,
nutrition_status: 'Normal',
ideal_weight_range: {
max: 68.0432,
min: 56.209599999999995,
},
daily_energy_needs: 1696.8,
daily_nutrition_needs: {
fat_needs: 56.559999999999995,
fiber_needs: 25,
protein_needs: 55.146,
carbohydrate_needs: 241.79399999999998,
},
vegetable_and_fruit_sufficiency: 'Kurang makan sayur dan buah',
vegetable_and_fruit_diet_recommendation:
'Yah.. asupan buah & sayur kamu masih kurang nih. Yuk, cari solusi agar kamu bisa konsumsi setidaknya 2 porsi sayur & 3 porsi buah atau sebaliknya per hari. Pokoknya kalau di total kamu perlu 5 porsi buah & sayur per hari.',
sugar_salt_fat_problem: 'Asupan gula, garam, dan lemak terkontrol',
sugar_salt_fat_diet_recommendation:
'Selamat! Kamu sudah memenuhi salah satu langkah untuk menuju hidup sehat. Pertahankan pemilihan jenis makanan yang seperti ini ya. Upayakan agar jumlah gorengan, cemilan manis, makan manis, makanan kemasan, dan makanan cepat saji yang kamu makan bisa terkontrol jumlahnya.',
large_meal_diet_recommendation:
'Hmmm.... Kenapa cuma makan besar 1 kali sehari? Makan besar cuma 1 kali dalam sehari berisiko membuat kamu makan berlebihan dan kekurangan zat gizi tertentu Lho! Yuk coba mulai diatur pola makannya, dengan merutinkan waktu makan agar metabolisme kamu lebih baik.',
snacks_diet_recommendation:
'Yah kok gak ngemil? Makan cemilan itu penting untuk menjaga kadar gula darah kamu seharian, dengan ngemil gula darah seharian kamu bisa terkontrol sehingga mencegah lemas dan berkurangnya konsentrasi.',
breakfast_recommendation:
'Perut kosong di pagi hari bisa mengarahkan kepada 2 keadaan, antara membuat makan berlebihan di siang hari atau kurang asupan zat gizi per hari. Makan pagi bisa disesuaikan dengan pola bangun tidur kamu lho, cari deh solusi terbaik agar kamu bisa beraktifitas tanpa perut kosong.',
energy_needed_per_dine: {
lunch: 509.03999999999996,
dinner: 509.03999999999996,
breakfast: 169.68,
morning_snack: 254.51999999999998,
afternoon_snack: 254.51999999999998,
},
physical_activity_recommendation:
'Hayooo kok duduk-duduk aja? Yuk ah mulai lakukan aktivitas fisik, dimulai dari aktivitas yang ringan seperti jalan kaki, jogging, senam, atau naik turun tangga 15 menit per hari. Tingkatkan perlahan hingga mencapai 30 menit per hari. Konsultasikan kebutuhan gizi harianmu kepada Dietela agar asupan makan kamu seimbang dengan energi yang kamu keluarkan setiap harinya',
program_recommendation: {
priority_1: 'TRIAL',
priority_2: null,
},
},
};
import AsyncStorage from '@react-native-community/async-storage';
import { ReactNode } from 'react';
export const setCache = async (
key: string,
value: ReactNode,
callback?: (error?: Error) => void,
) => await AsyncStorage.setItem(key, `${value}`, callback);
......@@ -9,10 +9,11 @@ const Stack = createStackNavigator();
interface TestProvider {
route: string;
params?: any;
children: ReactElement;
}
const TestProvider: FC<TestProvider> = ({ route, children }) => {
const TestProvider: FC<TestProvider> = ({ route, params, children }) => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName={route}>
......@@ -21,6 +22,7 @@ const TestProvider: FC<TestProvider> = ({ route, children }) => {
key={`nav${i}`}
name={nav.name}
component={route === nav.name ? () => children : nav.component}
initialParams={params}
options={{
title: nav.header,
headerShown: Boolean(nav.header),
......@@ -35,10 +37,15 @@ const TestProvider: FC<TestProvider> = ({ route, children }) => {
const customRender = (
ui: ReactElement,
route: string,
params?: any,
options?: Omit<RenderOptions, 'queries'>,
) =>
render(ui, {
wrapper: () => <TestProvider route={route}>{ui}</TestProvider>,
wrapper: () => (
<TestProvider route={route} params={params}>
{ui}
</TestProvider>
),
...options,
});
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment