Skip to content
Merged
Prev Previous commit
Next Next commit
Add tests
  • Loading branch information
grzesiek2010 committed Jan 29, 2026
commit 800e0a2ba3f08be8233a95b3d62115516932ffe3
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import androidx.activity.ComponentActivity;
import androidx.lifecycle.MutableLiveData;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.javarosa.form.api.FormEntryPrompt;
Expand Down Expand Up @@ -31,6 +32,7 @@ public class InternalRecordingRequesterTest {

@Before
public void setup() {
ApplicationProvider.getApplicationContext().setTheme(com.google.android.material.R.style.Theme_MaterialComponents);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change needed? If I take it out, then there's a failure, but it seems completely unrelated to everything else here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens because androidx.compose.ui:ui-test-manifest provides its own test manifest, which changes the default app theme used during tests. Robolectric picks up that manifest even for non-Compose tests, and the new default theme is not AppCompat/Material.

ComponentActivity activity = Robolectric.buildActivity(ComponentActivity.class).get();
when(audioRecorder.getCurrentSession()).thenReturn(new MutableLiveData<>(null));

Expand Down
2 changes: 2 additions & 0 deletions geo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,6 @@ dependencies {
testImplementation(libs.robolectric)
testImplementation(libs.androidxTestEspressoCore)
testImplementation(libs.androidxArchCoreTesting)
testImplementation(libs.androidXComposeUiTestJunit4)
debugImplementation(libs.androidXComposeUiTestManifest)
}
165 changes: 60 additions & 105 deletions geo/src/main/java/org/odk/collect/geo/geopoly/InfoDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.odk.collect.androidshared.R.dimen
import org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent
Expand All @@ -44,22 +43,7 @@ object InfoDialog {

val info = ComposeView(context).apply {
setContextThemedContent {
when (viewModel.recordingMode) {
GeoPolyViewModel.RecordingMode.PLACEMENT -> {
if (fromSnackbar) {
PlacementFromSnackbarInfo { dialog?.dismiss() }
} else {
PlacementFromInfoButtonInfo { dialog?.dismiss() }
}
}
GeoPolyViewModel.RecordingMode.MANUAL, GeoPolyViewModel.RecordingMode.AUTOMATIC -> {
if (fromSnackbar) {
ManualOrAutomaticFromSnackbarInfo { dialog?.dismiss() }
} else {
ManualOrAutomaticFromInfoButtonInfo { dialog?.dismiss() }
}
}
}
InfoContent(viewModel, fromSnackbar) { dialog?.dismiss() }
}
}

Expand All @@ -70,47 +54,67 @@ object InfoDialog {
}

@Composable
private fun PlacementFromSnackbarInfo(onDone: () -> Unit) {
InfoContent(InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_shape_to_start_over_info_item)),
InfoDialog.InfoItem(Icons.Filled.AddLocation, stringResource(string.add_point_info_item)),
onDone = onDone
)
}

@Composable
private fun PlacementFromInfoButtonInfo(onDone: () -> Unit) {
InfoContent(
InfoDialog.InfoItem(Icons.Filled.AddLocation, stringResource(string.tap_to_add_a_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item)),
onDone = onDone
)
}
fun InfoContent(
viewModel: GeoPolyViewModel,
fromSnackbar: Boolean,
onDone: () -> Unit
) {
val items = when (viewModel.recordingMode) {
GeoPolyViewModel.RecordingMode.PLACEMENT -> {
if (fromSnackbar) {
listOf(
InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_shape_to_start_over_info_item)),
InfoDialog.InfoItem(Icons.Filled.AddLocation, stringResource(string.add_point_info_item))
)
} else {
listOf(
InfoDialog.InfoItem(Icons.Filled.AddLocation, stringResource(string.tap_to_add_a_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item))
)
}
}
GeoPolyViewModel.RecordingMode.MANUAL, GeoPolyViewModel.RecordingMode.AUTOMATIC -> {
if (fromSnackbar) {
listOf(
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.DirectionsWalk, stringResource(string.physically_move_to_correct_info_item)),
InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item)),
)
} else {
listOf(
InfoDialog.InfoItem(Icons.Filled.AddLocation, stringResource(string.tap_to_add_a_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.DirectionsWalk, stringResource(string.physically_move_to_correct_info_item)),
InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item))
)
}
}
}

@Composable
private fun ManualOrAutomaticFromSnackbarInfo(onDone: () -> Unit) {
InfoContent(
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.DirectionsWalk, stringResource(string.physically_move_to_correct_info_item)),
InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item)),
onDone = onDone
)
}
val scrollState = rememberScrollState()

@Composable
private fun ManualOrAutomaticFromInfoButtonInfo(onDone: () -> Unit) {
InfoContent(
InfoDialog.InfoItem(Icons.Filled.AddLocation, stringResource(string.tap_to_add_a_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.DirectionsWalk, stringResource(string.physically_move_to_correct_info_item)),
InfoDialog.InfoItem(Icons.Filled.TouchApp, stringResource(string.long_press_to_move_point_info_item)),
InfoDialog.InfoItem(Icons.AutoMirrored.Filled.Backspace, stringResource(string.remove_last_point_info_item)),
InfoDialog.InfoItem(Icons.Filled.Delete, stringResource(string.delete_entire_shape_info_item)),
onDone = onDone
)
Column(
modifier = Modifier
.padding(dimensionResource(id = dimen.margin_standard))
.verticalScroll(scrollState)
) {
Title()
items.forEachIndexed { index, item ->
Info(item.icon, item.text)
if (index < items.lastIndex) {
HorizontalDivider(
Modifier.padding(horizontal = dimensionResource(id = dimen.margin_small))
)
}
}
DoneButton(onDone)
}
}

@Composable
Expand Down Expand Up @@ -146,31 +150,6 @@ private fun Info(icon: ImageVector, text: String) {
}
}

@Composable
private fun InfoContent(
vararg items: InfoDialog.InfoItem,
onDone: () -> Unit
) {
val scrollState = rememberScrollState()

Column(
modifier = Modifier
.padding(dimensionResource(id = dimen.margin_standard))
.verticalScroll(scrollState)
) {
Title()
items.forEachIndexed { index, item ->
Info(item.icon, item.text)
if (index < items.lastIndex) {
HorizontalDivider(
Modifier.padding(horizontal = dimensionResource(id = dimen.margin_small))
)
}
}
DoneButton(onDone)
}
}

@Composable
private fun DoneButton(onDone: () -> Unit) {
Row(
Expand All @@ -184,27 +163,3 @@ private fun DoneButton(onDone: () -> Unit) {
}
}
}

@Preview(showBackground = true)
@Composable
private fun PlacementFromSnackbarInfoPreview() {
PlacementFromSnackbarInfo {}
}

@Preview(showBackground = true)
@Composable
private fun PlacementFromInfoButtonInfoPreview() {
PlacementFromInfoButtonInfo {}
}

@Preview(showBackground = true)
@Composable
private fun ManualOrAutomaticFromSnackbarInfoPreview() {
ManualOrAutomaticFromSnackbarInfo {}
}

@Preview(showBackground = true)
@Composable
private fun ManualOrAutomaticFromInfoButtonInfoPreview() {
ManualOrAutomaticFromInfoButtonInfo {}
}
154 changes: 154 additions & 0 deletions geo/src/test/java/org/odk/collect/geo/geopoly/InfoDialogTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.odk.collect.geo.geopoly

import android.app.Application
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollTo
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class InfoDialogTest {

@get:Rule
val composeTestRule = createComposeRule()

private val context = ApplicationProvider.getApplicationContext<Application>()

@Test
fun `shows dialog content from snackbar in PLACEMENT mode`() {
val viewModel = mock<GeoPolyViewModel>().apply {
whenever(recordingMode).thenReturn(GeoPolyViewModel.RecordingMode.PLACEMENT)
}

composeTestRule.setContent {
InfoContent(viewModel, fromSnackbar = true, {})
}

assertInfo(
listOf(
org.odk.collect.strings.R.string.long_press_to_move_point_info_item,
org.odk.collect.strings.R.string.remove_last_point_info_item,
org.odk.collect.strings.R.string.delete_shape_to_start_over_info_item,
org.odk.collect.strings.R.string.add_point_info_item,
)
)
}

@Test
fun `shows dialog content from info button in PLACEMENT mode`() {
val viewModel = mock<GeoPolyViewModel>().apply {
whenever(recordingMode).thenReturn(GeoPolyViewModel.RecordingMode.PLACEMENT)
}

composeTestRule.setContent {
InfoContent(viewModel, fromSnackbar = false, {})
}

assertInfo(
listOf(
org.odk.collect.strings.R.string.tap_to_add_a_point_info_item,
org.odk.collect.strings.R.string.long_press_to_move_point_info_item,
org.odk.collect.strings.R.string.remove_last_point_info_item,
org.odk.collect.strings.R.string.delete_entire_shape_info_item,
)
)
}

@Test
fun `shows dialog content from snackbar in MANUAL mode`() {
val viewModel = mock<GeoPolyViewModel>().apply {
whenever(recordingMode).thenReturn(GeoPolyViewModel.RecordingMode.MANUAL)
}

composeTestRule.setContent {
InfoContent(viewModel, fromSnackbar = true, {})
}

assertInfo(
listOf(
org.odk.collect.strings.R.string.physically_move_to_correct_info_item,
org.odk.collect.strings.R.string.long_press_to_move_point_info_item,
org.odk.collect.strings.R.string.remove_last_point_info_item,
org.odk.collect.strings.R.string.delete_entire_shape_info_item,
)
)
}

@Test
fun `shows dialog content from info button in MANUAL mode`() {
val viewModel = mock<GeoPolyViewModel>().apply {
whenever(recordingMode).thenReturn(GeoPolyViewModel.RecordingMode.MANUAL)
}

composeTestRule.setContent {
InfoContent(viewModel, fromSnackbar = false, {})
}

assertInfo(
listOf(
org.odk.collect.strings.R.string.tap_to_add_a_point_info_item,
org.odk.collect.strings.R.string.physically_move_to_correct_info_item,
org.odk.collect.strings.R.string.long_press_to_move_point_info_item,
org.odk.collect.strings.R.string.remove_last_point_info_item,
org.odk.collect.strings.R.string.delete_entire_shape_info_item,
)
)
}

@Test
fun `shows dialog content from snackbar in AUTOMATIC mode`() {
val viewModel = mock<GeoPolyViewModel>().apply {
whenever(recordingMode).thenReturn(GeoPolyViewModel.RecordingMode.AUTOMATIC)
}

composeTestRule.setContent {
InfoContent(viewModel, fromSnackbar = true, {})
}

assertInfo(
listOf(
org.odk.collect.strings.R.string.physically_move_to_correct_info_item,
org.odk.collect.strings.R.string.long_press_to_move_point_info_item,
org.odk.collect.strings.R.string.remove_last_point_info_item,
org.odk.collect.strings.R.string.delete_entire_shape_info_item,
)
)
}

@Test
fun `shows dialog content from info button in AUTOMATIC mode`() {
val viewModel = mock<GeoPolyViewModel>().apply {
whenever(recordingMode).thenReturn(GeoPolyViewModel.RecordingMode.AUTOMATIC)
}

composeTestRule.setContent {
InfoContent(viewModel, fromSnackbar = false, {})
}

assertInfo(
listOf(
org.odk.collect.strings.R.string.tap_to_add_a_point_info_item,
org.odk.collect.strings.R.string.physically_move_to_correct_info_item,
org.odk.collect.strings.R.string.long_press_to_move_point_info_item,
org.odk.collect.strings.R.string.remove_last_point_info_item,
org.odk.collect.strings.R.string.delete_entire_shape_info_item,
)
)
}

private fun assertInfo(items: List<Int>) {
items.forEach {
composeTestRule
.onNodeWithText(context.getString(it))
.performScrollTo()
.assertIsDisplayed()
}
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ androidXComposeMaterialIconsExtended = { group = "androidx.compose.material", na
androidXComposePreview = { group = "androidx.compose.ui", name = "ui-tooling-preview"}
androidXComposeTooling = { group = "androidx.compose.ui", name = "ui-tooling"}
androidXComposeUiTestJunit4 = { group = "androidx.compose.ui", name = "ui-test-junit4"}
androidXComposeUiTestManifest = { group = "androidx.compose.ui", name = "ui-test-manifest"}
androidXConstraintLayoutCompose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version = "1.1.1" }
androidXComposeMaterialIcons = { group = "androidx.compose.material", name = "material-icons-extended", version ="1.7.8"}
playServicesMaps = { group = "com.google.android.gms", name = "play-services-maps", version = "19.2.0" }
Expand Down