diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt index 7318bf0484..394c3c77d7 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt @@ -112,21 +112,17 @@ internal constructor( } } - fun isFirstPosition(taskId: String): Boolean = withReady { - taskSequenceHandler.isFirstPosition(taskId) - } - - fun isLastPosition(taskId: String): Boolean = withReady { - taskSequenceHandler.isLastPosition(taskId) - } + private fun isFirstPosition(taskId: String): Boolean = + withReadyOrNull { taskSequenceHandler.isFirstPosition(taskId) } ?: false - fun isLastPositionWithValue(task: Task, newValue: TaskData?): Boolean = withReady { - if (taskDataHandler.getData(task) == newValue) { - taskSequenceHandler.isLastPosition(task.id) - } else { - taskSequenceHandler.checkIfTaskIsLastWithValue(task.id to newValue) - } - } + private fun isLastPositionWithValue(task: Task, newValue: TaskData?): Boolean = + withReadyOrNull { + if (taskDataHandler.getData(task) == newValue) { + taskSequenceHandler.isLastPosition(task.id) + } else { + taskSequenceHandler.checkIfTaskIsLastWithValue(task.id to newValue) + } + } ?: false fun isAtFirstTask(): Boolean = withReady { taskSequenceHandler.isFirstPosition(it.currentTaskId) } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/ButtonActionState.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/ButtonActionState.kt similarity index 82% rename from app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/ButtonActionState.kt rename to app/src/main/java/org/groundplatform/android/ui/datacollection/components/ButtonActionState.kt index 73dc864a85..e3d9213be8 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/ButtonActionState.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/ButtonActionState.kt @@ -13,9 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.ui.datacollection.components.refactor - -import org.groundplatform.android.ui.datacollection.components.ButtonAction +package org.groundplatform.android.ui.datacollection.components data class ButtonActionState( val action: ButtonAction, diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskButton.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskButton.kt index 5d659c6315..4442545745 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskButton.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskButton.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,129 +15,95 @@ */ package org.groundplatform.android.ui.datacollection.components +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.groundplatform.android.model.submission.TaskData - -class TaskButton(initialAction: ButtonAction) { - - private lateinit var clickCallback: () -> Unit - - private var action: MutableState = mutableStateOf(initialAction) - private var enabled: MutableState = mutableStateOf(true) - private var hidden: MutableState = mutableStateOf(false) - - private var taskUpdatedCallback: ((button: TaskButton, taskData: TaskData?) -> Unit)? = null - - @Composable - fun CreateButton() { - if (!hidden.value) { - when (action.value.theme) { - ButtonAction.Theme.DARK_GREEN -> - Button(onClick = { clickCallback() }, enabled = enabled.value) { Content() } - ButtonAction.Theme.LIGHT_GREEN -> - FilledTonalButton(onClick = { clickCallback() }, enabled = enabled.value) { Content() } - ButtonAction.Theme.OUTLINED -> - OutlinedButton(onClick = { clickCallback() }, enabled = enabled.value) { Content() } - ButtonAction.Theme.TRANSPARENT -> - OutlinedButton( - border = null, - onClick = { clickCallback() }, - enabled = enabled.value, - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), - ) { - Content() - } +import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport +import org.groundplatform.android.ui.theme.AppTheme + +@Composable +fun TaskButton( + modifier: Modifier = Modifier, + state: ButtonActionState, + onClick: (ButtonAction) -> Unit, +) { + when (state.action.theme) { + ButtonAction.Theme.DARK_GREEN -> + Button(modifier = modifier, onClick = { onClick(state.action) }, enabled = state.isEnabled) { + Content(action = state.action) + } + ButtonAction.Theme.LIGHT_GREEN -> + FilledTonalButton( + modifier = modifier, + onClick = { onClick(state.action) }, + enabled = state.isEnabled, + ) { + Content(action = state.action) + } + ButtonAction.Theme.OUTLINED -> + OutlinedButton( + modifier = modifier, + onClick = { onClick(state.action) }, + enabled = state.isEnabled, + ) { + Content(action = state.action) + } + ButtonAction.Theme.TRANSPARENT -> + OutlinedButton( + modifier = modifier, + border = null, + onClick = { onClick(state.action) }, + enabled = state.isEnabled, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), + ) { + Content(action = state.action) } - } } +} - @Composable - private fun Content() { - // Icon - action.value.drawableId?.let { +@Composable +private fun Content(modifier: Modifier = Modifier, action: ButtonAction) { + when { + action.drawableId != null -> { Icon( - imageVector = ImageVector.vectorResource(id = it), - contentDescription = action.value.contentDescription?.let { resId -> stringResource(resId) }, + modifier = modifier, + imageVector = ImageVector.vectorResource(id = action.drawableId), + contentDescription = action.contentDescription?.let { resId -> stringResource(resId) }, ) } - - // Label - action.value.textId?.let { textId -> Text(text = stringResource(id = textId)) } - } - - /** Updates the `visibility` property button. */ - fun showIfTrue(result: Boolean): TaskButton = if (result) show() else hide() - - /** Updates the `isEnabled` property of button. */ - fun enableIfTrue(result: Boolean): TaskButton = if (result) enable() else disable() - - fun getAction(): ButtonAction = action.value - - fun done(): TaskButton { - action.value = ButtonAction.DONE - return this - } - - fun next(): TaskButton { - action.value = ButtonAction.NEXT - return this - } - - fun toggleDone(done: Boolean): TaskButton { - if (action.value == ButtonAction.NEXT && done) { - done() - } else if (action.value == ButtonAction.DONE && !done) { - next() + action.textId != null -> { + Text(modifier = modifier, text = stringResource(id = action.textId)) } - return this - } - - fun show(): TaskButton { - hidden.value = false - return this - } - - fun hide(): TaskButton { - hidden.value = true - return this - } - - fun enable(): TaskButton { - enabled.value = true - return this - } - - fun disable(): TaskButton { - enabled.value = false - return this - } - - /** Register a callback to be invoked when this view is clicked. */ - fun setOnClickListener(block: () -> Unit): TaskButton { - this.clickCallback = block - return this - } - - /** Register a callback to be invoked when [TaskData] is updated. */ - fun setOnValueChanged(block: (button: TaskButton, taskData: TaskData?) -> Unit): TaskButton { - this.taskUpdatedCallback = block - return this } +} - /** Must be called when a new [TaskData] is available. */ - fun onValueChanged(taskData: TaskData?) { - taskUpdatedCallback?.let { it(this, taskData) } +@Preview(showBackground = true) +@Composable +@ExcludeFromJacocoGeneratedReport +private fun TaskButtonAllPreview() { + AppTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ButtonAction.entries.forEach { action -> + TaskButton(state = ButtonActionState(action), onClick = {}) + } + } } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskFooter.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskFooter.kt similarity index 56% rename from app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskFooter.kt rename to app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskFooter.kt index aa8a63c51f..3cef30e6f2 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskFooter.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskFooter.kt @@ -13,34 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.ui.datacollection.components.refactor +package org.groundplatform.android.ui.datacollection.components import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport -import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.tasks.location.LocationAccuracyCard import org.groundplatform.android.ui.theme.AppTheme -@Suppress("UnusedPrivateMember") // To be implemented in the follow up PR @Composable fun TaskFooter( modifier: Modifier = Modifier, + headerCard: (@Composable () -> Unit)? = null, buttonActionStates: List, onButtonClicked: (ButtonAction) -> Unit, ) { - Row( - modifier = modifier.padding(24.dp).fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - buttonActionStates.forEach { state -> - if (state.isVisible) { - TaskButton(state = state, onClick = { onButtonClicked(state.action) }) + Column(modifier = modifier.padding(24.dp).fillMaxWidth()) { + if (headerCard != null) { + headerCard() + Spacer(Modifier.height(12.dp)) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + buttonActionStates.forEach { state -> + if (state.isVisible) { + TaskButton(state = state, onClick = { onButtonClicked(state.action) }) + } } } } @@ -49,10 +55,25 @@ fun TaskFooter( @Preview(showBackground = true) @Composable @ExcludeFromJacocoGeneratedReport -private fun TaskFooterPreview() { +private fun TaskFooterNoHeaderPreview() { val actions = listOf(ButtonAction.PREVIOUS, ButtonAction.UNDO, ButtonAction.REDO, ButtonAction.NEXT) AppTheme { TaskFooter(buttonActionStates = actions.map { ButtonActionState(it) }, onButtonClicked = {}) } } + +@Preview(showBackground = true) +@Composable +@ExcludeFromJacocoGeneratedReport +private fun TaskFooterWithHeaderPreview() { + val actions = + listOf(ButtonAction.PREVIOUS, ButtonAction.UNDO, ButtonAction.REDO, ButtonAction.NEXT) + AppTheme { + TaskFooter( + headerCard = { LocationAccuracyCard(onDismiss = {}) }, + buttonActionStates = actions.map { ButtonActionState(it) }, + onButtonClicked = {}, + ) + } +} diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskView.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskView.kt index d41bf277db..b5931060b5 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskView.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/TaskView.kt @@ -16,8 +16,8 @@ package org.groundplatform.android.ui.datacollection.components import android.view.View +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import org.groundplatform.android.databinding.TaskFragActionButtonsBinding import org.groundplatform.android.databinding.TaskFragWithCombinedHeaderBinding import org.groundplatform.android.databinding.TaskFragWithHeaderBinding import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel @@ -25,8 +25,8 @@ import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel /** Wrapper class for holding entire task's view (except toolbar). */ sealed interface TaskView { - /** Container for adding the action buttons for the task. */ - val actionButtonsContainer: TaskFragActionButtonsBinding + /** ComposeView for the action buttons. */ + val actionButtonsContainer: ComposeView /** Root-level view for the current task. */ val root: View @@ -59,7 +59,7 @@ data class TaskViewWithHeader(private val binding: TaskFragWithHeaderBinding) : data class TaskViewWithCombinedHeader(private val binding: TaskFragWithCombinedHeaderBinding) : TaskView { - override val actionButtonsContainer = binding.actionButtons + override val actionButtonsContainer: ComposeView = binding.actionButtons override val root = binding.root diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskButton.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskButton.kt deleted file mode 100644 index 628cf95acf..0000000000 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskButton.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2026 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 - * - * https://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 org.groundplatform.android.ui.datacollection.components.refactor - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport -import org.groundplatform.android.ui.datacollection.components.ButtonAction -import org.groundplatform.android.ui.theme.AppTheme - -@Composable -fun TaskButton( - modifier: Modifier = Modifier, - state: ButtonActionState, - onClick: (ButtonAction) -> Unit, -) { - when (state.action.theme) { - ButtonAction.Theme.DARK_GREEN -> - Button(modifier = modifier, onClick = { onClick(state.action) }, enabled = state.isEnabled) { - Content(action = state.action) - } - ButtonAction.Theme.LIGHT_GREEN -> - FilledTonalButton( - modifier = modifier, - onClick = { onClick(state.action) }, - enabled = state.isEnabled, - ) { - Content(action = state.action) - } - ButtonAction.Theme.OUTLINED -> - OutlinedButton( - modifier = modifier, - onClick = { onClick(state.action) }, - enabled = state.isEnabled, - ) { - Content(action = state.action) - } - ButtonAction.Theme.TRANSPARENT -> - OutlinedButton( - modifier = modifier, - border = null, - onClick = { onClick(state.action) }, - enabled = state.isEnabled, - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), - ) { - Content(action = state.action) - } - } -} - -@Composable -private fun Content(modifier: Modifier = Modifier, action: ButtonAction) { - when { - action.drawableId != null -> { - Icon( - modifier = modifier, - imageVector = ImageVector.vectorResource(id = action.drawableId), - contentDescription = action.contentDescription?.let { resId -> stringResource(resId) }, - ) - } - action.textId != null -> { - Text(modifier = modifier, text = stringResource(id = action.textId)) - } - } -} - -@Preview(showBackground = true) -@Composable -@ExcludeFromJacocoGeneratedReport -private fun TaskButtonAllPreview() { - AppTheme { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - ButtonAction.entries.forEach { action -> - TaskButton(state = ButtonActionState(action), onClick = {}) - } - } - } -} diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskFragment.kt index bad8704787..25510f1bc5 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskFragment.kt @@ -19,41 +19,27 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.doOnAttach import androidx.hilt.navigation.fragment.hiltNavGraphViewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import kotlin.properties.Delegates -import kotlinx.coroutines.launch import org.groundplatform.android.R -import org.groundplatform.android.model.submission.TaskData -import org.groundplatform.android.model.submission.isNotNullOrEmpty -import org.groundplatform.android.model.submission.isNullOrEmpty import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.AbstractFragment import org.groundplatform.android.ui.datacollection.DataCollectionUiState import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.LoiNameDialog -import org.groundplatform.android.ui.datacollection.components.TaskButton +import org.groundplatform.android.ui.datacollection.components.TaskFooter import org.groundplatform.android.ui.datacollection.components.TaskView import org.groundplatform.android.util.renderComposableDialog import org.groundplatform.android.util.setComposableContent -import org.jetbrains.annotations.TestOnly import timber.log.Timber abstract class AbstractTaskFragment : AbstractFragment() { @@ -61,8 +47,6 @@ abstract class AbstractTaskFragment : AbstractFragmen protected val dataCollectionViewModel: DataCollectionViewModel by hiltNavGraphViewModels(R.id.data_collection) - @TestOnly val buttonDataList: MutableList = mutableListOf() - private lateinit var taskView: TaskView protected lateinit var viewModel: T @@ -75,7 +59,7 @@ abstract class AbstractTaskFragment : AbstractFragmen viewModel true } catch (e: UninitializedPropertyAccessException) { - Timber.d("Viewmodel is not initialized", e) + Timber.e(e, "Viewmodel is not initialized") false } @@ -112,10 +96,7 @@ abstract class AbstractTaskFragment : AbstractFragmen taskView.addTaskView(onCreateTaskBody(layoutInflater)) // Add actions buttons after the view model is bound to the view. - addPreviousButton() - onCreateActionButtons() - renderButtons() - onActionButtonsCreated() + setupTaskFooter() onTaskViewAttached() } @@ -138,53 +119,6 @@ abstract class AbstractTaskFragment : AbstractFragmen /** Invoked when the task fragment is visible to the user. */ open fun onTaskResume() {} - /** Invoked when the fragment is ready to add buttons to the current [TaskView]. */ - open fun onCreateActionButtons() { - addSkipButton() - addNextButton() - } - - /** Invoked when the all [ButtonAction]s are added to the current [TaskView]. */ - open fun onActionButtonsCreated() { - viewLifecycleOwner.lifecycleScope.launch { - viewModel.taskTaskData.collect { onValueChanged(it) } - } - } - - /** Invoked when the data associated with the current task gets modified. */ - protected open fun onValueChanged(taskData: TaskData?) { - for ((_, button) in buttonDataList) { - button.onValueChanged(taskData) - } - } - - private fun addPreviousButton() = - addButton(ButtonAction.PREVIOUS) - .setOnClickListener { moveToPrevious() } - .enableIfTrue(!dataCollectionViewModel.isFirstPosition(taskId)) - - protected fun addNextButton(hideIfEmpty: Boolean = false) = - addButton(ButtonAction.NEXT) - .setOnClickListener { handleNext() } - .setOnValueChanged { button, value -> - if (hideIfEmpty) { - button.showIfTrue(value.isNotNullOrEmpty()) - } - button.enableIfTrue(value.isNotNullOrEmpty()) - val isLastPosition = dataCollectionViewModel.isLastPositionWithValue(viewModel.task, value) - button.toggleDone(done = isLastPosition) - } - .disable() - - /** Skip button is only visible iff the task is optional and the task doesn't contain any data. */ - protected fun addSkipButton() = - addButton(ButtonAction.SKIP) - .setOnClickListener { onSkip() } - .setOnValueChanged { button, value -> - button.showIfTrue(viewModel.isTaskOptional() && value.isNullOrEmpty()) - } - .showIfTrue(viewModel.isTaskOptional()) - private fun onSkip() { check(viewModel.hasNoData()) { "User should not be able to skip a task with data." } viewModel.setSkipped() @@ -214,51 +148,27 @@ abstract class AbstractTaskFragment : AbstractFragmen } } - fun addUndoButton() = addUndoButton { viewModel.clearResponse() } - - fun addUndoButton(clickHandler: () -> Unit) = - addButton(ButtonAction.UNDO) - .setOnClickListener { clickHandler() } - .setOnValueChanged { button, value -> button.showIfTrue(value.isNotNullOrEmpty()) } - .hide() - - protected fun addButton(buttonAction: ButtonAction): TaskButton { - val action = - if (buttonAction == ButtonAction.NEXT && isLastPosition()) ButtonAction.DONE else buttonAction - check(!buttonDataList.any { it.button.getAction() == action }) { - "Button $action already bound" - } - val button = TaskButton(action) - buttonDataList.add(ButtonData(index = buttonDataList.size, button)) - return button - } - /** Adds the action buttons to the UI. */ - private fun renderButtons() { - taskView.actionButtonsContainer.composeView.setComposableContent { - if (shouldShowHeader()) { - Column(modifier = Modifier.fillMaxWidth()) { - HeaderCard() - Spacer(Modifier.height(12.dp)) - ActionButtonsRow() - } - } else { - ActionButtonsRow() + private fun setupTaskFooter() { + with(taskView.actionButtonsContainer) { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setComposableContent { + val taskActionButtonsStates by + viewModel.taskActionButtonStates.collectAsStateWithLifecycle() + TaskFooter( + headerCard = + if (shouldShowHeader()) { + { HeaderCard() } + } else { + null + }, + buttonActionStates = taskActionButtonsStates, + onButtonClicked = { handleButtonClick(it) }, + ) } } } - @Composable - private fun ActionButtonsRow() { - Row( - modifier = Modifier.fillMaxWidth().height(48.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - buttonDataList.sortedBy { it.index }.forEach { (_, button) -> button.CreateButton() } - } - } - - @Suppress("UnusedPrivateMember") // To be implemented in the follow up PR private fun handleButtonClick(action: ButtonAction) { when (action) { // Navigation actions @@ -276,9 +186,6 @@ abstract class AbstractTaskFragment : AbstractFragmen @Composable open fun HeaderCard() {} - /** Returns true if the current task is in the last position in the sequence. */ - private fun isLastPosition() = dataCollectionViewModel.isLastPosition(taskId) - private fun getTask(): Task = viewModel.task private fun launchLoiNameDialog() { @@ -314,8 +221,6 @@ abstract class AbstractTaskFragment : AbstractFragmen } } - data class ButtonData(val index: Int, val button: TaskButton) - companion object { const val TASK_ID = "taskId" } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt index b39c1493e1..9e2c28ff32 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt @@ -17,7 +17,7 @@ package org.groundplatform.android.ui.datacollection.tasks import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged @@ -33,7 +33,7 @@ import org.groundplatform.android.model.submission.isNullOrEmpty import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.AbstractViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction -import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState +import org.groundplatform.android.ui.datacollection.components.ButtonActionState /** Defines the state of an inflated [Task] and controls its UI. */ abstract class AbstractTaskViewModel internal constructor() : AbstractViewModel() { @@ -46,7 +46,7 @@ abstract class AbstractTaskViewModel internal constructor() : AbstractViewModel( taskTaskData .map { getButtonStates(it) } .distinctUntilChanged() - .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + .stateIn(viewModelScope, WhileSubscribed(5_000), emptyList()) } lateinit var task: Task diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskFragment.kt index b507c25b7d..6d8f882017 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskFragment.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dagger.hilt.android.AndroidEntryPoint import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.TaskView import org.groundplatform.android.ui.datacollection.components.TaskViewFactory import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskFragment @@ -49,10 +48,6 @@ class InstructionTaskFragment : AbstractTaskFragment() ShowTextField(viewModel.task.label) } - override fun onCreateActionButtons() { - addButton(ButtonAction.NEXT).setOnClickListener { moveToNext() } - } - @Composable private fun ShowTextField(text: String) { Box( diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskViewModel.kt index e2c002e3ab..46d5f192eb 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskViewModel.kt @@ -17,7 +17,7 @@ package org.groundplatform.android.ui.datacollection.tasks.instruction import javax.inject.Inject import org.groundplatform.android.model.submission.TaskData -import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel class InstructionTaskViewModel @Inject constructor() : AbstractTaskViewModel() { diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskFragment.kt index 9f8cef11de..1ca574e3e1 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskFragment.kt @@ -39,9 +39,7 @@ import javax.inject.Provider import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.groundplatform.android.R -import org.groundplatform.android.model.submission.isNullOrEmpty import org.groundplatform.android.ui.components.ConfirmationDialog -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.TaskView import org.groundplatform.android.ui.datacollection.components.TaskViewFactory import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskFragment @@ -87,20 +85,6 @@ class CaptureLocationTaskFragment @Inject constructor() : } } - override fun onCreateActionButtons() { - addSkipButton() - addUndoButton() - addButton(ButtonAction.CAPTURE_LOCATION) - .setOnClickListener { viewModel.updateResponse() } - .setOnValueChanged { button, value -> button.showIfTrue(value.isNullOrEmpty()) } - .apply { - viewLifecycleOwner.lifecycleScope.launch { - viewModel.isCaptureEnabled.collect { isEnabled -> enableIfTrue(isEnabled) } - } - } - addNextButton(hideIfEmpty = true) - } - private fun showLocationPermissionDialog() { renderComposableDialog { ConfirmationDialog( diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt index bd9228702b..1fa7daa5fc 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt @@ -16,15 +16,17 @@ package org.groundplatform.android.ui.datacollection.tasks.location import android.location.Location +import androidx.annotation.VisibleForTesting import androidx.lifecycle.viewModelScope import javax.inject.Inject import kotlin.lazy import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -34,7 +36,7 @@ import org.groundplatform.android.model.submission.CaptureLocationTaskData import org.groundplatform.android.model.submission.TaskData import org.groundplatform.android.model.submission.isNullOrEmpty import org.groundplatform.android.ui.datacollection.components.ButtonAction -import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.AbstractMapTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.LocationLockEnabledState import org.groundplatform.android.ui.map.gms.getAccuracyOrNull @@ -62,13 +64,15 @@ class CaptureLocationTaskViewModel @Inject constructor() : AbstractMapTaskViewMo getNextButton(taskData, hideIfEmpty = true), ) } - .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + .distinctUntilChanged() + .stateIn(viewModelScope, WhileSubscribed(5_000), emptyList()) } fun updateLocation(location: Location) { _lastLocation.update { location } } + @VisibleForTesting fun updateResponse() { val location = _lastLocation.value if (location == null) { diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskFragment.kt index dec27f945c..870afdeb39 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskFragment.kt @@ -87,12 +87,6 @@ class PhotoTaskFragment : AbstractTaskFragment() { viewModel.surveyId = dataCollectionViewModel.requireSurveyId() } - override fun onCreateActionButtons() { - addUndoButton() - addSkipButton() - addNextButton() - } - override fun onResume() { super.onResume() diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt index 636f0ce4bc..cd4c83db3c 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskViewModel.kt @@ -31,7 +31,7 @@ import org.groundplatform.android.model.submission.PhotoTaskData import org.groundplatform.android.model.submission.TaskData import org.groundplatform.android.model.submission.isNotNullOrEmpty import org.groundplatform.android.repository.UserMediaRepository -import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel import timber.log.Timber diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskFragment.kt index ec70c302fa..1d8210bb7b 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskFragment.kt @@ -23,8 +23,6 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import javax.inject.Provider import org.groundplatform.android.R -import org.groundplatform.android.model.submission.isNullOrEmpty -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.InstructionsDialog import org.groundplatform.android.ui.datacollection.components.TaskView import org.groundplatform.android.ui.datacollection.components.TaskViewFactory @@ -54,16 +52,6 @@ class DropPinTaskFragment @Inject constructor() : AbstractTaskFragment button.showIfTrue(value.isNullOrEmpty()) } - - addNextButton(hideIfEmpty = true) - } - override fun onTaskResume() { if (isVisible && viewModel.shouldShowInstructionsDialog()) { showInstructionsDialog() diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskViewModel.kt index 4c0f99d1b2..39da5371de 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskViewModel.kt @@ -29,7 +29,7 @@ import org.groundplatform.android.model.submission.TaskData import org.groundplatform.android.model.submission.isNullOrEmpty import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.datacollection.components.ButtonAction -import org.groundplatform.android.ui.datacollection.components.refactor.ButtonActionState +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.AbstractMapTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface import org.groundplatform.android.ui.map.Feature @@ -96,7 +96,7 @@ constructor( selected = true, ) - fun dropPin() { + private fun dropPin() { getLastCameraPosition()?.let { updateResponse(Point(it.coordinates)) } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskFragment.kt index d0c59342c2..85831fc545 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskFragment.kt @@ -23,36 +23,22 @@ import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import javax.inject.Provider -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.groundplatform.android.R import org.groundplatform.android.databinding.FragmentDrawAreaTaskBinding -import org.groundplatform.android.model.geometry.LineString -import org.groundplatform.android.model.geometry.LineString.Companion.lineStringOf -import org.groundplatform.android.model.submission.isNotNullOrEmpty import org.groundplatform.android.ui.components.ConfirmationDialog -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.InstructionsDialog -import org.groundplatform.android.ui.datacollection.components.TaskButton import org.groundplatform.android.ui.datacollection.components.TaskView import org.groundplatform.android.ui.datacollection.components.TaskViewFactory import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskFragment import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskMapFragment.Companion.TASK_ID_FRAGMENT_ARG_KEY -import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.util.renderComposableDialog @AndroidEntryPoint class DrawAreaTaskFragment @Inject constructor() : AbstractTaskFragment() { @Inject lateinit var drawAreaTaskMapFragmentProvider: Provider - // Action buttons - private lateinit var completeButton: TaskButton - private lateinit var addPointButton: TaskButton - private lateinit var nextButton: TaskButton - private lateinit var drawAreaTaskMapFragment: DrawAreaTaskMapFragment override fun onCreateTaskView(inflater: LayoutInflater): TaskView = @@ -82,58 +68,7 @@ class DrawAreaTaskFragment @Inject constructor() : AbstractTaskFragment button.enableIfTrue(value.isNotNullOrEmpty()) } - addButton(ButtonAction.REDO) - .setOnClickListener { restoreLastVertex() } - .setOnValueChanged { button, value -> - button.enableIfTrue(viewModel.redoVertexStack.isNotEmpty() && value.isNotNullOrEmpty()) - } - nextButton = addNextButton() - addPointButton = - addButton(ButtonAction.ADD_POINT).setOnClickListener { - viewModel.addLastVertex() - val intersected = viewModel.checkVertexIntersection() - if (!intersected) viewModel.triggerVibration() - } - completeButton = - addButton(ButtonAction.COMPLETE).setOnClickListener { - if (viewModel.validatePolygonCompletion()) { - viewModel.completePolygon() - } - } - } - - /** Removes the last vertex from the polygon. */ - private fun removeLastVertex() { - viewModel.removeLastVertex() - - // Move the camera to the last vertex, if any. - moveToPosition() - } - - private fun restoreLastVertex() { - viewModel.redoLastVertex() - - moveToPosition() - } - - private fun moveToPosition() { - val lastVertex = viewModel.getLastVertex() ?: return - drawAreaTaskMapFragment.moveToPosition(lastVertex) - } - override fun onTaskViewAttached() { - onFeatureUpdated(null) - viewLifecycleOwner.lifecycleScope.launch { - merge(viewModel.draftArea, viewModel.draftUpdates) - .filterNotNull() - .collectLatest(::onFeatureUpdated) - } - // Collect camera movement events from ViewModel (e.g., after undo/redo) viewModel.cameraMoveEvents .onEach { coordinates -> drawAreaTaskMapFragment.moveToPosition(coordinates) } @@ -163,22 +98,6 @@ class DrawAreaTaskFragment @Inject constructor() : AbstractTaskFragment - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/task_frag_with_combined_header.xml b/app/src/main/res/layout/task_frag_with_combined_header.xml index b0f679998b..847df76393 100644 --- a/app/src/main/res/layout/task_frag_with_combined_header.xml +++ b/app/src/main/res/layout/task_frag_with_combined_header.xml @@ -62,13 +62,9 @@ android:layout_height="0dp" android:layout_weight="1" /> - + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/app/src/main/res/layout/task_frag_with_header.xml b/app/src/main/res/layout/task_frag_with_header.xml index a633581c48..ccd6e0dfd2 100644 --- a/app/src/main/res/layout/task_frag_with_header.xml +++ b/app/src/main/res/layout/task_frag_with_header.xml @@ -26,9 +26,10 @@ type="org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel" /> - + android:layout_height="match_parent" + android:orientation="vertical"> + android:layout_weight="1" /> - - + android:layout_height="wrap_content" /> + \ No newline at end of file diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskButtonTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/components/TaskButtonTest.kt similarity index 95% rename from app/src/test/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskButtonTest.kt rename to app/src/test/java/org/groundplatform/android/ui/datacollection/components/TaskButtonTest.kt index af0f075ade..47c5c87442 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskButtonTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/components/TaskButtonTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.ui.datacollection.components.refactor +package org.groundplatform.android.ui.datacollection.components import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertHasClickAction @@ -28,7 +28,6 @@ import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest import kotlin.test.assertNull import org.groundplatform.android.BaseHiltTest -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskFooterTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/components/TaskFooterTest.kt similarity index 94% rename from app/src/test/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskFooterTest.kt rename to app/src/test/java/org/groundplatform/android/ui/datacollection/components/TaskFooterTest.kt index d4ebe597d0..8d0cb06ff1 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/components/refactor/TaskFooterTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/components/TaskFooterTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.ui.datacollection.components.refactor +package org.groundplatform.android.ui.datacollection.components import androidx.activity.ComponentActivity import androidx.compose.ui.test.assertIsDisplayed @@ -24,7 +24,6 @@ import androidx.compose.ui.test.performClick import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest import org.groundplatform.android.BaseHiltTest -import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/BaseTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/BaseTaskFragmentTest.kt index cfa5b3e6f4..c8f3f519b0 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/BaseTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/BaseTaskFragmentTest.kt @@ -16,6 +16,10 @@ package org.groundplatform.android.ui.datacollection.tasks +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText import androidx.fragment.app.Fragment import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches @@ -24,7 +28,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.R import org.groundplatform.android.launchFragmentWithNavController @@ -34,7 +37,7 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.TaskFragmentRunner -import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.util.view.isGone import org.mockito.kotlin.whenever @@ -68,15 +71,26 @@ abstract class BaseTaskFragmentTest, VM : AbstractT viewModel.taskTaskData.test { assertThat(expectMostRecentItem()).isEqualTo(taskData) } } - /** Asserts that the task fragment has the given list of buttons in the exact same order. */ - protected fun assertFragmentHasButtons(vararg buttonActions: ButtonAction) { - // TODO: Also verify the visibility/state of the button - // Issue URL: https://github.com/google/ground-android/issues/2134 - assertThat(fragment.buttonDataList.map { it.button.getAction() }) - .containsExactlyElementsIn(buttonActions) - buttonActions.withIndex().forEach { (index, expected) -> - val actual = fragment.buttonDataList[index].button.getAction() - assertWithMessage("Incorrect button order").that(actual).isEqualTo(expected) + protected fun assertFragmentHasButtons(vararg buttonStates: ButtonActionState) { + buttonStates.forEach { state -> + val node = + state.action.contentDescription?.let { + composeTestRule.onNodeWithContentDescription(fragment.context!!.resources.getString(it)) + } + ?: composeTestRule.onNodeWithText( + fragment.context!!.resources.getString(state.action.textId!!) + ) + + if (state.isVisible) { + node.assertExists() + if (state.isEnabled) { + node.assertIsEnabled() + } else { + node.assertIsNotEnabled() + } + } else { + node.assertDoesNotExist() + } } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskFragmentTest.kt index aa2baf36fe..b9ccd583e0 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/date/DateTaskFragmentTest.kt @@ -18,6 +18,7 @@ package org.groundplatform.android.ui.datacollection.tasks.date import android.app.DatePickerDialog import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isNotDisplayed import androidx.compose.ui.test.onNodeWithText @@ -38,6 +39,7 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Test import org.junit.runner.RunWith @@ -66,6 +68,28 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(job, task) + + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) + } + + @Test + fun `Initial action buttons state when task is required`() { + setupTaskFragment(job, task.copy(isRequired = true)) + + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) + } + @Test fun `default response is empty`() { setupTaskFragment(job, task) @@ -86,7 +110,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(job, task) val view: View? = fragment.view?.findViewById(R.id.task_container) - view?.layoutParams = ViewGroup.LayoutParams(0, 1) + view?.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1) onView(withId(R.id.user_date_response_text)).perform(click()) assertThat(fragment.getDatePickerDialog()?.isShowing).isTrue() @@ -113,6 +137,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(job, task) val view: View? = fragment.view?.findViewById(R.id.task_container) - view?.layoutParams = ViewGroup.LayoutParams(0, 1) + view?.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1) onView(withId(R.id.user_date_response_text)).perform(click()) assertThat(fragment.getDatePickerDialog()?.isShowing).isTrue() @@ -144,24 +169,4 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(job, task) - assertFragmentHasButtons(ButtonAction.PREVIOUS, ButtonAction.SKIP, ButtonAction.NEXT) - } - - @Test - fun `action buttons when task is optional`() { - setupTaskFragment(job, task.copy(isRequired = false)) - - runner().assertButtonIsDisabled("Next").assertButtonIsEnabled("Skip") - } - - @Test - fun `action buttons when task is required`() { - setupTaskFragment(job, task.copy(isRequired = true)) - - runner().assertButtonIsDisabled("Next").assertButtonIsHidden("Skip") - } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskFragmentTest.kt index 57c86dbfb8..c3a72cba21 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/instruction/InstructionTaskFragmentTest.kt @@ -27,6 +27,7 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Rule import org.junit.Test @@ -63,7 +64,10 @@ class InstructionTaskFragmentTest : @Test fun `action buttons`() { setupTaskFragment(job, task) - assertFragmentHasButtons(ButtonAction.PREVIOUS, ButtonAction.NEXT) + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = true, isVisible = true), + ) } @Test diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt index c5fda8dba5..6dbe7dabb2 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt @@ -34,6 +34,7 @@ import org.groundplatform.android.ui.common.MapConfig import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Test import org.junit.runner.RunWith @@ -126,40 +127,31 @@ class CaptureLocationTaskFragmentTest : } @Test - fun `displays correct action buttons`() { + fun `Initial action buttons state when task is optional`() = runWithTestDispatcher { setupTaskFragment(job, task) + setupLocation(accuracy = 10.0) assertFragmentHasButtons( - ButtonAction.PREVIOUS, - ButtonAction.SKIP, - ButtonAction.UNDO, - ButtonAction.CAPTURE_LOCATION, - ButtonAction.NEXT, + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.CAPTURE_LOCATION, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = false), ) } @Test - fun `action buttons when task is optional`() = runWithTestDispatcher { - setupTaskFragment(job, task.copy(isRequired = false)) - setupLocation() - - runner() - .assertButtonIsHidden("Next") - .assertButtonIsEnabled("Skip") - .assertButtonIsHidden("Undo", true) - .assertButtonIsEnabled("Capture") - } - - @Test - fun `action buttons when task is required`() = runWithTestDispatcher { + fun `Initial action buttons state when task is required`() = runWithTestDispatcher { setupTaskFragment(job, task.copy(isRequired = true)) - setupLocation() + setupLocation(accuracy = 10.0) - runner() - .assertButtonIsHidden("Next") - .assertButtonIsHidden("Skip") - .assertButtonIsHidden("Undo", true) - .assertButtonIsEnabled("Capture") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.CAPTURE_LOCATION, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = false), + ) } @Test diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt index 44610aba50..ec3f7cadf1 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt @@ -32,14 +32,12 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.kotlin.any -import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.shadows.ShadowAlertDialog @@ -110,7 +108,11 @@ class MultipleChoiceTaskFragmentTest : val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) - runner().selectOption("Option 1").selectOption("Option 2").assertButtonIsEnabled("Next") + runner() + .assertButtonIsDisabled("Next") + .selectOption("Option 1") + .selectOption("Option 2") + .assertButtonIsEnabled("Next") hasValue(MultipleChoiceTaskData(multipleChoice, listOf("option id 2"))) } @@ -220,30 +222,51 @@ class MultipleChoiceTaskFragmentTest : } @Test - fun `renders action buttons`() { + fun `Initial action buttons state when task is optional`() { setupTaskFragment(job, task) - assertFragmentHasButtons(ButtonAction.PREVIOUS, ButtonAction.SKIP, ButtonAction.NEXT) + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } @Test - fun `renders action buttons on last task`() { - whenever(dataCollectionViewModel.isLastPosition(any())).thenReturn(true) - whenever(dataCollectionViewModel.isLastPositionWithValue(any(), eq(null))).thenReturn(true) - setupTaskFragment(job, task) + fun `Initial action buttons state when task is the first and optional`() { + setupTaskFragment(job, task, isFistPosition = true) - assertFragmentHasButtons(ButtonAction.PREVIOUS, ButtonAction.SKIP, ButtonAction.DONE) + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = false, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } @Test - fun `renders action buttons when first task and optional`() { - whenever(dataCollectionViewModel.isFirstPosition(task.id)).thenReturn(true) - setupTaskFragment(job, task.copy(isRequired = false)) + fun `Initial action buttons state when it's the last task and optional`() { + setupTaskFragment(job, task, isLastPosition = true) - runner() - .assertButtonIsDisabled("Previous") - .assertButtonIsDisabled("Next") - .assertButtonIsEnabled("Skip") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.DONE, isEnabled = false, isVisible = true), + ) + } + + @Test + fun `Initial action buttons state when it's the first task and required`() { + setupTaskFragment( + job, + task.copy(isRequired = true), + isFistPosition = true, + ) + + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = false, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } @Test @@ -264,28 +287,6 @@ class MultipleChoiceTaskFragmentTest : assertThat(ShadowAlertDialog.getShownDialogs().isEmpty()).isTrue() } - @Test - fun `renders action buttons when task is first and required`() { - whenever(dataCollectionViewModel.isFirstPosition(task.id)).thenReturn(true) - setupTaskFragment(job, task.copy(isRequired = true)) - - runner() - .assertButtonIsDisabled("Previous") - .assertButtonIsDisabled("Next") - .assertButtonIsHidden("Skip") - } - - @Test - fun `renders action buttons when task is not first`() { - whenever(dataCollectionViewModel.isFirstPosition(task.id)).thenReturn(false) - setupTaskFragment(job, task) - - runner() - .assertButtonIsEnabled("Previous") - .assertButtonIsDisabled("Next") - .assertButtonIsEnabled("Skip") - } - @Test fun `doesn't save response when other text is missing and task is required`() = runWithTestDispatcher { diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt index db70960ffc..df4b595e2a 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt @@ -24,6 +24,7 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Test import org.junit.runner.RunWith @@ -67,7 +68,11 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(job, task) - runner().inputNumber(123.1).assertInputNumberDisplayed("123.1").assertButtonIsEnabled("Next") + runner() + .assertButtonIsDisabled("Next") + .inputNumber(123.1) + .assertInputNumberDisplayed("123.1") + .assertButtonIsEnabled("Next") hasValue(NumberTaskData("123.1")) } @@ -86,23 +91,24 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(job, task) - assertFragmentHasButtons(ButtonAction.PREVIOUS, ButtonAction.SKIP, ButtonAction.NEXT) - } - - @Test - fun `action buttons when task is optional`() { - setupTaskFragment(job, task.copy(isRequired = false)) - - runner().assertButtonIsDisabled("Next").assertButtonIsEnabled("Skip") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } @Test - fun `action buttons when task is required`() { + fun `Initial action buttons state when task is required`() { setupTaskFragment(job, task.copy(isRequired = true)) - runner().assertButtonIsDisabled("Next").assertButtonIsHidden("Skip") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskFragmentTest.kt index b4c9e6a03b..22a9736619 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/photo/PhotoTaskFragmentTest.kt @@ -32,6 +32,7 @@ import org.groundplatform.android.ui.common.EphemeralPopups import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.groundplatform.android.ui.home.HomeScreenViewModel import org.junit.Test @@ -99,25 +100,27 @@ class PhotoTaskFragmentTest : BaseTaskFragmentTest(job, task) assertFragmentHasButtons( - ButtonAction.PREVIOUS, - ButtonAction.UNDO, - ButtonAction.SKIP, - ButtonAction.NEXT, + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), ) } @Test - fun `action buttons when task is optional`() { - setupTaskFragment(job, task.copy(isRequired = false)) + fun `Initial action buttons state when task is required`() { + setupTaskFragment(job, task.copy(isRequired = true)) - runner() - .assertButtonIsDisabled("Next") - .assertButtonIsEnabled("Skip") - .assertButtonIsHidden("Undo", true) + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } @Test diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskFragmentTest.kt index 8f8338dbba..a52c64c607 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskFragmentTest.kt @@ -29,6 +29,7 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Before import org.junit.Test @@ -108,37 +109,28 @@ class DropPinTaskFragmentTest : BaseTaskFragmentTest(job, task) assertFragmentHasButtons( - ButtonAction.PREVIOUS, - ButtonAction.SKIP, - ButtonAction.UNDO, - ButtonAction.DROP_PIN, - ButtonAction.NEXT, + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.DROP_PIN, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = false), ) } @Test - fun `shows skip when task is optional`() { - setupTaskFragment(job, task.copy(isRequired = false)) - - runner() - .assertButtonIsHidden("Next") - .assertButtonIsEnabled("Skip") - .assertButtonIsHidden("Undo", true) - .assertButtonIsEnabled("Drop pin") - } - - @Test - fun `hides skip when task is required`() { + fun `Initial action buttons state when task is required`() { setupTaskFragment(job, task.copy(isRequired = true)) - runner() - .assertButtonIsHidden("Next") - .assertButtonIsHidden("Skip") - .assertButtonIsHidden("Undo", true) - .assertButtonIsEnabled("Drop pin") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.DROP_PIN, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = false), + ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskFragmentTest.kt index dd5be15335..84e5f73fdc 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskFragmentTest.kt @@ -31,6 +31,7 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Test import org.junit.runner.RunWith @@ -71,44 +72,33 @@ class DrawAreaTaskFragmentTest : } @Test - fun `action buttons`() { + fun `Initial action buttons state when task is optional`() { setupTaskFragment(job, task) assertFragmentHasButtons( - ButtonAction.PREVIOUS, - ButtonAction.SKIP, - ButtonAction.UNDO, - ButtonAction.REDO, - ButtonAction.NEXT, - ButtonAction.ADD_POINT, - ButtonAction.COMPLETE, + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = true), + ButtonActionState(ButtonAction.REDO, isEnabled = false, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.ADD_POINT, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.COMPLETE, isEnabled = false, isVisible = false), ) } @Test - fun `action buttons when task is optional`() { - setupTaskFragment(job, task.copy(isRequired = false)) - - runner() - .assertButtonIsHidden(NEXT_POINT_BUTTON_TEXT) - .assertButtonIsEnabled(SKIP_POINT_BUTTON_TEXT) - .assertButtonIsDisabled(UNDO_POINT_BUTTON_TEXT, true) - .assertButtonIsDisabled(REDO_POINT_BUTTON_TEXT, true) - .assertButtonIsEnabled(ADD_POINT_BUTTON_TEXT) - .assertButtonIsHidden(COMPLETE_POINT_BUTTON_TEXT) - } - - @Test - fun `action buttons when task is required`() { + fun `Initial action buttons state when task is required`() { setupTaskFragment(job, task.copy(isRequired = true)) - runner() - .assertButtonIsHidden(NEXT_POINT_BUTTON_TEXT) - .assertButtonIsHidden(SKIP_POINT_BUTTON_TEXT) - .assertButtonIsDisabled(UNDO_POINT_BUTTON_TEXT, true) - .assertButtonIsDisabled(REDO_POINT_BUTTON_TEXT, true) - .assertButtonIsEnabled(ADD_POINT_BUTTON_TEXT) - .assertButtonIsHidden(COMPLETE_POINT_BUTTON_TEXT) + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.UNDO, isEnabled = false, isVisible = true), + ButtonActionState(ButtonAction.REDO, isEnabled = false, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.ADD_POINT, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.COMPLETE, isEnabled = false, isVisible = false), + ) } @Test @@ -160,6 +150,7 @@ class DrawAreaTaskFragmentTest : .assertButtonIsEnabled(UNDO_POINT_BUTTON_TEXT, true) .assertButtonIsDisabled(REDO_POINT_BUTTON_TEXT, true) .assertButtonIsHidden(ADD_POINT_BUTTON_TEXT) + .assertButtonIsEnabled(NEXT_POINT_BUTTON_TEXT) hasValue( DrawAreaTaskData( diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt index 9c3c2afdf7..4da473000f 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt @@ -492,7 +492,7 @@ class DrawAreaTaskViewModelTest : BaseHiltTest() { val states = viewModel.taskActionButtonStates.first() - with(requireNotNull(states.find { it.action == ButtonAction.UNDO })) { + with(requireNotNull(states.find { it.action == ButtonAction.REDO })) { assertTrue(isVisible) assertFalse(isEnabled) } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskFragmentTest.kt index c743340b81..5e09e2eff0 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskFragmentTest.kt @@ -28,6 +28,7 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Test import org.junit.runner.RunWith @@ -98,29 +99,34 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(job, task) - runner().inputText("Hello world").clickNextButton() + runner() + .assertButtonIsDisabled("Next") + .inputText("Hello world") + .assertButtonIsEnabled("Next") + .clickNextButton() hasValue(TextTaskData("Hello world")) } @Test - fun `displays correct action buttons`() { + fun `Initial action buttons state when task is optional`() { setupTaskFragment(job, task) - assertFragmentHasButtons(ButtonAction.PREVIOUS, ButtonAction.SKIP, ButtonAction.NEXT) - } - - @Test - fun `action buttons when task is optional`() { - setupTaskFragment(job, task.copy(isRequired = false)) - - runner().assertButtonIsDisabled("Next").assertButtonIsEnabled("Skip") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } @Test - fun `action buttons when task is required`() { + fun `Initial action buttons state when task is required`() { setupTaskFragment(job, task.copy(isRequired = true)) - runner().assertButtonIsDisabled("Next").assertButtonIsHidden("Skip") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } } diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt index fcb7f0def1..0e6e329ba2 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt @@ -17,6 +17,7 @@ package org.groundplatform.android.ui.datacollection.tasks.time import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.onNodeWithText import androidx.test.espresso.Espresso.onView @@ -36,6 +37,7 @@ import org.groundplatform.android.model.task.Task import org.groundplatform.android.ui.common.ViewModelFactory import org.groundplatform.android.ui.datacollection.DataCollectionViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction +import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.BaseTaskFragmentTest import org.junit.Test import org.junit.runner.RunWith @@ -83,32 +85,35 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(job, task) - assertFragmentHasButtons(ButtonAction.PREVIOUS, ButtonAction.SKIP, ButtonAction.NEXT) - } - - @Test - fun `action buttons when task is optional`() { - setupTaskFragment(job, task.copy(isRequired = false)) - - runner().assertButtonIsDisabled("Next").assertButtonIsEnabled("Skip") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } @Test - fun `action buttons when task is required`() { + fun `Initial action buttons state when task is required`() { setupTaskFragment(job, task.copy(isRequired = true)) - runner().assertButtonIsDisabled("Next").assertButtonIsHidden("Skip") + assertFragmentHasButtons( + ButtonActionState(ButtonAction.PREVIOUS, isEnabled = true, isVisible = true), + ButtonActionState(ButtonAction.SKIP, isEnabled = false, isVisible = false), + ButtonActionState(ButtonAction.NEXT, isEnabled = false, isVisible = true), + ) } @Test