Fakultas Ilmu Komputer UI
Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
ppl-fasilkom-ui
2021
Kelas D
PT Gizi Sehat - Dietela
Dietela Mobile
Commits
a71cd96a
Commit
a71cd96a
authored
Apr 21, 2021
by
Wulan Mantiri
Browse files
Revise Dietela Quiz error validation logic
parent
d8a1113c
Changes
9
Hide whitespace changes
Inline
Side-by-side
.gitlab-ci.yml
View file @
a71cd96a
...
...
@@ -26,7 +26,7 @@ test:
before_script
:
-
yarn install
script
:
-
yarn test
--coverage --watchAll=false --verbose --collectCoverageFrom="src/**/*.tsx"
-
yarn test
artifacts
:
paths
:
-
coverage
...
...
src/components/core/WizardContainer/index.tsx
View file @
a71cd96a
...
...
@@ -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
>
...
...
src/components/core/WizardContainer/types.ts
View file @
a71cd96a
...
...
@@ -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
;
}
src/components/form/TextField/index.test.tsx
View file @
a71cd96a
...
...
@@ -7,4 +7,8 @@ describe('TextField component', () => {
it
(
'
renders correctly
'
,
()
=>
{
render
(<
TextField
/>);
});
it
(
'
renders correctly with error message
'
,
()
=>
{
render
(<
TextField
errorMessage
=
"error"
/>);
});
});
src/hooks/useApi/index.ts
View file @
a71cd96a
...
...
@@ -27,6 +27,7 @@ const useApi = <T>(
}
setIsLoading
(
false
);
};
fetchData
();
},
[
fetchApi
]);
...
...
src/hooks/useForm/index.ts
View file @
a71cd96a
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
,
...
...
src/scenes/questionnaire/AllAccessQuestionnaire/index.test.tsx
View file @
a71cd96a
...
...
@@ -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
(
/
Pri
a/i
);
fireEvent
.
press
(
maleChoice
);
const
fe
maleChoice
=
getByText
(
/
Wanit
a/i
);
fireEvent
.
press
(
fe
maleChoice
);
allAccessQuestions
.
forEach
(({
choiceList
})
=>
{
const
nextButton
=
getByText
(
/Lanjut/i
);
...
...
src/scenes/questionnaire/AllAccessQuestionnaire/index.tsx
View file @
a71cd96a
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
,
validateF
or
m
,
getFirstErrorIndex
,
isFieldErr
or
,
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
),
}))
}
/>,
...
allAccessQ
uestions
.
map
((
question
,
i
)
=>
{
...
q
uestions
.
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
=
{
allAccessQ
uestions
.
length
}
totalQuestions
=
{
q
uestions
.
length
}
helperText
=
{
question
.
helperText
}
choices
=
{
question
.
choiceList
.
map
((
choice
,
choiceId
)
=>
({
label
:
choice
,
...
...
@@ -101,8 +120,6 @@ const AllAccessQuestionnaire: FC = () => {
);
}),
]
}
onFinish
=
{
onSubmit
}
isLoading
=
{
isSubmitting
}
/>
);
};
...
...
src/scenes/questionnaire/AllAccessQuestionnaire/schema.ts
View file @
a71cd96a
...
...
@@ -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
,
});
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment