Fakultas Ilmu Komputer UI

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

Implement Dietela Quiz form validation

parent 3dd7f403
module.exports = {
preset: 'react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'],
setupFiles: [
'./jestSetup.js',
'./node_modules/react-native-gesture-handler/jestSetup.js',
],
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$':
'identity-obj-proxy',
......
import { jest } from '@jest/globals';
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
......@@ -19,7 +19,7 @@ const App: FC = () => {
<ThemeProvider theme={theme}>
<NavigationContainer>
<Stack.Navigator
initialRouteName={ROUTES.home}
initialRouteName={ROUTES.initial}
screenOptions={screenOptions}>
{navigation.map((nav, i) => (
<Stack.Screen
......
......@@ -6,37 +6,43 @@ import { Text } from 'react-native';
describe('WizardContainer component', () => {
const components = [<Text>hai</Text>, <Text>hello</Text>, <Text>hei</Text>];
let currentStep = 1;
const props = {
components,
currentStep,
setCurrentStep: (v: typeof currentStep) => (currentStep = v),
onFinish: jest.fn(),
};
it('displays first component as default if step props is not given', () => {
const { getByText } = render(<WizardContainer {...props} />);
const haiText = getByText(/hai/i);
expect(haiText).toBeDefined();
expect(haiText).toBeTruthy();
});
it('can go back and forth when "Kembali" or "Lanjut" button is pressed', () => {
const { getByText } = render(<WizardContainer {...props} />);
const { getByText, rerender } = render(<WizardContainer {...props} />);
const nextButton = getByText(/Lanjut/i);
fireEvent.press(nextButton);
rerender(<WizardContainer {...props} currentStep={currentStep} />);
const helloText = getByText(/hello/i);
expect(helloText).toBeDefined();
expect(helloText).toBeTruthy();
const backButton = getByText(/Kembali/i);
expect(backButton).toBeDefined();
expect(backButton).toBeTruthy();
fireEvent.press(backButton);
rerender(<WizardContainer {...props} currentStep={currentStep} />);
const haiText = getByText(/hai/i);
expect(haiText).toBeDefined();
expect(haiText).toBeTruthy();
});
it('shows finish button in the last component', () => {
const { findByText } = render(
<WizardContainer {...props} step={components.length} />,
<WizardContainer {...props} currentStep={components.length} />,
);
const finishButton = findByText(/Selesai/i);
expect(finishButton).toBeDefined();
expect(finishButton).toBeTruthy();
});
});
import React, { FC, useState, useEffect } from 'react';
import React, { FC } from 'react';
import { ScrollView, View } from 'react-native';
import { Button } from 'react-native-elements';
import { colors } from 'styles';
......@@ -25,16 +25,11 @@ const NextButton: FC<NextButtonProps> = ({ goNext }) => (
const WizardContainer: FC<Props> = ({
components,
step,
currentStep,
setCurrentStep,
finishButtonLabel,
onFinish,
}) => {
const [currentStep, setCurrentStep] = useState(1);
useEffect(() => {
setCurrentStep(step ?? 1);
}, [step]);
const goBack = () => {
setCurrentStep(currentStep - 1);
};
......
......@@ -2,9 +2,10 @@ import { ReactNode } from 'react';
export interface Props {
components: ReactNode[];
step?: number;
currentStep: number;
finishButtonLabel?: string;
onFinish: () => void;
setCurrentStep: (_: number) => void;
}
export interface NextButtonProps {
......
......@@ -40,7 +40,7 @@ describe('MultipleCheckbox component', () => {
expect(value).toContain(2);
});
it('is able to unselect checkbox', async () => {
it('is able to unselect checkbox', () => {
let value: Array<string | number> = [2];
const { getByText } = render(
......@@ -56,4 +56,18 @@ describe('MultipleCheckbox component', () => {
fireEvent.press(getByText(/Choice 2/i));
expect(value).toStrictEqual([]);
});
it('shows red helper text when there is error message props', () => {
const { getByText } = render(
<MultipleCheckbox
{...props}
value={[]}
onChange={jest.fn()}
errorMessage="error"
/>,
);
const helperText = getByText(/Pilih semua yang berlaku/i);
expect(helperText).toBeTruthy();
});
});
......@@ -3,14 +3,15 @@ import { View } from 'react-native';
import { CheckBox, Text } from 'react-native-elements';
import { typographyStyles } from 'styles';
import { styles } from './styles';
import { Props, Choice } from './types';
import { styles } from '../MultipleChoice/styles';
import { Props, Choice } from '../MultipleChoice/types';
const MultipleCheckbox: FC<Props> = ({
questionNumber,
totalQuestions,
questionLabel,
helperText,
errorMessage,
choices,
value,
onChange,
......@@ -31,7 +32,12 @@ const MultipleCheckbox: FC<Props> = ({
<Text style={[typographyStyles.headingLarge, styles.spacing]}>
{questionLabel}
</Text>
<Text style={[typographyStyles.bodyMedium, styles.bigSpacing]}>
<Text
style={[
typographyStyles.bodyMedium,
styles.bigSpacing,
errorMessage ? styles.red : null,
]}>
{helperText || 'Pilih semua yang berlaku'}
</Text>
{choices.map((choice) => (
......
import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
spacing: {
marginBottom: 14,
},
bigSpacing: {
marginBottom: 24,
},
});
import { Choice } from '../MultipleChoice/types';
export type { Choice };
export interface Props {
questionNumber: number;
totalQuestions: number;
questionLabel: string;
helperText?: string;
choices: Choice[];
value: any;
onChange: (_: any) => void;
}
......@@ -33,4 +33,13 @@ describe('MultipleChoice component', () => {
fireEvent.press(getByText(/Choice 2/i));
expect(value).toEqual(2);
});
it('shows red helper text when there is error message props', () => {
const { getByText } = render(
<MultipleChoice {...props} errorMessage="error" />,
);
const helperText = getByText(/Pilih satu yang paling cocok/i);
expect(helperText).toBeTruthy();
});
});
......@@ -12,6 +12,7 @@ const MultipleChoice: FC<Props> = ({
totalQuestions,
questionLabel,
helperText,
errorMessage,
choices,
value,
onChange,
......@@ -21,9 +22,14 @@ const MultipleChoice: FC<Props> = ({
Pertanyaan {questionNumber} / {totalQuestions}
</Text>
<Text style={[typographyStyles.headingLarge, styles.spacing]}>
{questionLabel}
{questionLabel} <Text style={styles.red}>*</Text>
</Text>
<Text style={[typographyStyles.bodyMedium, styles.bigSpacing]}>
<Text
style={[
typographyStyles.bodyMedium,
styles.bigSpacing,
errorMessage ? styles.red : null,
]}>
{helperText || 'Pilih satu yang paling cocok'}
</Text>
{choices.map((choice) => (
......
......@@ -7,4 +7,7 @@ export const styles = StyleSheet.create({
bigSpacing: {
marginBottom: 24,
},
red: {
color: 'red',
},
});
......@@ -8,8 +8,8 @@ export interface Props {
totalQuestions: number;
questionLabel: string;
helperText?: string;
errorMessage?: any;
choices: Choice[];
value: any;
onChange: (_: any) => void;
onPress?: () => void;
}
......@@ -36,4 +36,14 @@ describe('RadioButtonGroup component', () => {
fireEvent.press(getByText(/Choice 2/i));
expect(value).toEqual(2);
});
it('shows error message when there is error message props', () => {
const errorMessage = 'error';
const { getByText } = render(
<RadioButtonGroup {...props} errorMessage={errorMessage} />,
);
const errorText = getByText(errorMessage);
expect(errorText).toBeTruthy();
});
});
import React, { FC } from 'react';
import { View } from 'react-native';
import { CheckBox, CheckBoxProps } from 'react-native-elements';
import { Text, CheckBox, CheckBoxProps } from 'react-native-elements';
import { styles } from './styles';
import FormLabel from '../FormLabel';
import { RadioButtonGroupProps } from './types';
import { typographyStyles } from 'styles';
const RadioButton: FC<CheckBoxProps> = (props) => (
<CheckBox {...props} checkedIcon="dot-circle-o" uncheckedIcon="circle-o" />
......@@ -14,6 +15,7 @@ export const RadioButtonGroup: FC<RadioButtonGroupProps> = ({
choices,
label,
required,
errorMessage,
value,
onChange,
}) => (
......@@ -30,6 +32,9 @@ export const RadioButtonGroup: FC<RadioButtonGroupProps> = ({
/>
))}
</View>
{errorMessage ? (
<Text style={[typographyStyles.caption, styles.red]}>{errorMessage}</Text>
) : null}
</View>
);
......
......@@ -10,4 +10,7 @@ export const styles = StyleSheet.create({
radioButton: {
flex: 0.48,
},
red: {
color: 'red',
},
});
......@@ -3,6 +3,7 @@ import { Choice } from '../MultipleChoice/types';
export interface RadioButtonGroupProps extends FormLabelProps {
choices: Choice[];
errorMessage?: any;
value: any;
onChange: (_: any) => void;
}
......@@ -2,4 +2,5 @@ import { InputProps } from 'react-native-elements';
export interface Props extends InputProps {
required?: boolean;
errorMessage?: any;
}
import { FormikConfig, useFormik } from 'formik';
import { useState } from 'react';
const useForm = <T>(formConfig: FormikConfig<T>) => {
const [shouldValidate, setShouldValidate] = useState(false);
const {
handleChange,
handleBlur,
setFieldValue,
values,
errors,
validateForm,
...methods
} = useFormik(formConfig);
const getTextInputProps = (name: keyof T) => ({
onChangeText: handleChange(name),
onBlur: handleBlur(name),
value: values[name],
errorMessage: errors[name],
} = useFormik({
validateOnChange: shouldValidate,
validateOnBlur: shouldValidate,
...formConfig,
});
const getFormFieldProps = (name: string) => ({
onChange: (value: any) => setFieldValue(name, value),
value: values[name as keyof T],
});
const getTextInputProps = (fieldName: string) => {
const name = fieldName as keyof T;
return {
onChangeText: handleChange(name),
onBlur: handleBlur(name),
value: `${values[name]}`,
errorMessage: errors[name],
};
};
const getFormFieldProps = (fieldName: string) => {
const name = fieldName as keyof T;
return {
onChange: (value: any) => setFieldValue(fieldName, value),
value: values[name],
errorMessage: errors[name],
};
};
const getFirstErrorIndex = (err: any = errors) =>
Object.keys(formConfig.initialValues).findIndex(
(name) => name === Object.keys(err)[0],
);
const validate = async () => {
setShouldValidate(true);
const error = await validateForm();
return {
isValid: Object.keys(error).length === 0,
error,
};
};
return {
getTextInputProps,
getFormFieldProps,
getFirstErrorIndex,
errors,
values,
validateForm: validate,
...methods,
};
};
......
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