Fakultas Ilmu Komputer UI

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

Merge branch 'add-testing-param-mock-and-configure-ci' into 'staging'

Add routes param mock for testing and configure new CI stages

See merge request !22
parents 1339c97a d0d754a8
Pipeline #70011 passed with stages
in 52 minutes and 21 seconds
...@@ -64,3 +64,4 @@ buck-out/ ...@@ -64,3 +64,4 @@ buck-out/
coverage/ coverage/
.husky .husky
.eslintcache
image: reactnativecommunity/react-native-android
cache: cache:
key: ${CI_COMMIT_REF_SLUG} key: ${CI_COMMIT_REF_SLUG}
paths: paths:
...@@ -8,21 +6,47 @@ cache: ...@@ -8,21 +6,47 @@ cache:
- ios/ - ios/
stages: stages:
- lint
- test - test
- pbi-sonar-scanner
- sonar-scanner - sonar-scanner
- build - build
lint-test: lint:
stage: test image: node:slim
stage: lint
before_script: before_script:
- yarn install - yarn install
script: script:
- yarn lint - yarn lint
test:
image: node:slim
stage: test
before_script:
- yarn install
script:
- yarn test --coverage --watchAll=false --verbose --collectCoverageFrom="src/**/*.tsx" - yarn test --coverage --watchAll=false --verbose --collectCoverageFrom="src/**/*.tsx"
artifacts: artifacts:
paths: paths:
- coverage - 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: sonar-scanner:
image: image:
name: sonarsource/sonar-scanner-cli:latest name: sonarsource/sonar-scanner-cli:latest
...@@ -34,15 +58,19 @@ sonar-scanner: ...@@ -34,15 +58,19 @@ sonar-scanner:
-Dsonar.login=$SONARQUBE_TOKEN -Dsonar.login=$SONARQUBE_TOKEN
-Dsonar.branch.name=$CI_COMMIT_REF_NAME -Dsonar.branch.name=$CI_COMMIT_REF_NAME
-Dsonar.projectKey=$SONARQUBE_PROJECT_KEY -Dsonar.projectKey=$SONARQUBE_PROJECT_KEY
only:
- staging
- master
android: android:
image: reactnativecommunity/react-native-android
stage: build stage: build
before_script: before_script:
- yarn install - yarn install
- export ANDROID_SDK_ROOT=/usr/lib/android-sdk - export ANDROID_SDK_ROOT=/usr/lib/android-sdk
script: script:
- cd android - 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 - cd .. && cp android/app/build/outputs/apk/release/app-release.apk $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
artifacts: artifacts:
name: '$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME' name: '$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME'
......
...@@ -59,10 +59,9 @@ ...@@ -59,10 +59,9 @@
"@types/react": "^16" "@types/react": "^16"
}, },
"lint-staged": { "lint-staged": {
"./src/*.{ts,tsx}": [ "*.{ts,tsx}": [
"yarn run lint", "yarn lint",
"yarn run prettify" "yarn prettify"
], ]
"*.{ts,tsx}": "eslint --cache --fix"
} }
} }
...@@ -39,10 +39,10 @@ describe('WizardContainer component', () => { ...@@ -39,10 +39,10 @@ describe('WizardContainer component', () => {
}); });
it('shows finish button in the last component', () => { it('shows finish button in the last component', () => {
const { findByText } = render( const { queryByText } = render(
<WizardContainer {...props} currentStep={components.length} />, <WizardContainer {...props} currentStep={components.length} />,
); );
const finishButton = findByText(/Selesai/i); const finishButton = queryByText(/Selesai/i);
expect(finishButton).toBeTruthy(); 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'; ...@@ -4,6 +4,7 @@ import axios from 'axios';
import * as ROUTES from 'constants/routes'; import * as ROUTES from 'constants/routes';
import ChoosePlan from '.'; import ChoosePlan from '.';
import { mockProgramRecommendations } from 'mocks/quizResult';
jest.mock('axios'); jest.mock('axios');
...@@ -46,21 +47,26 @@ describe('ChoosePlan', () => { ...@@ -46,21 +47,26 @@ describe('ChoosePlan', () => {
}); });
it('redirects to program detail page when user clicks Baca selengkapnya button for program', async () => { 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()); await waitFor(() => expect(mockAxios.request).toBeCalled());
const readMoreButton = queryAllByText(/Baca selengkapnya/i)[0]; const readMoreButton = getAllByText(/Baca selengkapnya/i)[0];
expect(readMoreButton).toBeFalsy(); expect(readMoreButton).toBeTruthy();
// fireEvent.press(readMoreButton); fireEvent.press(readMoreButton);
// const programDetailPage = queryByText(/Coming Soon/i); const programDetailPage = getByText(/Coming Soon/i);
// expect(programDetailPage).toBeFalsy(); expect(programDetailPage).toBeTruthy();
}); });
it('redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist', async () => { it('redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist', async () => {
const { getAllByText, getByText } = render( const { getAllByText, getByText } = render(
<ChoosePlan />, <ChoosePlan />,
ROUTES.choosePlan, ROUTES.choosePlan,
mockProgramRecommendations,
); );
await waitFor(() => expect(mockAxios.request).toBeCalled()); await waitFor(() => expect(mockAxios.request).toBeCalled());
......
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { ScrollView } from 'react-native'; import { ScrollView } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import { useNavigation, useRoute } from '@react-navigation/native'; import { useNavigation, useRoute } from '@react-navigation/native';
import { WizardContainer, Loader } from 'components/core'; import { WizardContainer, Loader } from 'components/core';
...@@ -10,11 +9,12 @@ import { useForm, useApi } from 'hooks'; ...@@ -10,11 +9,12 @@ import { useForm, useApi } from 'hooks';
import { dietPrograms } from 'constants/dietelaProgram'; import { dietPrograms } from 'constants/dietelaProgram';
import { retrieveNutritionistsApi } from 'services/nutritionists'; import { retrieveNutritionistsApi } from 'services/nutritionists';
import { Nutritionist } from 'services/nutritionists/models'; import { Nutritionist } from 'services/nutritionists/models';
import { ProgramRecommendations } from 'services/dietelaQuiz/quizResult';
import { layoutStyles } from 'styles'; import { layoutStyles } from 'styles';
import { setCache } from 'utils/cache';
import { PricingList } from './components'; import { PricingList } from './components';
import { initialValues, getRecommendedPrograms } from './schema'; import { initialValues, getRecommendedPrograms } from './schema';
import { ProgramRecommendations } from 'services/dietelaQuiz/quizResult';
const ChoosePlan: FC = () => { const ChoosePlan: FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
...@@ -25,15 +25,9 @@ const ChoosePlan: FC = () => { ...@@ -25,15 +25,9 @@ const ChoosePlan: FC = () => {
const { handleSubmit, getFormFieldProps } = useForm({ const { handleSubmit, getFormFieldProps } = useForm({
initialValues, initialValues,
onSubmit: async (values) => { onSubmit: (values) => {
await AsyncStorage.setItem( setCache(CACHE_KEYS.selectedProgramId, values.program);
CACHE_KEYS.selectedProgramId, setCache(CACHE_KEYS.selectedNutritionistId, values.nutritionist);
`${values.program}`,
);
await AsyncStorage.setItem(
CACHE_KEYS.selectedNutritionistId,
`${values.nutritionist}`,
);
navigation.navigate(ROUTES.cart); navigation.navigate(ROUTES.cart);
}, },
}); });
......
...@@ -5,6 +5,7 @@ import axios from 'axios'; ...@@ -5,6 +5,7 @@ import axios from 'axios';
import AllAccessQuestionnaire from '.'; import AllAccessQuestionnaire from '.';
import { allAccessQuestions, textFields } from './schema'; import { allAccessQuestions, textFields } from './schema';
import { mockQuizResult } from 'mocks/quizResult';
jest.mock('react-native-toast-message'); jest.mock('react-native-toast-message');
jest.mock('axios'); jest.mock('axios');
...@@ -14,9 +15,9 @@ describe('AllAccessQuestionnaire', () => { ...@@ -14,9 +15,9 @@ describe('AllAccessQuestionnaire', () => {
const validFormValues: { [_: string]: any } = { const validFormValues: { [_: string]: any } = {
name: 'Dietela', name: 'Dietela',
email: 'dietela@gmail.com', email: 'dietela@gmail.com',
age: '29', age: '20',
weight: '82', weight: '60',
height: '178', height: '172',
gender: 1, gender: 1,
special_condition: 1, special_condition: 1,
body_activity: 1, body_activity: 1,
...@@ -31,7 +32,7 @@ describe('AllAccessQuestionnaire', () => { ...@@ -31,7 +32,7 @@ describe('AllAccessQuestionnaire', () => {
breakfast_type: 1, breakfast_type: 1,
current_condition: 1, current_condition: 1,
problem_to_solve: 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 () => { it('shows biodata form and all required errors after submit with empty form values', async () => {
...@@ -91,44 +92,41 @@ describe('AllAccessQuestionnaire', () => { ...@@ -91,44 +92,41 @@ describe('AllAccessQuestionnaire', () => {
expect(nextPage).toBeTruthy(); expect(nextPage).toBeTruthy();
}); });
// it('redirects to quiz result page if all form values are valid and submit success', async () => { it('redirects to quiz result page if all form values are valid and submit success', async () => {
// const createDietProfileApi = () => const createDietProfileApi = () =>
// Promise.resolve({ Promise.resolve({
// status: 201, status: 201,
// data: { data: mockQuizResult,
// id: 1, });
// ...validFormValues, mockAxios.request.mockImplementationOnce(createDietProfileApi);
// },
// }); const { getByText, getByPlaceholderText } = render(
// mockAxios.request.mockImplementationOnce(createDietProfileApi); <AllAccessQuestionnaire />,
ROUTES.allAccessQuestionnaire,
// const { getByText, getByPlaceholderText } = render( );
// <AllAccessQuestionnaire />,
// ROUTES.allAccessQuestionnaire, textFields.forEach(({ name, placeholder }) => {
// ); const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, validFormValues[name]);
// textFields.forEach(({ name, placeholder }) => { });
// const formField = getByPlaceholderText(placeholder as string);
// fireEvent.changeText(formField, validFormValues[name]); const maleChoice = getByText(/Pria/i);
// }); fireEvent.press(maleChoice);
// const maleChoice = getByText(/Pria/i); allAccessQuestions.forEach(({ choiceList }) => {
// fireEvent.press(maleChoice); const nextButton = getByText(/Lanjut/i);
fireEvent.press(nextButton);
// allAccessQuestions.forEach(({ choiceList }) => {
// const nextButton = getByText(/Lanjut/i); const firstChoice = getByText(choiceList[0]);
// fireEvent.press(nextButton); fireEvent.press(firstChoice);
});
// const firstChoice = getByText(choiceList[0]);
// fireEvent.press(firstChoice); const submitButton = getByText('Selesai');
// }); await waitFor(() => fireEvent.press(submitButton));
// const submitButton = getByText('Selesai'); const quizResultPage = getByText(/Quiz Result/i);
// await waitFor(() => fireEvent.press(submitButton)); expect(quizResultPage).toBeTruthy();
});
// 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 () => { it('does not redirect to quiz result page if all form values are valid but submit fails', async () => {
const createDietProfileApi = () => const createDietProfileApi = () =>
......
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { WizardContainer, Toast } from 'components/core'; import { WizardContainer, Toast } from 'components/core';
...@@ -21,6 +20,7 @@ import { ...@@ -21,6 +20,7 @@ import {
convertPayload, convertPayload,
} from './schema'; } from './schema';
import { generateValidationSchema } from 'utils/form'; import { generateValidationSchema } from 'utils/form';
import { setCache } from 'utils/cache';
const AllAccessQuestionnaire: FC = () => { const AllAccessQuestionnaire: FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
...@@ -41,10 +41,7 @@ const AllAccessQuestionnaire: FC = () => { ...@@ -41,10 +41,7 @@ const AllAccessQuestionnaire: FC = () => {
const response = await createDietProfileApi(convertPayload(values)); const response = await createDietProfileApi(convertPayload(values));
if (response.success) { if (response.success) {
await AsyncStorage.setItem( setCache(CACHE_KEYS.dietProfileId, response.data?.id);
CACHE_KEYS.dietProfileId,
`${response.data?.id}`,
);
navigation.navigate(ROUTES.dietelaQuizResult, response.data); navigation.navigate(ROUTES.dietelaQuizResult, response.data);
} else { } else {
Toast.show({ Toast.show({
......
...@@ -5,13 +5,13 @@ import Carousel from 'react-native-snap-carousel'; ...@@ -5,13 +5,13 @@ import Carousel from 'react-native-snap-carousel';
import { CarouselPagination } from 'components/core'; import { CarouselPagination } from 'components/core';
import { ResultPage, pages } from './pages'; import { ResultPage, pages } from './pages';
import { styles } from './styles'; import { styles } from './styles';
import { useRoute } from '@react-navigation/core'; import { useRoute } from '@react-navigation/native';
import { DietProfileResponse } from 'services/dietelaQuiz/models'; import { DietProfileResponse } from 'services/dietelaQuiz/models';
const DietelaQuizResult: FC = () => { const DietelaQuizResult: FC = () => {
const [activeSlide, setActiveSlide] = useState(0); const [activeSlide, setActiveSlide] = useState(0);
const route = useRoute(); const route = useRoute();
const resultData: DietProfileResponse = route.params; const resultData = route.params as DietProfileResponse;
return ( return (
<View style={styles.view}> <View style={styles.view}>
...@@ -19,14 +19,15 @@ const DietelaQuizResult: FC = () => { ...@@ -19,14 +19,15 @@ const DietelaQuizResult: FC = () => {
data={pages.map((page, idx) => data={pages.map((page, idx) =>
idx === 8 ? ( idx === 8 ? (
<ResultPage <ResultPage
key={idx}
content={page(resultData)} content={page(resultData)}
cta={resultData.quiz_result.program_recommendation} 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} sliderWidth={Dimensions.get('window').width}
itemWidth={Dimensions.get('window').width} itemWidth={Dimensions.get('window').width}
onSnapToItem={(index) => setActiveSlide(index)} onSnapToItem={(index) => setActiveSlide(index)}
......
...@@ -33,11 +33,11 @@ const ResultPage: FC<{ ...@@ -33,11 +33,11 @@ const ResultPage: FC<{
<Section key={i}> <Section key={i}>
<Text style={[typographyStyles.bodySmall]}>{section.header}</Text> <Text style={[typographyStyles.bodySmall]}>{section.header}</Text>
{section.content.statistics?.map((statRow) => ( {section.content.statistics?.map((statRow, ii) => (
<View style={styles.marginTop}> <View style={styles.marginTop} key={`stat${ii}`}>
<Row> <Row>
{statRow.map((stat) => ( {statRow.map((stat, iii) => (
<Column> <Column key={`statrow${iii}`}>
<Statistic <Statistic
title={stat.label} title={stat.label}
emote={stat.emote} emote={stat.emote}
......
import { DietProfileResponse } from 'services/dietelaQuiz/models';
import { DietelaProgram } from 'services/cart/models';