Skip to content

Commit 975cbc4

Browse files
authored
Filter saved data to those present in the task sequence (#2489)
* Filter only to data in task sequence * Add DataCollectionViewModelTest * Add tests for conditional task rendering and hidden task saving logic * Rename hidden to conditional * Formatting --------- Co-authored-by: Sufyan Abbasi <[email protected]>
1 parent 9e2dfff commit 975cbc4

File tree

3 files changed

+126
-8
lines changed

3 files changed

+126
-8
lines changed

ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,10 @@ internal constructor(
256256
}
257257

258258
private fun getDeltas(): List<ValueDelta> =
259-
data.map { (task, value) -> ValueDelta(task.id, task.type, value) }
259+
// Filter deltas to valid tasks.
260+
data
261+
.filter { (task) -> task in getTaskSequence() }
262+
.map { (task, value) -> ValueDelta(task.id, task.type, value) }
260263

261264
/** Persists the changes locally and enqueues a worker to sync with remote datastore. */
262265
private fun saveChanges(deltas: List<ValueDelta>) {

ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt

Lines changed: 116 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ import com.google.android.ground.R
2222
import com.google.android.ground.capture
2323
import com.google.android.ground.domain.usecases.survey.ActivateSurveyUseCase
2424
import com.google.android.ground.launchFragmentWithNavController
25+
import com.google.android.ground.model.submission.MultipleChoiceTaskData
2526
import com.google.android.ground.model.submission.TextTaskData
2627
import com.google.android.ground.model.submission.ValueDelta
28+
import com.google.android.ground.model.task.Condition
29+
import com.google.android.ground.model.task.Expression
30+
import com.google.android.ground.model.task.MultipleChoice
31+
import com.google.android.ground.model.task.Option
2732
import com.google.android.ground.model.task.Task
2833
import com.google.android.ground.persistence.local.room.converter.SubmissionDeltasConverter
2934
import com.google.android.ground.repository.SubmissionRepository
@@ -35,6 +40,7 @@ import com.sharedtest.persistence.remote.FakeRemoteDataStore
3540
import dagger.hilt.android.testing.BindValue
3641
import dagger.hilt.android.testing.HiltAndroidTest
3742
import javax.inject.Inject
43+
import kotlinx.collections.immutable.persistentListOf
3844
import kotlinx.coroutines.ExperimentalCoroutinesApi
3945
import kotlinx.coroutines.test.advanceUntilIdle
4046
import org.junit.Test
@@ -134,7 +140,7 @@ class DataCollectionFragmentTest : BaseHiltTest() {
134140
runner()
135141
.inputText(TASK_1_RESPONSE)
136142
.clickNextButton()
137-
.inputText(TASK_2_RESPONSE)
143+
.selectMultipleChoiceOption(TASK_2_OPTION_LABEL)
138144
.clickPreviousButton()
139145

140146
// Both deletion and creating happens twice as we do it on every previous/next step
@@ -187,7 +193,7 @@ class DataCollectionFragmentTest : BaseHiltTest() {
187193
runner()
188194
.validateTextIsDisplayed(TASK_1_RESPONSE)
189195
.clickNextButton()
190-
.validateTextIsDisplayed(TASK_2_RESPONSE)
196+
.validateTextIsDisplayed(TASK_2_OPTION_LABEL)
191197
}
192198

193199
@Test
@@ -197,7 +203,7 @@ class DataCollectionFragmentTest : BaseHiltTest() {
197203
.clickNextButton()
198204
.validateTextIsNotDisplayed(TASK_1_NAME)
199205
.validateTextIsDisplayed(TASK_2_NAME)
200-
.inputText(TASK_2_RESPONSE)
206+
.selectMultipleChoiceOption(TASK_2_OPTION_LABEL)
201207
.clickDoneButton() // Click "done" on final task
202208

203209
verify(submissionRepository)
@@ -216,6 +222,60 @@ class DataCollectionFragmentTest : BaseHiltTest() {
216222
verify(submissionRepository, times(1)).deleteDraftSubmission()
217223
}
218224

225+
@Test
226+
fun `Clicking done after triggering conditional task saves task data`() = runWithTestDispatcher {
227+
runner()
228+
.inputText(TASK_1_RESPONSE)
229+
.clickNextButton()
230+
.validateTextIsDisplayed(TASK_2_NAME)
231+
// Select the option to unhide the conditional task.
232+
.selectMultipleChoiceOption(TASK_2_OPTION_CONDITIONAL_LABEL)
233+
// TODO(#2394): Next button should be rendered here.
234+
.clickDoneButton()
235+
// Conditional task is rendered.
236+
.validateTextIsDisplayed(TASK_CONDITIONAL_NAME)
237+
.inputText(TASK_CONDITIONAL_RESPONSE)
238+
.clickNextButton()
239+
240+
verify(submissionRepository)
241+
.saveSubmission(eq(SURVEY.id), eq(LOCATION_OF_INTEREST.id), capture(deltaCaptor))
242+
243+
// Conditional task data is submitted.
244+
listOf(TASK_1_VALUE_DELTA, TASK_2_CONDITIONAL_VALUE_DELTA, TASK_CONDITIONAL_VALUE_DELTA)
245+
.forEach { value -> assertThat(deltaCaptor.value).contains(value) }
246+
}
247+
248+
@Test
249+
fun `Clicking done after editing conditional task state doesn't save inputted conditional task`() =
250+
runWithTestDispatcher {
251+
runner()
252+
.inputText(TASK_1_RESPONSE)
253+
.clickNextButton()
254+
.validateTextIsDisplayed(TASK_2_NAME)
255+
// Select the option to unhide the conditional task.
256+
.selectMultipleChoiceOption(TASK_2_OPTION_CONDITIONAL_LABEL)
257+
// TODO(#2394): Next button should be rendered here.
258+
.clickDoneButton()
259+
.validateTextIsDisplayed(TASK_CONDITIONAL_NAME)
260+
// Input a value, then go back to hide the task again.
261+
.inputText(TASK_CONDITIONAL_RESPONSE)
262+
.clickPreviousButton()
263+
.validateTextIsDisplayed(TASK_2_NAME)
264+
// Unselect the option to hide the conditional task.
265+
.selectMultipleChoiceOption(TASK_2_OPTION_CONDITIONAL_LABEL)
266+
.selectMultipleChoiceOption(TASK_2_OPTION_LABEL)
267+
.clickDoneButton()
268+
.validateTextIsNotDisplayed(TASK_CONDITIONAL_NAME)
269+
270+
verify(submissionRepository)
271+
.saveSubmission(eq(SURVEY.id), eq(LOCATION_OF_INTEREST.id), capture(deltaCaptor))
272+
273+
// Conditional task data is not submitted.
274+
listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA).forEach { value ->
275+
assertThat(deltaCaptor.value).contains(value)
276+
}
277+
}
278+
219279
private fun setupSubmission() = runWithTestDispatcher {
220280
whenever(submissionRepository.createSubmission(SURVEY.id, LOCATION_OF_INTEREST.id))
221281
.thenReturn(SUBMISSION)
@@ -258,14 +318,63 @@ class DataCollectionFragmentTest : BaseHiltTest() {
258318

259319
private const val TASK_ID_2 = "2"
260320
const val TASK_2_NAME = "task 2"
261-
private const val TASK_2_RESPONSE = "response 2"
262-
private val TASK_2_VALUE = TextTaskData.fromString(TASK_2_RESPONSE)
263-
private val TASK_2_VALUE_DELTA = ValueDelta(TASK_ID_2, Task.Type.TEXT, TASK_2_VALUE)
321+
private const val TASK_2_OPTION = "option 1"
322+
private const val TASK_2_OPTION_LABEL = "Option 1"
323+
private const val TASK_2_OPTION_CONDITIONAL = "option 2"
324+
private const val TASK_2_OPTION_CONDITIONAL_LABEL = "Option 2"
325+
private val TASK_2_MULTIPLE_CHOICE =
326+
MultipleChoice(
327+
persistentListOf(
328+
Option(TASK_2_OPTION, "code1", TASK_2_OPTION_LABEL),
329+
Option(TASK_2_OPTION_CONDITIONAL, "code2", TASK_2_OPTION_CONDITIONAL_LABEL),
330+
),
331+
MultipleChoice.Cardinality.SELECT_MULTIPLE,
332+
)
333+
private val TASK_2_VALUE =
334+
MultipleChoiceTaskData.fromList(TASK_2_MULTIPLE_CHOICE, listOf(TASK_2_OPTION))
335+
private val TASK_2_CONDITIONAL_VALUE =
336+
MultipleChoiceTaskData.fromList(TASK_2_MULTIPLE_CHOICE, listOf(TASK_2_OPTION_CONDITIONAL))
337+
private val TASK_2_VALUE_DELTA = ValueDelta(TASK_ID_2, Task.Type.MULTIPLE_CHOICE, TASK_2_VALUE)
338+
private val TASK_2_CONDITIONAL_VALUE_DELTA =
339+
ValueDelta(TASK_ID_2, Task.Type.MULTIPLE_CHOICE, TASK_2_CONDITIONAL_VALUE)
340+
341+
private const val TASK_ID_CONDITIONAL = "conditional"
342+
const val TASK_CONDITIONAL_NAME = "conditional task"
343+
private const val TASK_CONDITIONAL_RESPONSE = "conditional response"
344+
private val TASK_CONDITIONAL_VALUE = TextTaskData.fromString(TASK_CONDITIONAL_RESPONSE)
345+
private val TASK_CONDITIONAL_VALUE_DELTA =
346+
ValueDelta(TASK_ID_CONDITIONAL, Task.Type.TEXT, TASK_CONDITIONAL_VALUE)
264347

265348
private val TASKS =
266349
listOf(
267350
Task(TASK_ID_1, 0, Task.Type.TEXT, TASK_1_NAME, true),
268-
Task(TASK_ID_2, 1, Task.Type.TEXT, TASK_2_NAME, true),
351+
Task(
352+
TASK_ID_2,
353+
1,
354+
Task.Type.MULTIPLE_CHOICE,
355+
TASK_2_NAME,
356+
true,
357+
multipleChoice = TASK_2_MULTIPLE_CHOICE,
358+
),
359+
Task(
360+
TASK_ID_CONDITIONAL,
361+
2,
362+
Task.Type.TEXT,
363+
TASK_CONDITIONAL_NAME,
364+
true,
365+
condition =
366+
Condition(
367+
Condition.MatchType.MATCH_ANY,
368+
expressions =
369+
listOf(
370+
Expression(
371+
Expression.ExpressionType.ANY_OF_SELECTED,
372+
TASK_ID_2,
373+
optionIds = setOf(TASK_2_OPTION_CONDITIONAL),
374+
)
375+
),
376+
),
377+
),
269378
)
270379

271380
private val JOB = FakeData.JOB.copy(tasks = TASKS.associateBy { it.id })

ground/src/test/java/com/google/android/ground/ui/datacollection/TaskFragmentRunner.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
2727
import androidx.compose.ui.test.onNodeWithText
2828
import androidx.compose.ui.test.performClick
2929
import androidx.test.espresso.Espresso.onView
30+
import androidx.test.espresso.action.ViewActions.click
3031
import androidx.test.espresso.action.ViewActions.typeText
3132
import androidx.test.espresso.assertion.ViewAssertions.matches
3233
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -74,6 +75,11 @@ class TaskFragmentRunner(
7475
return this
7576
}
7677

78+
internal fun selectMultipleChoiceOption(optionText: String): TaskFragmentRunner {
79+
onView(withText(optionText)).perform(click())
80+
return this
81+
}
82+
7783
internal fun validateTextIsDisplayed(text: String): TaskFragmentRunner {
7884
onView(withText(text)).check(matches(isDisplayed()))
7985
return this

0 commit comments

Comments
 (0)