diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/DraftSubmissionDao.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/DraftSubmissionDao.kt index 03a286d8cf..495208660e 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/DraftSubmissionDao.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/DraftSubmissionDao.kt @@ -27,4 +27,6 @@ interface DraftSubmissionDao : BaseDao { suspend fun findById(draftSubmissionId: String): DraftSubmissionEntity? @Query("DELETE FROM draft_submission") fun delete() + + @Query("SELECT COUNT(*) FROM draft_submission") suspend fun countAll(): Int } diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt index 5c57aa86b7..df09e3038a 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt @@ -44,7 +44,6 @@ import com.google.android.ground.persistence.local.room.fields.MutationEntityTyp import com.google.android.ground.persistence.local.room.fields.UserDetails import com.google.android.ground.persistence.local.stores.LocalSubmissionStore import com.google.android.ground.util.Debug.logOnFailure -import com.google.firebase.crashlytics.FirebaseCrashlytics import javax.inject.Inject import javax.inject.Singleton import kotlinx.collections.immutable.toPersistentList @@ -211,9 +210,10 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore apply(mutation) enqueue(mutation) } catch (e: LocalDataStoreException) { - FirebaseCrashlytics.getInstance() - .log("Error enqueueing ${mutation.type} mutation for submission ${mutation.submissionId}") - FirebaseCrashlytics.getInstance().recordException(e) + Timber.e( + e, + "Error enqueueing ${mutation.type} mutation for submission ${mutation.submissionId}", + ) throw e } } @@ -269,4 +269,6 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore override suspend fun deleteDraftSubmissions() { draftSubmissionDao.delete() } + + override suspend fun countDraftSubmissions(): Int = draftSubmissionDao.countAll() } diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt index c3be233866..b5e86d5f5d 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt @@ -78,4 +78,7 @@ interface LocalSubmissionStore : LocalMutationStore> + @Inject lateinit var mutationRepository: MutationRepository + @Inject lateinit var submissionRepository: SubmissionRepository + @Inject lateinit var userRepository: UserRepository + lateinit var fragment: DataCollectionFragment - @Inject lateinit var uuidGenerator: OfflineUuidGenerator - lateinit var collectionId: String override fun setUp() = runBlocking { super.setUp() - collectionId = uuidGenerator.generateUuid() - setupSubmission() setupFragment() } @@ -123,20 +119,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { fun `Next click saves draft`() = runWithTestDispatcher { runner().inputText(TASK_1_RESPONSE).clickNextButton() - // Validate that previous drafts were cleared - verify(submissionRepository, times(1)).deleteDraftSubmission() - - // Validate that new draft was created - verify(submissionRepository, times(1)) - .saveDraftSubmission( - eq(JOB.id), - eq(LOCATION_OF_INTEREST.id), - eq(SURVEY.id), - capture(deltaCaptor), - eq(LOCATION_OF_INTEREST_NAME), - ) - - listOf(TASK_1_VALUE_DELTA).forEach { value -> assertThat(deltaCaptor.value).contains(value) } + assertDraftSaved(listOf(TASK_1_VALUE_DELTA)) } @Test @@ -147,20 +130,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { .selectMultipleChoiceOption(TASK_2_OPTION_LABEL) .clickPreviousButton() - // Both deletion and creating happens twice as we do it on every previous/next step - verify(submissionRepository, times(2)).deleteDraftSubmission() - verify(submissionRepository, times(2)) - .saveDraftSubmission( - eq(JOB.id), - eq(LOCATION_OF_INTEREST.id), - eq(SURVEY.id), - capture(deltaCaptor), - eq(LOCATION_OF_INTEREST_NAME), - ) - - listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA).forEach { value -> - assertThat(deltaCaptor.value).contains(value) - } + assertDraftSaved(listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA)) } @Test @@ -210,25 +180,19 @@ class DataCollectionFragmentTest : BaseHiltTest() { .selectMultipleChoiceOption(TASK_2_OPTION_LABEL) .clickDoneButton() // Click "done" on final task - verify(submissionRepository) - .saveSubmission( - eq(SURVEY.id), - eq(LOCATION_OF_INTEREST.id), - capture(deltaCaptor), - eq(collectionId), - ) - - listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA).forEach { value -> - assertThat(deltaCaptor.value).contains(value) - } + assertSubmissionSaved(listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA)) } @Test fun `Clicking back button on first task clears the draft and returns false`() = runWithTestDispatcher { - runner().pressBackButton(false) + runner() + .inputText(TASK_1_RESPONSE) + .clickNextButton() + .pressBackButton(true) + .pressBackButton(false) - verify(submissionRepository, times(1)).deleteDraftSubmission() + assertNoDraftSaved() } @Test @@ -245,17 +209,10 @@ class DataCollectionFragmentTest : BaseHiltTest() { .inputText(TASK_CONDITIONAL_RESPONSE) .clickDoneButton() - verify(submissionRepository) - .saveSubmission( - eq(SURVEY.id), - eq(LOCATION_OF_INTEREST.id), - capture(deltaCaptor), - eq(collectionId), - ) - // Conditional task data is submitted. - listOf(TASK_1_VALUE_DELTA, TASK_2_CONDITIONAL_VALUE_DELTA, TASK_CONDITIONAL_VALUE_DELTA) - .forEach { value -> assertThat(deltaCaptor.value).contains(value) } + assertSubmissionSaved( + listOf(TASK_1_VALUE_DELTA, TASK_2_CONDITIONAL_VALUE_DELTA, TASK_CONDITIONAL_VALUE_DELTA) + ) } @Test @@ -279,21 +236,69 @@ class DataCollectionFragmentTest : BaseHiltTest() { .clickDoneButton() .validateTextIsNotDisplayed(TASK_CONDITIONAL_NAME) - verify(submissionRepository) - .saveSubmission( - eq(SURVEY.id), - eq(LOCATION_OF_INTEREST.id), - capture(deltaCaptor), - eq(collectionId), - ) - // Conditional task data is not submitted. - listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA).forEach { value -> - assertThat(deltaCaptor.value).contains(value) - } + assertSubmissionSaved(listOf(TASK_1_VALUE_DELTA, TASK_2_VALUE_DELTA)) } + private suspend fun assertSubmissionSaved(valueDeltas: List) { + assertNoDraftSaved() + + val loiId = LOCATION_OF_INTEREST.id + + // Exactly 1 submission should be saved. + assertThat(submissionRepository.getPendingCreateCount(loiId)).isEqualTo(1) + + val testDate = Date() + val mutation = + mutationRepository + .getIncompleteUploads()[0] + .submissionMutation!! + .copy(clientTimestamp = testDate) // seed dummy test date + + assertThat(mutation) + .isEqualTo( + SubmissionMutation( + id = 1, + type = Mutation.Type.CREATE, + syncStatus = Mutation.SyncStatus.PENDING, + surveyId = SURVEY.id, + locationOfInterestId = loiId, + userId = USER.id, + clientTimestamp = testDate, + collectionId = "TEST UUID", + job = JOB, + submissionId = "TEST UUID", + deltas = valueDeltas, + ) + ) + } + + private suspend fun assertDraftSaved(valueDeltas: List) { + val draftId = submissionRepository.getDraftSubmissionsId() + assertThat(draftId).isNotEmpty() + + // Exactly 1 draft should be present always. + assertThat(submissionRepository.countDraftSubmissions()).isEqualTo(1) + assertThat(submissionRepository.getDraftSubmission(draftId, SURVEY)) + .isEqualTo( + DraftSubmission( + id = draftId, + jobId = JOB.id, + loiId = LOCATION_OF_INTEREST.id, + loiName = LOCATION_OF_INTEREST_NAME, + surveyId = SURVEY.id, + deltas = valueDeltas, + ) + ) + } + + private suspend fun assertNoDraftSaved() { + assertThat(submissionRepository.getDraftSubmissionsId()).isEmpty() + assertThat(submissionRepository.countDraftSubmissions()).isEqualTo(0) + } + private fun setupSubmission() = runWithTestDispatcher { + userRepository.saveUserDetails(USER) fakeRemoteDataStore.surveys = listOf(SURVEY) fakeRemoteDataStore.predefinedLois = listOf(LOCATION_OF_INTEREST) activateSurvey(SURVEY.id)