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