Fakultas Ilmu Komputer UI

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

Revise Dietela Quiz error validation logic

parent d8a1113c
...@@ -26,7 +26,7 @@ test: ...@@ -26,7 +26,7 @@ test:
before_script: before_script:
- yarn install - yarn install
script: script:
- yarn test --coverage --watchAll=false --verbose --collectCoverageFrom="src/**/*.tsx" - yarn test
artifacts: artifacts:
paths: paths:
- coverage - coverage
......
...@@ -6,12 +6,12 @@ import { colors } from 'styles'; ...@@ -6,12 +6,12 @@ import { colors } from 'styles';
import { styles } from './styles'; import { styles } from './styles';
import { NextButtonProps, Props } from './types'; import { NextButtonProps, Props } from './types';
const NextButton: FC<NextButtonProps> = ({ goNext }) => ( const NextButton: FC<NextButtonProps> = ({ goNext, disabled }) => (
<Button <Button
icon={{ icon={{
name: 'caretright', name: 'caretright',
type: 'antdesign', type: 'antdesign',
color: colors.textBlack, color: disabled ? 'gray' : colors.textBlack,
size: 20, size: 20,
}} }}
title="Lanjut" title="Lanjut"
...@@ -21,6 +21,7 @@ const NextButton: FC<NextButtonProps> = ({ goNext }) => ( ...@@ -21,6 +21,7 @@ const NextButton: FC<NextButtonProps> = ({ goNext }) => (
color: colors.textBlack, color: colors.textBlack,
}} }}
buttonStyle={styles.nextButton} buttonStyle={styles.nextButton}
disabled={disabled}
/> />
); );
...@@ -31,6 +32,7 @@ const WizardContainer: FC<Props> = ({ ...@@ -31,6 +32,7 @@ const WizardContainer: FC<Props> = ({
finishButtonLabel, finishButtonLabel,
onFinish, onFinish,
isLoading, isLoading,
isNextDisabled,
}) => { }) => {
const goBack = () => { const goBack = () => {
setCurrentStep(currentStep - 1); setCurrentStep(currentStep - 1);
...@@ -69,14 +71,15 @@ const WizardContainer: FC<Props> = ({ ...@@ -69,14 +71,15 @@ const WizardContainer: FC<Props> = ({
titleStyle={{ titleStyle={{
color: colors.textBlack, color: colors.textBlack,
}} }}
disabled={isNextDisabled}
/> />
) : ( ) : (
<NextButton goNext={goNext} /> <NextButton goNext={goNext} disabled={isNextDisabled} />
)} )}
</View> </View>
) : ( ) : (
<View style={styles.flexEnd}> <View style={styles.flexEnd}>
<NextButton goNext={goNext} /> <NextButton goNext={goNext} disabled={isNextDisabled} />
</View> </View>
)} )}
</ScrollView> </ScrollView>
......
...@@ -3,12 +3,14 @@ import { ReactNode } from 'react'; ...@@ -3,12 +3,14 @@ import { ReactNode } from 'react';
export interface Props { export interface Props {
components: ReactNode[]; components: ReactNode[];
currentStep: number; currentStep: number;
setCurrentStep: (_: number) => void;
finishButtonLabel?: string; finishButtonLabel?: string;
isLoading?: boolean;
onFinish: () => void; onFinish: () => void;
setCurrentStep: (_: number) => void; isLoading?: boolean;
isNextDisabled?: boolean;
} }
export interface NextButtonProps { export interface NextButtonProps {
goNext: () => void; goNext: () => void;
disabled?: boolean;
} }
...@@ -7,4 +7,8 @@ describe('TextField component', () => { ...@@ -7,4 +7,8 @@ describe('TextField component', () => {
it('renders correctly', () => { it('renders correctly', () => {
render(<TextField />); render(<TextField />);
}); });
it('renders correctly with error message', () => {
render(<TextField errorMessage="error" />);
});
}); });
...@@ -27,6 +27,7 @@ const useApi = <T>( ...@@ -27,6 +27,7 @@ const useApi = <T>(
} }
setIsLoading(false); setIsLoading(false);
}; };
fetchData(); fetchData();
}, [fetchApi]); }, [fetchApi]);
......
import { FormikConfig, useFormik } from 'formik'; import { FormikConfig, useFormik } from 'formik';
import { useState } from 'react'; import { useState } from 'react';
const useForm = <T>(formConfig: FormikConfig<T>) => { const useForm = <T>(
const [shouldValidate, setShouldValidate] = useState(false); formConfig: FormikConfig<T>,
immediateValidate: boolean = true,
) => {
const [shouldValidate, setShouldValidate] = useState(immediateValidate);
const { const {
handleChange, handleChange,
handleBlur, handleBlur,
setFieldValue, setFieldValue,
setFieldTouched,
touched,
values, values,
errors, errors,
validateForm, validateForm,
...@@ -24,23 +29,30 @@ const useForm = <T>(formConfig: FormikConfig<T>) => { ...@@ -24,23 +29,30 @@ const useForm = <T>(formConfig: FormikConfig<T>) => {
onChangeText: handleChange(name), onChangeText: handleChange(name),
onBlur: handleBlur(name), onBlur: handleBlur(name),
value: `${values[name]}`, value: `${values[name]}`,
errorMessage: errors[name], errorMessage: touched[name] ? errors[name] : null,
}; };
}; };
const getFormFieldProps = (fieldName: string) => { const getFormFieldProps = (fieldName: string) => {
const name = fieldName as keyof T; const name = fieldName as keyof T;
return { return {
onChange: (value: any) => setFieldValue(fieldName, value), onChange: (value: any) => {
setFieldTouched(fieldName, true);
setFieldValue(fieldName, value);
},
value: values[name], value: values[name],
errorMessage: errors[name], errorMessage: touched[name] ? errors[name] : null,
}; };
}; };
const getFirstErrorIndex = (err: any = errors) => const getError = (fieldName: string) => {
Object.keys(formConfig.initialValues).findIndex( const name = fieldName as keyof T;
(name) => name === Object.keys(err)[0], return errors[name];
); };
const isFieldError = (fieldName: string) => Boolean(getError(fieldName));
const isFormUntouched = () => Object.keys(touched).length === 0;
const validate = async () => { const validate = async () => {
setShouldValidate(true); setShouldValidate(true);
...@@ -54,7 +66,11 @@ const useForm = <T>(formConfig: FormikConfig<T>) => { ...@@ -54,7 +66,11 @@ const useForm = <T>(formConfig: FormikConfig<T>) => {
return { return {
getTextInputProps, getTextInputProps,
getFormFieldProps, getFormFieldProps,
getFirstErrorIndex, getError,
isFieldError,
isFormUntouched,
setFieldValue,
touched,
errors, errors,
values, values,
validateForm: validate, validateForm: validate,
......
...@@ -35,61 +35,20 @@ describe('AllAccessQuestionnaire', () => { ...@@ -35,61 +35,20 @@ describe('AllAccessQuestionnaire', () => {
health_problem: [2, 3], health_problem: [2, 3],
}; };
it('shows biodata form and all required errors after submit with empty form values', async () => { it('initially has disabled next button', () => {
const { getByText, queryByText } = render( const { getByText, queryByText } = render(
<AllAccessQuestionnaire />, <AllAccessQuestionnaire />,
ROUTES.allAccessQuestionnaire, ROUTES.allAccessQuestionnaire,
); );
allAccessQuestions.forEach(() => { const biodataForm = queryByText(/Data Diri/i);
const nextButton = getByText(/Lanjut/i); expect(biodataForm).toBeTruthy();
expect(nextButton).toBeTruthy();
fireEvent.press(nextButton);
});
const submitButton = getByText('Selesai');
expect(submitButton).toBeTruthy();
await waitFor(() => fireEvent.press(submitButton));
const preQuizHeader = getByText(/Data Diri/i);
expect(preQuizHeader).toBeTruthy();
textFields.forEach(({ label }) => {
const errorMessage = getByText(`${label} harus diisi`);
expect(errorMessage).toBeTruthy();
});
const genderErrorMessage = getByText('Pilihan harus diisi');
expect(genderErrorMessage).toBeTruthy();
const sampleNextPage = queryByText(/Pertanyaan/i);
expect(sampleNextPage).toBeFalsy();
});
it('shows the foremost page with error after submit', async () => {
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(() => {
const nextButton = getByText(/Lanjut/i); const nextButton = getByText(/Lanjut/i);
expect(nextButton).toBeTruthy();
fireEvent.press(nextButton); fireEvent.press(nextButton);
});
const submitButton = getByText('Selesai');
await waitFor(() => fireEvent.press(submitButton));
const nextPage = getByText(/Pertanyaan/i); expect(queryByText(/Data Diri/i)).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 () => {
...@@ -113,7 +72,7 @@ describe('AllAccessQuestionnaire', () => { ...@@ -113,7 +72,7 @@ describe('AllAccessQuestionnaire', () => {
const maleChoice = getByText(/Pria/i); const maleChoice = getByText(/Pria/i);
fireEvent.press(maleChoice); fireEvent.press(maleChoice);
allAccessQuestions.forEach(({ choiceList }) => { allAccessQuestions.slice(1).forEach(({ choiceList }) => {
const nextButton = getByText(/Lanjut/i); const nextButton = getByText(/Lanjut/i);
fireEvent.press(nextButton); fireEvent.press(nextButton);
...@@ -146,8 +105,8 @@ describe('AllAccessQuestionnaire', () => { ...@@ -146,8 +105,8 @@ describe('AllAccessQuestionnaire', () => {
fireEvent.changeText(formField, validFormValues[name]); fireEvent.changeText(formField, validFormValues[name]);
}); });
const maleChoice = getByText(/Pria/i); const femaleChoice = getByText(/Wanita/i);
fireEvent.press(maleChoice); fireEvent.press(femaleChoice);
allAccessQuestions.forEach(({ choiceList }) => { allAccessQuestions.forEach(({ choiceList }) => {
const nextButton = getByText(/Lanjut/i); const nextButton = getByText(/Lanjut/i);
......
import React, { FC, useState } from 'react'; import React, { FC, useState, useEffect } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
...@@ -30,10 +30,12 @@ const AllAccessQuestionnaire: FC = () => { ...@@ -30,10 +30,12 @@ const AllAccessQuestionnaire: FC = () => {
const { const {
getTextInputProps, getTextInputProps,
getFormFieldProps, getFormFieldProps,
validateForm, isFieldError,
getFirstErrorIndex, isFormUntouched,
handleSubmit, handleSubmit,
isSubmitting, isSubmitting,
values: formValues,
setFieldValue,
} = useForm({ } = useForm({
initialValues, initialValues,
validationSchema: generateValidationSchema(fieldValidations), validationSchema: generateValidationSchema(fieldValidations),
...@@ -53,20 +55,37 @@ const AllAccessQuestionnaire: FC = () => { ...@@ -53,20 +55,37 @@ const AllAccessQuestionnaire: FC = () => {
}, },
}); });
const onSubmit = async () => { const isCurrentPageError = (): boolean => {
const { isValid, error } = await validateForm(); if (currentPage === 1) {
if (isValid) { const fields = [...textFields, ...radioButtonGroups];
handleSubmit(); return (
} else { isFormUntouched() ||
const firstErrorIdx = getFirstErrorIndex(error) - 4; fields.reduce(
setCurrentPage(firstErrorIdx < 1 ? 1 : firstErrorIdx); (acc: boolean, item) => acc || isFieldError(item.name),
false,
)
);
} }
const fieldPage = currentPage - (formValues.gender === 1 ? 1 : 2);
const fieldName = allAccessQuestions[fieldPage].fieldName;
return isFieldError(fieldName);
}; };
const questions = allAccessQuestions.slice(formValues.gender === 1 ? 1 : 0);
useEffect(() => {
if (formValues.gender === 1) {
setFieldValue('special_condition', 1);
}
}, [formValues.gender, setFieldValue]);
return ( return (
<WizardContainer <WizardContainer
currentStep={currentPage} currentStep={currentPage}
setCurrentStep={setCurrentPage} setCurrentStep={setCurrentPage}
onFinish={handleSubmit}
isLoading={isSubmitting}
isNextDisabled={isCurrentPageError()}
components={[ components={[
<BiodataForm <BiodataForm
textFields={textFields.map((fieldProps) => ({ textFields={textFields.map((fieldProps) => ({
...@@ -78,7 +97,7 @@ const AllAccessQuestionnaire: FC = () => { ...@@ -78,7 +97,7 @@ const AllAccessQuestionnaire: FC = () => {
...getFormFieldProps(fieldProps.name), ...getFormFieldProps(fieldProps.name),
}))} }))}
/>, />,
...allAccessQuestions.map((question, i) => { ...questions.map((question, i) => {
const FormField = question.multiple const FormField = question.multiple
? MultipleCheckbox ? MultipleCheckbox
: MultipleChoice; : MultipleChoice;
...@@ -89,7 +108,7 @@ const AllAccessQuestionnaire: FC = () => { ...@@ -89,7 +108,7 @@ const AllAccessQuestionnaire: FC = () => {
key={`allAccessQn${i}`} key={`allAccessQn${i}`}
questionNumber={i + 1} questionNumber={i + 1}
questionLabel={question.questionLabel} questionLabel={question.questionLabel}
totalQuestions={allAccessQuestions.length} totalQuestions={questions.length}
helperText={question.helperText} helperText={question.helperText}
choices={question.choiceList.map((choice, choiceId) => ({ choices={question.choiceList.map((choice, choiceId) => ({
label: choice, label: choice,
...@@ -101,8 +120,6 @@ const AllAccessQuestionnaire: FC = () => { ...@@ -101,8 +120,6 @@ const AllAccessQuestionnaire: FC = () => {
); );
}), }),
]} ]}
onFinish={onSubmit}
isLoading={isSubmitting}
/> />
); );
}; };
......
...@@ -288,6 +288,7 @@ export const convertPayload = ( ...@@ -288,6 +288,7 @@ export const convertPayload = (
age: parseInt(values.age, 10), age: parseInt(values.age, 10),
height: parseInt(values.height, 10), height: parseInt(values.height, 10),
weight: parseInt(values.weight, 10), weight: parseInt(values.weight, 10),
special_condition: values.gender === 1 ? 1 : values.special_condition,
health_problem: health_problem:
values.health_problem.length === 0 ? [1] : values.health_problem, values.health_problem.length === 0 ? [1] : values.health_problem,
}); });
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