diff --git a/catalog/src/main/assets/behavior_questionnaire_constraint.json b/catalog/src/main/assets/behavior_questionnaire_constraint.json new file mode 100644 index 0000000000..a592ccb466 --- /dev/null +++ b/catalog/src/main/assets/behavior_questionnaire_constraint.json @@ -0,0 +1,86 @@ +{ + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "1", + "text": "Password", + "type": "string", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-display-category", + "code": "instructions" + } + ] + } + } + ], + "linkId": "1.1", + "text": "Fill the password first", + "type": "display" + } + ] + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-constraint", + "extension": [ + { + "url": "key", + "valueId": "constraint-1" + }, + { + "url": "requirements", + "valueString": "Confirm password field must have the same value as password field" + }, + { + "url": "severity", + "valueCode": "error" + }, + { + "url": "expression", + "valueString": "%context.answer.value = %resource.descendants().where(linkId='1').answer.value" + }, + { + "url": "human", + "valueString": "Password does not match" + }, + { + "url": "location", + "valueString": "1" + } + ] + } + ], + "linkId": "2", + "text": "Confirm password", + "type": "string", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-display-category", + "code": "instructions" + } + ] + } + } + ], + "linkId": "2.1", + "text": "Show error message if confirm password does not match with password", + "type": "display" + } + ] + } + ] +} \ No newline at end of file diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt index f7e7f10232..e5cbe1618d 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt @@ -63,6 +63,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica R.string.behavior_name_dynamic_question_text, "behavior_dynamic_question_text.json", ), + QUESTIONNAIRE_CONSTRAINT( + R.drawable.ic_rule, + R.string.behavior_name_questionnaire_constraint, + "behavior_questionnaire_constraint.json", + ), } fun isBehavior(context: Context, title: String) = diff --git a/catalog/src/main/res/drawable/ic_rule.xml b/catalog/src/main/res/drawable/ic_rule.xml new file mode 100644 index 0000000000..5f9363d680 --- /dev/null +++ b/catalog/src/main/res/drawable/ic_rule.xml @@ -0,0 +1,12 @@ + + + diff --git a/catalog/src/main/res/layout/behavior_list_fragment.xml b/catalog/src/main/res/layout/behavior_list_fragment.xml index f0f2979d60..0d06e5cb0c 100644 --- a/catalog/src/main/res/layout/behavior_list_fragment.xml +++ b/catalog/src/main/res/layout/behavior_list_fragment.xml @@ -11,6 +11,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/component_horizontal_margin" + android:layout_marginBottom="@dimen/bottom_navigation_view_height" /> diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml index 5509e201a6..8934420572 100644 --- a/catalog/src/main/res/values/strings.xml +++ b/catalog/src/main/res/values/strings.xml @@ -52,6 +52,9 @@ Dynamic question text + Questionnaire constraint Initial Value Type?, - ): Result - - /** - * The validation result containing whether the answer is valid and any error message if it is not - * valid. - */ - data class Result(val isValid: Boolean, val errorMessage: String?) + ): ConstraintValidator.Result } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt index 2d8397b912..0bf32d1a06 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt @@ -50,7 +50,7 @@ internal open class AnswerExtensionConstraintValidator( answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, context: Context, expressionEvaluator: suspend (Expression) -> Type?, - ): AnswerConstraintValidator.Result { + ): ConstraintValidator.Result { if (questionnaireItem.hasExtension(url)) { val extension = questionnaireItem.getExtensionByUrl(url) val extensionValue = @@ -61,12 +61,12 @@ internal open class AnswerExtensionConstraintValidator( if ( extensionValue.hasValue() && answer.value.hasValue() && predicate(extensionValue, answer) ) { - return AnswerConstraintValidator.Result( + return ConstraintValidator.Result( false, messageGenerator(extensionValue, context), ) } } - return AnswerConstraintValidator.Result(true, null) + return ConstraintValidator.Result(true, null) } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ConstraintItemExtensionValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ConstraintItemExtensionValidator.kt new file mode 100644 index 0000000000..940fb0d73f --- /dev/null +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ConstraintItemExtensionValidator.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2022-2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.datacapture.validation + +import android.content.Context +import com.google.android.fhir.datacapture.extensions.ConstraintSeverityTypes +import com.google.android.fhir.datacapture.extensions.EXTENSION_QUESTIONNAIRE_CONSTRAINT_EXPRESSION +import com.google.android.fhir.datacapture.extensions.EXTENSION_QUESTIONNAIRE_CONSTRAINT_HUMAN +import com.google.android.fhir.datacapture.extensions.EXTENSION_QUESTIONNAIRE_CONSTRAINT_SEVERITY +import com.google.android.fhir.datacapture.extensions.EXTENSION_QUESTIONNAIRE_CONSTRAINT_URL +import com.google.android.fhir.datacapture.extensions.asStringValue +import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator +import com.google.android.fhir.datacapture.fhirpath.convertToBoolean +import org.hl7.fhir.r4.model.CodeType +import org.hl7.fhir.r4.model.Expression +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent + +/** + * TODO: Add constraint support for global case, create a separate validator, + * https://github.com/google/android-fhir/issues/2479 + */ +internal class ConstraintItemExtensionValidator( + private val expressionEvaluator: ExpressionEvaluator, +) : QuestionnaireResponseItemConstraintValidator { + override suspend fun validate( + questionnaireItem: QuestionnaireItemComponent, + questionnaireResponseItem: QuestionnaireResponseItemComponent, + context: Context, + ): List { + return questionnaireItem.extension + .filter { extension -> + /** + * TODO: Add constraint support for warning case, update the [ConstraintValidator.Result] + * data class to also include warning state, + * https://github.com/google/android-fhir/issues/2480 + */ + extension.url == EXTENSION_QUESTIONNAIRE_CONSTRAINT_URL && + (extension.getExtensionByUrl(EXTENSION_QUESTIONNAIRE_CONSTRAINT_SEVERITY).value + as CodeType) + .valueAsString == ConstraintSeverityTypes.ERROR.code + } + .map { extension -> + val expression = + Expression().apply { + language = "text/fhirpath" + expression = + extension + .getExtensionByUrl(EXTENSION_QUESTIONNAIRE_CONSTRAINT_EXPRESSION) + .value + .asStringValue() + } + val isValid = + expressionEvaluator + .evaluateExpression( + questionnaireItem, + questionnaireResponseItem, + expression, + ) + .let { convertToBoolean(it) } + if (isValid) { + ConstraintValidator.Result(true, null) + } else { + val errorMessage = + extension + .getExtensionByUrl(EXTENSION_QUESTIONNAIRE_CONSTRAINT_HUMAN) + .value + .asStringValue() + ConstraintValidator.Result(false, errorMessage) + } + } + } +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ConstraintValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ConstraintValidator.kt new file mode 100644 index 0000000000..7d1ae69979 --- /dev/null +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ConstraintValidator.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022-2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.datacapture.validation + +/** Validator base interface. */ +internal interface ConstraintValidator { + + /** + * The validation result containing whether the response item is valid and any error message if it + * is not valid. + */ + data class Result(val isValid: Boolean, val errorMessage: String?) +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt index 0d3fb6aa55..51e35674bb 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt @@ -35,18 +35,18 @@ internal object MaxLengthValidator : AnswerConstraintValidator { answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, context: Context, expressionEvaluator: suspend (Expression) -> Type?, - ): AnswerConstraintValidator.Result { + ): ConstraintValidator.Result { if ( questionnaireItem.hasMaxLength() && answer.value.isPrimitive && answer.value.asStringValue().length > questionnaireItem.maxLength ) { - return AnswerConstraintValidator.Result( + return ConstraintValidator.Result( false, "The maximum number of characters that are permitted in the answer is: " + questionnaireItem.maxLength, ) } - return AnswerConstraintValidator.Result(true, null) + return ConstraintValidator.Result(true, null) } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemConstraintValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemConstraintValidator.kt index c2cb275333..828a16af84 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemConstraintValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemConstraintValidator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,26 +24,20 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse * Validates [QuestionnaireResponse.QuestionnaireResponseItemComponent] against a particular * constraint. */ -internal interface QuestionnaireResponseItemConstraintValidator { +internal interface QuestionnaireResponseItemConstraintValidator : ConstraintValidator { /** - * Validates that [answers] satisfy a particular constraint of the [questionnaireItem] according - * to the [structured data capture implementation guide] + * Validates that [questionnaireResponseItem] satisfy a particular constraint of the + * [questionnaireItem] according to the [structured data capture implementation guide] * (http://build.fhir.org/ig/HL7/sdc/behavior.html). * - * This does not validate the consistency between the structure of the [answers] and their - * descendants and that of the [questionnaireItem] and its descendants. + * This does not validate the consistency between the structure of the [questionnaireResponseItem] + * and their descendants and that of the [questionnaireItem] and its descendants. * * [Learn more](https://www.hl7.org/fhir/questionnaireresponse.html#link). */ - fun validate( + suspend fun validate( questionnaireItem: Questionnaire.QuestionnaireItemComponent, - answers: List, + questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent, context: Context, - ): Result - - /** - * The validation result containing whether the response item is valid and any error message if it - * is not valid. - */ - data class Result(val isValid: Boolean, val errorMessage: String?) + ): List } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt index 7a386e48b2..bdd2fbea78 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt @@ -18,17 +18,19 @@ package com.google.android.fhir.datacapture.validation import android.content.Context import com.google.android.fhir.datacapture.extensions.isHidden -import org.hl7.fhir.r4.model.Expression +import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse -import org.hl7.fhir.r4.model.Type -internal object QuestionnaireResponseItemValidator { +internal class QuestionnaireResponseItemValidator( + val expressionEvaluator: ExpressionEvaluator, +) { /** Validators for [QuestionnaireResponse.QuestionnaireResponseItemComponent]. */ private val questionnaireResponseItemConstraintValidators = listOf( RequiredValidator, + ConstraintItemExtensionValidator(expressionEvaluator), ) /** Validators for [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent]. */ @@ -42,23 +44,28 @@ internal object QuestionnaireResponseItemValidator { RegexValidator, ) - /** Validates [answers] contains valid answer(s) to [questionnaireItem]. */ + /** Validates [questionnaireResponseItem] contains valid answer(s) to [questionnaireItem]. */ suspend fun validate( questionnaireItem: Questionnaire.QuestionnaireItemComponent, - answers: List, + questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent, context: Context, - expressionEvaluator: suspend (Expression) -> Type?, ): ValidationResult { if (questionnaireItem.isHidden) return NotValidated val questionnaireResponseItemConstraintValidationResult = - questionnaireResponseItemConstraintValidators.map { - it.validate(questionnaireItem, answers, context) + questionnaireResponseItemConstraintValidators.flatMap { + it.validate(questionnaireItem, questionnaireResponseItem, context) } val questionnaireResponseItemAnswerConstraintValidationResult = answerConstraintValidators.flatMap { validator -> - answers.map { answer -> - validator.validate(questionnaireItem, answer, context, expressionEvaluator) + questionnaireResponseItem.answer.map { answer -> + validator.validate(questionnaireItem, answer, context) { + expressionEvaluator.evaluateExpressionValue( + questionnaireItem, + questionnaireResponseItem, + it, + ) + } } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt index ec449b36d9..fce6d84d0e 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt @@ -71,25 +71,31 @@ object QuestionnaireResponseValidator { "Mismatching Questionnaire ${questionnaire.url} and QuestionnaireResponse (for Questionnaire ${questionnaireResponse.questionnaire})" } - val linkIdToValidationResultMap = mutableMapOf>() - - validateQuestionnaireResponseItems( - questionnaire.item, - questionnaireResponse.item, - context, + val enablementEvaluator = EnablementEvaluator( questionnaire, questionnaireResponse, questionnaireItemParentMap, launchContextMap, xFhirQueryResolver, - ), - ExpressionEvaluator( - questionnaire, - questionnaireResponse, - questionnaireItemParentMap, - launchContextMap, - ), + ) + val questionnaireResponseItemValidator = + QuestionnaireResponseItemValidator( + ExpressionEvaluator( + questionnaire, + questionnaireResponse, + questionnaireItemParentMap, + launchContextMap, + ), + ) + val linkIdToValidationResultMap = mutableMapOf>() + + validateQuestionnaireResponseItems( + questionnaire.item, + questionnaireResponse.item, + context, + enablementEvaluator, + questionnaireResponseItemValidator, linkIdToValidationResultMap, ) @@ -101,7 +107,7 @@ object QuestionnaireResponseValidator { questionnaireResponseItemList: List, context: Context, enablementEvaluator: EnablementEvaluator, - expressionEvaluator: ExpressionEvaluator, + questionnaireResponseItemValidator: QuestionnaireResponseItemValidator, linkIdToValidationResultMap: MutableMap>, ): Map> { val questionnaireItemListIterator = questionnaireItemList.iterator() @@ -129,7 +135,7 @@ object QuestionnaireResponseValidator { questionnaireResponseItem, context, enablementEvaluator, - expressionEvaluator, + questionnaireResponseItemValidator, linkIdToValidationResultMap, ) } @@ -142,7 +148,7 @@ object QuestionnaireResponseValidator { questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent, context: Context, enablementEvaluator: EnablementEvaluator, - expressionEvaluator: ExpressionEvaluator, + questionnaireResponseItemValidator: QuestionnaireResponseItemValidator, linkIdToValidationResultMap: MutableMap>, ): Map> { when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) { @@ -156,7 +162,7 @@ object QuestionnaireResponseValidator { questionnaireResponseItem.item, context, enablementEvaluator, - expressionEvaluator, + questionnaireResponseItemValidator, linkIdToValidationResultMap, ) else -> { @@ -170,24 +176,18 @@ object QuestionnaireResponseValidator { it.item, context, enablementEvaluator, - expressionEvaluator, + questionnaireResponseItemValidator, linkIdToValidationResultMap, ) } linkIdToValidationResultMap[questionnaireItem.linkId] = mutableListOf() linkIdToValidationResultMap[questionnaireItem.linkId]?.add( - QuestionnaireResponseItemValidator.validate( + questionnaireResponseItemValidator.validate( questionnaireItem, - questionnaireResponseItem.answer, + questionnaireResponseItem, context, - ) { - expressionEvaluator.evaluateExpressionValue( - questionnaireItem, - questionnaireResponseItem, - it, - ) - }, + ), ) } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RequiredValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RequiredValidator.kt index 87cad9c630..fabdeb9a45 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RequiredValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RequiredValidator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,17 +22,19 @@ import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse internal object RequiredValidator : QuestionnaireResponseItemConstraintValidator { - override fun validate( + override suspend fun validate( questionnaireItem: Questionnaire.QuestionnaireItemComponent, - answers: List, + questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent, context: Context, - ): QuestionnaireResponseItemConstraintValidator.Result { - if (!questionnaireItem.required || answers.any { it.hasValue() }) { - return QuestionnaireResponseItemConstraintValidator.Result(true, null) + ): List { + if (!questionnaireItem.required || questionnaireResponseItem.answer.any { it.hasValue() }) { + return listOf(ConstraintValidator.Result(true, null)) } - return QuestionnaireResponseItemConstraintValidator.Result( - false, - context.getString(R.string.required_constraint_validation_error_msg), + return listOf( + ConstraintValidator.Result( + false, + context.getString(R.string.required_constraint_validation_error_msg), + ), ) } } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt index a0219d3f27..47b71e1553 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt @@ -19,12 +19,14 @@ package com.google.android.fhir.datacapture.validation import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider +import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.IntegerType import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent import org.hl7.fhir.r4.model.StringType import org.junit.Before import org.junit.Test @@ -72,15 +74,26 @@ class QuestionnaireResponseItemValidatorTest { value = IntegerType(275) }, ) + val questionnaire = Questionnaire().apply { addItem(questionnaireItem) } + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponseItemComponent().apply { answer = answers }, + ) + } + val expressionEvaluator = + ExpressionEvaluator( + questionnaire, + questionnaireResponse, + ) val validationResult = - QuestionnaireResponseItemValidator.validate( - questionnaireItem, - answers, - context, - ) { - TestExpressionValueEvaluator.evaluate(questionnaireItem, it) - } + QuestionnaireResponseItemValidator(expressionEvaluator) + .validate( + questionnaire.item.first(), + questionnaireResponse.item.first(), + context, + ) assertThat(validationResult).isEqualTo(Valid) } @@ -116,15 +129,26 @@ class QuestionnaireResponseItemValidatorTest { value = IntegerType(250) }, ) + val questionnaire = Questionnaire().apply { addItem(questionnaireItem) } + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponseItemComponent().apply { answer = answers }, + ) + } + val expressionEvaluator = + ExpressionEvaluator( + questionnaire, + questionnaireResponse, + ) val validationResult = - QuestionnaireResponseItemValidator.validate( - questionnaireItem, - answers, - context, - ) { - TestExpressionValueEvaluator.evaluate(questionnaireItem, it) - } + QuestionnaireResponseItemValidator(expressionEvaluator) + .validate( + questionnaire.item.first(), + questionnaireResponse.item.first(), + context, + ) assertThat(validationResult).isInstanceOf(Invalid::class.java) val invalidValidationResult = validationResult as Invalid @@ -140,15 +164,27 @@ class QuestionnaireResponseItemValidatorTest { required = true } val answers = listOf() + val questionnaire = Questionnaire().apply { addItem(questionnaireItem) } + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponseItemComponent().apply { answer = answers }, + ) + } + + val expressionEvaluator = + ExpressionEvaluator( + questionnaire, + questionnaireResponse, + ) val validationResult = - QuestionnaireResponseItemValidator.validate( - questionnaireItem, - answers, - context, - ) { - TestExpressionValueEvaluator.evaluate(questionnaireItem, it) - } + QuestionnaireResponseItemValidator(expressionEvaluator) + .validate( + questionnaire.item.first(), + questionnaireResponse.item.first(), + context, + ) assertThat(validationResult).isInstanceOf(Invalid::class.java) val invalidValidationResult = validationResult as Invalid diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RequiredValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RequiredValidatorTest.kt index 4f6b500cc4..cbb021d9b7 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RequiredValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RequiredValidatorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture.validation import android.os.Build import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.IntegerType import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -33,55 +34,71 @@ class RequiredValidatorTest { @Test fun shouldReturnValidResult() { - val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { required = true } - val response = - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = IntegerType(9) - }, - ) + runTest { + val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { required = true } + val questionnaireResponseItem = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = IntegerType(9) + }, + ) + } - val validationResult = - RequiredValidator.validate( - questionnaireItem, - response, - InstrumentationRegistry.getInstrumentation().context, - ) - assertThat(validationResult.isValid).isTrue() + val validationResult = + RequiredValidator.validate( + questionnaireItem, + questionnaireResponseItem, + InstrumentationRegistry.getInstrumentation().context, + ) + assertThat(validationResult.first().isValid).isTrue() + } } @Test fun shouldReturnInvalidResult() { - val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { required = true } + runTest { + val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { required = true } + val questionnaireResponseItem = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { answer = listOf() } - val validationResult = - RequiredValidator.validate( - questionnaireItem, - listOf(), - InstrumentationRegistry.getInstrumentation().context, - ) - assertThat(validationResult.isValid).isFalse() - assertThat(validationResult.errorMessage).isEqualTo("Missing answer for required field.") + val validationResult = + RequiredValidator.validate( + questionnaireItem, + questionnaireResponseItem, + InstrumentationRegistry.getInstrumentation().context, + ) + assertThat(validationResult.first().isValid).isFalse() + assertThat(validationResult.first().errorMessage) + .isEqualTo("Missing answer for required field.") + } } @Test fun noAnswerHasValue_shouldReturnInvalidResult() { - val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { required = true } - val response = - listOf( - // one answer with no value - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(), - // second answer with no value - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(), - ) + runTest { + val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { required = true } + val questionnaireResponseItem = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + answer = + listOf( + // one answer with no value + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(), + // second answer with no value + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(), + ) + } - val validationResult = - RequiredValidator.validate( - questionnaireItem, - response, - InstrumentationRegistry.getInstrumentation().context, - ) - assertThat(validationResult.isValid).isFalse() - assertThat(validationResult.errorMessage).isEqualTo("Missing answer for required field.") + val validationResult = + RequiredValidator.validate( + questionnaireItem, + questionnaireResponseItem, + InstrumentationRegistry.getInstrumentation().context, + ) + assertThat(validationResult.first().isValid).isFalse() + assertThat(validationResult.first().errorMessage) + .isEqualTo("Missing answer for required field.") + } } }