diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt index d78d082bc09..1ad92a09c3f 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt @@ -75,7 +75,10 @@ object SnackbarUtils { } fun show(snackbar: Snackbar) { - lastSnackbar?.dismiss() + if (snackbar != lastSnackbar) { + lastSnackbar?.dismiss() + } + snackbar.show() lastSnackbar = snackbar diff --git a/androidtest/build.gradle.kts b/androidtest/build.gradle.kts index 00fa81f559a..e31d3f587d3 100644 --- a/androidtest/build.gradle.kts +++ b/androidtest/build.gradle.kts @@ -44,4 +44,9 @@ dependencies { var composeBom = platform(libs.androidxComposeBom) implementation(composeBom) implementation(libs.androidXComposeUiTestJunit4) + + //noinspection FragmentGradleConfiguration + debugApi(libs.androidxFragmentTesting) { + exclude(group = "androidx.test", module = "monitor") // fixes issue https://github.com/android/android-test/issues/731 + } } diff --git a/androidtest/src/main/java/org/odk/collect/androidtest/FragmentScenarioExtensions.kt b/androidtest/src/main/java/org/odk/collect/androidtest/FragmentScenarioExtensions.kt new file mode 100644 index 00000000000..a355788be4f --- /dev/null +++ b/androidtest/src/main/java/org/odk/collect/androidtest/FragmentScenarioExtensions.kt @@ -0,0 +1,17 @@ +package org.odk.collect.androidtest + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentResultListener +import androidx.fragment.app.testing.FragmentScenario + +object FragmentScenarioExtensions { + + fun FragmentScenario.setFragmentResultListener( + requestKey: String, + listener: FragmentResultListener + ) { + onFragment { + it.parentFragmentManager.setFragmentResultListener(requestKey, it, listener) + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/AdditionalAttributes.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/AdditionalAttributes.kt new file mode 100644 index 00000000000..4ad9ec1e924 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/AdditionalAttributes.kt @@ -0,0 +1,5 @@ +package org.odk.collect.android.widgets.utilities + +object AdditionalAttributes { + const val INCREMENTAL = "incremental" +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/BindAttributes.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/BindAttributes.kt index 2fdb69367af..94b356b5a3e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/BindAttributes.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/BindAttributes.kt @@ -2,7 +2,6 @@ package org.odk.collect.android.widgets.utilities object BindAttributes { const val ALLOW_MOCK_ACCURACY = "allow-mock-accuracy" - const val INCREMENTAL = "incremental" const val QUALITY = "quality" enum class Quality(val value: String) { diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragment.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragment.kt index e90a392ac52..ee6b799fb18 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragment.kt @@ -1,13 +1,14 @@ package org.odk.collect.android.widgets.utilities +import androidx.activity.ComponentDialog import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map import org.javarosa.core.model.Constants import org.javarosa.core.model.data.StringData import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.utilities.FormEntryPromptUtils +import org.odk.collect.android.widgets.utilities.AdditionalAttributes.INCREMENTAL import org.odk.collect.android.widgets.utilities.BindAttributes.ALLOW_MOCK_ACCURACY -import org.odk.collect.android.widgets.utilities.BindAttributes.INCREMENTAL import org.odk.collect.geo.geopoly.GeoPolyFragment import org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode import org.odk.collect.geo.geopoly.GeoPolyUtils @@ -25,7 +26,7 @@ class GeoPolyDialogFragment(viewModelFactory: ViewModelProvider.Factory) : ) { _, result -> val geopolyChange = result.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE) val geopoly = result.getString(GeoPolyFragment.RESULT_GEOPOLY) - val incremental = FormEntryPromptUtils.getBindAttribute(prompt, INCREMENTAL) + val incremental = FormEntryPromptUtils.getAdditionalAttribute(prompt, INCREMENTAL) if (geopolyChange != null) { if (incremental == "true") { @@ -50,6 +51,7 @@ class GeoPolyDialogFragment(viewModelFactory: ViewModelProvider.Factory) : val inputPolygon = GeoPolyUtils.parseGeometry(answer?.value as String?) return GeoPolyFragment( + { (requireDialog() as ComponentDialog).onBackPressedDispatcher }, outputMode, prompt.isReadOnly, retainMockAccuracy, diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragmentTest.kt index 700e47ff598..7c148b637c3 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/utilities/GeoPolyDialogFragmentTest.kt @@ -26,6 +26,7 @@ import org.odk.collect.android.formentry.FormEntryViewModel import org.odk.collect.android.javarosawrapper.FailedValidationResult import org.odk.collect.android.support.CollectHelpers import org.odk.collect.android.support.MockFormEntryPromptBuilder +import org.odk.collect.android.widgets.utilities.AdditionalAttributes.INCREMENTAL import org.odk.collect.android.widgets.utilities.WidgetAnswerDialogFragment.Companion.ARG_FORM_INDEX import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule @@ -231,7 +232,7 @@ class GeoPolyDialogFragmentTest { @Test fun `sets answer with validate when REQUEST_GEOPOLY_CHANGE is returned if question is incremental`() { prompt = MockFormEntryPromptBuilder(prompt) - .withBindAttribute("", "incremental", "true") + .withAdditionalAttribute(INCREMENTAL, "true") .build() val answer = "0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0" @@ -267,7 +268,7 @@ class GeoPolyDialogFragmentTest { verify(formEntryViewModel, never()).answerQuestion(prompt.index, StringData(answer)) prompt = MockFormEntryPromptBuilder(prompt) - .withBindAttribute("", "incremental", "false") + .withAdditionalAttribute(INCREMENTAL, "false") .build() launcherRule.launch( @@ -286,7 +287,7 @@ class GeoPolyDialogFragmentTest { @Test fun `does not dismiss when REQUEST_GEOPOLY_CHANGE is returned regardless of incremental value`() { prompt = MockFormEntryPromptBuilder(prompt) - .withBindAttribute("", "incremental", "true") + .withAdditionalAttribute(INCREMENTAL, "true") .build() val answer = "0.0 0.0 1.0 1.0; 0.0 1.0 1.0 1.0" @@ -305,7 +306,7 @@ class GeoPolyDialogFragmentTest { } prompt = MockFormEntryPromptBuilder(prompt) - .withBindAttribute("", "incremental", "false") + .withAdditionalAttribute(INCREMENTAL, "false") .build() launcherRule.launch( diff --git a/fragments-test/src/main/java/org/odk/collect/fragmentstest/FragmentScenarioLauncherRule.kt b/fragments-test/src/main/java/org/odk/collect/fragmentstest/FragmentScenarioLauncherRule.kt index 7d0f85a0e41..e82a71993ce 100644 --- a/fragments-test/src/main/java/org/odk/collect/fragmentstest/FragmentScenarioLauncherRule.kt +++ b/fragments-test/src/main/java/org/odk/collect/fragmentstest/FragmentScenarioLauncherRule.kt @@ -6,6 +6,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentFactory import androidx.fragment.app.testing.FragmentScenario import androidx.lifecycle.Lifecycle +import com.google.android.material.R import org.junit.rules.ExternalResource import kotlin.reflect.KClass @@ -16,7 +17,7 @@ import kotlin.reflect.KClass */ class FragmentScenarioLauncherRule @JvmOverloads constructor( private val defaultFactory: FragmentFactory? = null, - @StyleRes private val defaultThemeResId: Int = com.google.android.material.R.style.Theme_Material3_Light + @StyleRes private val defaultThemeResId: Int = R.style.Theme_Material3_Light ) : ExternalResource() { private val scenarios = mutableListOf>() @@ -51,6 +52,25 @@ class FragmentScenarioLauncherRule @JvmOverloads constructor( } } + inline fun launchInContainer(crossinline factory: () -> F): FragmentScenario { + val fragmentFactory = object : FragmentFactory() { + override fun instantiate( + classLoader: ClassLoader, + className: String + ): Fragment { + val fragmentClass = loadFragmentClass(classLoader, className) + + return if (F::class.java.isAssignableFrom(fragmentClass)) { + factory() + } else { + super.instantiate(classLoader, className) + } + } + } + + return launchInContainer(F::class.java, factory = fragmentFactory) + } + @JvmOverloads fun launch( fragmentClass: Class, diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index b79fefd2994..acc58bbb41f 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -4,6 +4,8 @@ import android.content.Context import android.os.Bundle import android.view.View import androidx.activity.OnBackPressedCallback +import androidx.activity.OnBackPressedDispatcher +import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView @@ -41,6 +43,7 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class GeoPolyFragment @JvmOverloads constructor( + val onBackPressedDispatcher: () -> OnBackPressedDispatcher, val outputMode: OutputMode = OutputMode.GEOTRACE, val readOnly: Boolean = false, val retainMockAccuracy: Boolean = false, @@ -123,7 +126,6 @@ class GeoPolyFragment @JvmOverloads constructor( .build() requireLocationPermissions(requireActivity()) - requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback) } override fun onCreate(savedInstanceState: Bundle?) { @@ -157,6 +159,8 @@ class GeoPolyFragment @JvmOverloads constructor( snackbar.dismiss() } } + + onBackPressedDispatcher().addCallback(viewLifecycleOwner, onBackPressedCallback) } override fun onSaveInstanceState(state: Bundle) { @@ -215,7 +219,7 @@ class GeoPolyFragment @JvmOverloads constructor( saveAsPolygon() } } else { - setResult(RESULT_GEOPOLY) + setResult() } } @@ -293,7 +297,7 @@ class GeoPolyFragment @JvmOverloads constructor( private fun saveAsPolyline() { if (map!!.getPolyLinePoints(featureId).size > 1) { - setResult(RESULT_GEOPOLY) + setResult() } else { showShortToastInMiddle( requireActivity(), @@ -310,7 +314,7 @@ class GeoPolyFragment @JvmOverloads constructor( if (count > 1 && points[0] != points[count - 1]) { map!!.appendPointToPolyLine(featureId, points[0]) } - setResult(RESULT_GEOPOLY) + setResult() } else { showShortToastInMiddle( requireActivity(), @@ -319,15 +323,35 @@ class GeoPolyFragment @JvmOverloads constructor( } } - private fun setResult(result: String) { + private fun setChangeResult() { val points = map!!.getPolyLinePoints(featureId) - val geoString = GeoUtils.formatPointsResultString( + val geoString = if (outputMode == OutputMode.GEOSHAPE && points.size < 3) { + "" + } else if (points.size < 2) { + "" + } else { + getGeoString(points) + } + + getParentFragmentManager().setFragmentResult( + REQUEST_GEOPOLY, + bundleOf(RESULT_GEOPOLY_CHANGE to geoString) + ) + } + + private fun setResult() { + val points = map!!.getPolyLinePoints(featureId) + getParentFragmentManager().setFragmentResult( + REQUEST_GEOPOLY, + bundleOf(RESULT_GEOPOLY to getGeoString(points)) + ) + } + + private fun getGeoString(points: List): String? { + return GeoUtils.formatPointsResultString( points.toMutableList(), outputMode == OutputMode.GEOSHAPE ) - val bundle = Bundle() - bundle.putString(result, geoString) - getParentFragmentManager().setFragmentResult(REQUEST_GEOPOLY, bundle) } override fun startInput() { @@ -393,6 +417,17 @@ class GeoPolyFragment @JvmOverloads constructor( getParentFragmentManager().setFragmentResult(REQUEST_GEOPOLY, Bundle.EMPTY) } + private fun discard() { + val geoString = GeoUtils.formatPointsResultString( + (originalPoly ?: emptyList()).toMutableList(), + outputMode == OutputMode.GEOSHAPE + ) + + val bundle = Bundle() + bundle.putString(RESULT_GEOPOLY, geoString) + getParentFragmentManager().setFragmentResult(REQUEST_GEOPOLY, bundle) + } + private fun onClick(point: MapPoint) { if (inputActive && !recordingEnabled) { appendPointIfNew(point) @@ -427,7 +462,7 @@ class GeoPolyFragment @JvmOverloads constructor( updateUi() } - setResult(RESULT_GEOPOLY_CHANGE) + setChangeResult() } private fun isLocationAcceptable(point: MapPoint): Boolean { @@ -448,7 +483,7 @@ class GeoPolyFragment @JvmOverloads constructor( if (featureId != -1) { map!!.removePolyLineLastPoint(featureId) updateUi() - setResult(RESULT_GEOPOLY_CHANGE) + setChangeResult() } } @@ -572,7 +607,7 @@ class GeoPolyFragment @JvmOverloads constructor( private fun showBackDialog() { MaterialAlertDialogBuilder(requireContext()) .setMessage(getString(org.odk.collect.strings.R.string.geo_exit_warning)) - .setPositiveButton(org.odk.collect.strings.R.string.discard) { _, _ -> cancel() } + .setPositiveButton(org.odk.collect.strings.R.string.discard) { _, _ -> discard() } .setNegativeButton(org.odk.collect.strings.R.string.cancel, null) .show() } diff --git a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt index db8d14efbd7..c55c4c835a1 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt @@ -1,12 +1,10 @@ package org.odk.collect.geo.geopoly import android.app.Application -import android.os.Bundle -import androidx.fragment.app.FragmentResultListener +import androidx.activity.OnBackPressedDispatcher import androidx.lifecycle.Lifecycle import androidx.lifecycle.MutableLiveData import androidx.test.core.app.ApplicationProvider -import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -27,6 +25,7 @@ import org.mockito.Mockito import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.androidtest.FragmentScenarioExtensions.setFragmentResultListener import org.odk.collect.async.Scheduler import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule import org.odk.collect.geo.DaggerGeoDependencyComponent @@ -53,7 +52,7 @@ import org.robolectric.Shadows @RunWith(AndroidJUnit4::class) class GeoPolyFragmentTest { - private val mapFragment = FakeMapFragment() + private val mapFragment = FakeMapFragment(ready = true) private val locationTracker = mock() @get:Rule @@ -102,13 +101,9 @@ class GeoPolyFragmentTest { @Test fun testLocationTrackerLifecycle() { - val scenario = fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { GeoPolyFragment(OutputMode.GEOTRACE) } - .build() - ) - mapFragment.ready() + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment({ OnBackPressedDispatcher() }) + } // Stopping the activity should stop the location tracker scenario.moveToState(Lifecycle.State.DESTROYED) @@ -117,28 +112,20 @@ class GeoPolyFragmentTest { @Test fun recordButton_should_beHiddenForAutomaticMode() { - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { GeoPolyFragment(OutputMode.GEOTRACE) } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment({ OnBackPressedDispatcher() }) + } - mapFragment.ready() startInput(R.id.automatic_mode) onView(withId(R.id.record_button)).check(matches(not(isDisplayed()))) } @Test fun recordButton_should_beVisibleForManualMode() { - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { GeoPolyFragment(OutputMode.GEOTRACE) } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment({ OnBackPressedDispatcher() }) + } - mapFragment.ready() startInput(R.id.manual_mode) onView(withId(R.id.record_button)).check(matches(isDisplayed())) } @@ -147,16 +134,10 @@ class GeoPolyFragmentTest { fun whenPolygonExtraPresent_showsPoly() { val polygon = ArrayList() polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0)) - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, false, false, polygon) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment({ OnBackPressedDispatcher() }, inputPolygon = polygon) + } - mapFragment.ready() val polys = mapFragment.getPolyLines() assertThat(polys.size, equalTo(1)) assertThat(polys[0].points, equalTo(polygon)) @@ -168,16 +149,14 @@ class GeoPolyFragmentTest { polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0)) polygon.add(MapPoint(2.0, 3.0, 3.0, 4.0)) polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0)) - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOSHAPE, false, false, polygon) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOSHAPE, + inputPolygon = polygon + ) + } - mapFragment.ready() val polys = mapFragment.getPolyLines() assertThat(polys.size, equalTo(1)) val expectedPolygon = ArrayList() @@ -189,89 +168,220 @@ class GeoPolyFragmentTest { @Test fun whenPolygonExtraPresent_andPolyIsEmpty_andOutputModeIsShape_doesNotShowPoly() { - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOSHAPE, false, false, emptyList()) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOSHAPE, + inputPolygon = emptyList() + ) + } - mapFragment.ready() val polys = mapFragment.getPolyLines() assertThat(polys.size, equalTo(1)) assertThat(polys[0].points.isEmpty(), equalTo(true)) } @Test - fun whenPolygonExtraPresent_andPolyIsEmpty_pressingBack_setsCancelledResult() { - val scenario = fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, false, false, emptyList()) - } - .build() + fun pressingBack_setsCancelledResult() { + val onBackPressedDispatcher = OnBackPressedDispatcher() + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment({ onBackPressedDispatcher }) + } + + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) + + onBackPressedDispatcher.onBackPressed() + val result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat(result.second.isEmpty, equalTo(true)) + } + + @Test + fun whenInputPolyIsNotEmpty_pressingBack_setsCancelledResult() { + val onBackPressedDispatcher = OnBackPressedDispatcher() + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { onBackPressedDispatcher }, + inputPolygon = listOf(MapPoint(1.0, 1.0)) + ) + } + + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) + + onBackPressedDispatcher.onBackPressed() + val result = resultListener.getAll().lastOrNull() + assertThat(result!!.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat(result.second.isEmpty, equalTo(true)) + } + + @Test + fun whenPolygonHasBeenModified_pressingBack_andClickingCancel_setsNoResult() { + val onBackPressedDispatcher = OnBackPressedDispatcher() + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment({ onBackPressedDispatcher }) + } + + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) + + startInput(R.id.placement_mode) + mapFragment.click(MapPoint(1.0, 1.0)) + resultListener.clear() + + onBackPressedDispatcher.onBackPressed() + Interactions.clickOn(withText(string.cancel), root = isDialog()) + + val result = resultListener.getAll().lastOrNull() + assertThat(result, equalTo(null)) + } + + @Test + fun whenPolygonHasBeenCreated_pressingBack_andClickingDiscard_setsEmptyResult() { + val onBackPressedDispatcher = OnBackPressedDispatcher() + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment({ onBackPressedDispatcher }) + } + + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) + + startInput(R.id.placement_mode) + mapFragment.click(MapPoint(1.0, 1.0)) + mapFragment.click(MapPoint(2.0, 2.0)) + resultListener.clear() + + onBackPressedDispatcher.onBackPressed() + Interactions.clickOn(withText(string.discard), root = isDialog()) + + val result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat(result.second.getString(GeoPolyFragment.RESULT_GEOPOLY), equalTo("")) + } + + @Test + fun whenPolygonHasBeenModified_pressingBack_andClickingDiscard_setsOriginalAsResult() { + val onBackPressedDispatcher = OnBackPressedDispatcher() + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { onBackPressedDispatcher }, + inputPolygon = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0)) + ) + } + + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) + + startInput() + mapFragment.click(MapPoint(2.0, 2.0)) + resultListener.clear() + + onBackPressedDispatcher.onBackPressed() + Interactions.clickOn(withText(string.discard), root = isDialog()) + + val result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat( + result.second.getString(GeoPolyFragment.RESULT_GEOPOLY), + equalTo("0.0 0.0 0.0 0.0;1.0 1.0 0.0 0.0") ) + } + + @Test + fun whenInputPolygonIsOnlyOnePoint_andHasBeenModified_pressingBack_andClickingDiscard_setsOriginalAsResult() { + val onBackPressedDispatcher = OnBackPressedDispatcher() + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { onBackPressedDispatcher }, + inputPolygon = listOf(MapPoint(0.0, 0.0)) + ) + } + + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) - mapFragment.ready() + startInput() + mapFragment.click(MapPoint(2.0, 2.0)) + resultListener.clear() - val resultListener = mock() - scenario.onFragment { - it.parentFragmentManager.setFragmentResultListener( - GeoPolyFragment.REQUEST_GEOPOLY, - it, - resultListener + onBackPressedDispatcher.onBackPressed() + Interactions.clickOn(withText(string.discard), root = isDialog()) + + val result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat( + result.second.getString(GeoPolyFragment.RESULT_GEOPOLY), + equalTo("0.0 0.0 0.0 0.0") + ) + } + + @Test + fun whenPolygonHasBeenModified_recreating_andPressingBack_andClickingDiscard_setsOriginalAsResult() { + val onBackPressedDispatcher = OnBackPressedDispatcher() + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { onBackPressedDispatcher }, + inputPolygon = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0)) ) } + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) - Espresso.pressBack() - verify(resultListener).onFragmentResult(GeoPolyFragment.REQUEST_GEOPOLY, Bundle.EMPTY) + startInput() + mapFragment.click(MapPoint(2.0, 2.0)) + resultListener.clear() + + scenario.recreate() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) + + onBackPressedDispatcher.onBackPressed() + Interactions.clickOn(withText(string.discard), root = isDialog()) + + val result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat( + result.second.getString(GeoPolyFragment.RESULT_GEOPOLY), + equalTo("0.0 0.0 0.0 0.0;1.0 1.0 0.0 0.0") + ) } @Test fun startingInput_usingAutomaticMode_usesRetainMockAccuracyTrueToStartLocationTracker() { - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, false, true, emptyList()) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE, + false, + true, + emptyList() + ) + } - mapFragment.ready() startInput(R.id.automatic_mode) verify(locationTracker).start(true) } @Test fun startingInput_usingAutomaticMode_usesRetainMockAccuracyFalseToStartLocationTracker() { - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, false, false, emptyList()) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE, + false, + false, + emptyList() + ) + } - mapFragment.ready() startInput(R.id.automatic_mode) verify(locationTracker).start(false) } @Test fun recordingPointManually_whenPointIsADuplicateOfTheLastPoint_skipsPoint() { - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { GeoPolyFragment(OutputMode.GEOTRACE) } - .build() - ) + fragmentLauncherRule.launchInContainer { GeoPolyFragment({ OnBackPressedDispatcher() }) } - mapFragment.ready() startInput(R.id.manual_mode) mapFragment.setLocation(MapPoint(1.0, 1.0)) onView(withId(R.id.record_button)).perform(click()) @@ -281,14 +391,8 @@ class GeoPolyFragmentTest { @Test fun placingPoint_whenPointIsADuplicateOfTheLastPoint_skipsPoint() { - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { GeoPolyFragment(OutputMode.GEOTRACE) } - .build() - ) + fragmentLauncherRule.launchInContainer { GeoPolyFragment({ OnBackPressedDispatcher() }) } - mapFragment.ready() startInput(R.id.placement_mode) mapFragment.click(MapPoint(1.0, 1.0)) mapFragment.click(MapPoint(1.0, 1.0)) @@ -299,16 +403,16 @@ class GeoPolyFragmentTest { fun buttonsShouldBeEnabledInEditableMode() { val polyline = ArrayList() polyline.add(MapPoint(1.0, 2.0, 3.0, 4.0)) - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, false, false, polyline) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE, + false, + false, + polyline + ) + } - mapFragment.ready() Assertions.assertEnabled(withContentDescription(string.input_method)) Assertions.assertEnabled(withContentDescription(string.remove_last_point)) Assertions.assertEnabled(withContentDescription(string.clear)) @@ -319,16 +423,16 @@ class GeoPolyFragmentTest { fun buttonsShouldBeDisabledInReadOnlyMode() { val polygon = ArrayList() polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0)) - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, true, false, polygon) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE, + true, + false, + polygon + ) + } - mapFragment.ready() Assertions.assertDisabled(withContentDescription(string.input_method)) Assertions.assertDisabled(withContentDescription(string.remove_last_point)) Assertions.assertDisabled(withContentDescription(string.clear)) @@ -339,16 +443,16 @@ class GeoPolyFragmentTest { fun polyShouldBeDraggableInEditableMode() { val polyline = ArrayList() polyline.add(MapPoint(1.0, 2.0, 3.0, 4.0)) - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, false, false, polyline) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE, + false, + false, + polyline + ) + } - mapFragment.ready() assertThat(mapFragment.isPolyDraggable(0), equalTo(true)) } @@ -356,53 +460,55 @@ class GeoPolyFragmentTest { fun polyShouldNotBeDraggableInReadOnlyMode() { val polygon = ArrayList() polygon.add(MapPoint(1.0, 2.0, 3.0, 4.0)) - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, true, false, polygon) - } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE, + true, + false, + polygon + ) + } - mapFragment.ready() assertThat(mapFragment.isPolyDraggable(0), equalTo(false)) } @Test fun passingRetainMockAccuracyExtra_updatesMapFragmentState() { - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, false, true, emptyList()) - } - .build() - ) - mapFragment.ready() + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE, + false, + true, + emptyList() + ) + } + assertThat(mapFragment.isRetainMockAccuracy(), equalTo(true)) fragmentLauncherRule.launchInContainer( GeoPolyFragment::class.java, factory = FragmentFactoryBuilder() .forClass(GeoPolyFragment::class) { - GeoPolyFragment(OutputMode.GEOTRACE, false, false, emptyList()) + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE, + false, + false, + emptyList() + ) } .build() ) - mapFragment.ready() + assertThat(mapFragment.isRetainMockAccuracy(), equalTo(false)) } @Test fun recreatingTheFragmentWithTheLayersDialogDisplayedDoesNotCrashTheApp() { - val scenario = fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { GeoPolyFragment(OutputMode.GEOTRACE) } - .build() - ) - mapFragment.ready() + val scenario = + fragmentLauncherRule.launchInContainer { GeoPolyFragment({ OnBackPressedDispatcher() }) } onView(withId(R.id.layers)).perform(click()) @@ -413,12 +519,12 @@ class GeoPolyFragmentTest { fun showsAndHidesInvalidMessageSnackbarBasedOnValue() { val invalidMessage = MutableLiveData(null) - fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { GeoPolyFragment(invalidMessage = invalidMessage) } - .build() - ) + fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + invalidMessage = invalidMessage + ) + } val message = "Something is wrong" invalidMessage.value = message @@ -429,66 +535,107 @@ class GeoPolyFragmentTest { } @Test - fun setsChangeResultWheneverAPointIsAdded() { - val scenario = fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { GeoPolyFragment() } - .build() - ) + fun whenOutputModeIsGeoTrace_setsChangeResultWheneverAPointIsAddedAfterTheFirst() { + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOTRACE + ) + } val resultListener = FragmentResultRecorder() - scenario.onFragment { - it.parentFragmentManager.setFragmentResultListener( - GeoPolyFragment.REQUEST_GEOPOLY, - it, - resultListener + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) + + startInput(R.id.placement_mode) + + mapFragment.click(MapPoint(0.0, 0.0)) + var result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat( + result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE), + equalTo("") + ) + + mapFragment.click(MapPoint(1.0, 1.0)) + result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat( + result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE), + equalTo("0.0 0.0 0.0 0.0;1.0 1.0 0.0 0.0") + ) + } + + @Test + fun whenOutputModeIsGeoShape_doesNotSetChangeResultUntilThereAre3Points() { + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + OutputMode.GEOSHAPE ) } - mapFragment.ready() + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) startInput(R.id.placement_mode) + + mapFragment.click(MapPoint(0.0, 0.0)) + var result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat( + result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE), + equalTo("") + ) + + mapFragment.click(MapPoint(1.0, 0.0)) + result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat( + result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE), + equalTo("") + ) + mapFragment.click(MapPoint(1.0, 1.0)) - val result = resultListener.result - assertThat(result!!.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) assertThat( result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE), - equalTo("1.0 1.0 0.0 0.0") + equalTo("0.0 0.0 0.0 0.0;1.0 0.0 0.0 0.0;1.0 1.0 0.0 0.0;0.0 0.0 0.0 0.0") ) } @Test fun setsChangeResultWheneverAPointIsRemoved() { - val scenario = fragmentLauncherRule.launchInContainer( - GeoPolyFragment::class.java, - factory = FragmentFactoryBuilder() - .forClass(GeoPolyFragment::class) { - GeoPolyFragment(inputPolygon = listOf(MapPoint(1.0, 1.0))) - } - .build() - ) - - val resultListener = FragmentResultRecorder() - scenario.onFragment { - it.parentFragmentManager.setFragmentResultListener( - GeoPolyFragment.REQUEST_GEOPOLY, - it, - resultListener + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment( + { OnBackPressedDispatcher() }, + inputPolygon = + listOf( + MapPoint(0.0, 0.0), + MapPoint(1.0, 0.0), + MapPoint(1.0, 1.0) + ) ) } - mapFragment.ready() + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) Interactions.clickOn(withContentDescription(string.remove_last_point)) - val result = resultListener.result - assertThat(result!!.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) - assertThat(result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE), equalTo("")) + val result = resultListener.getAll().last() + assertThat(result.first, equalTo(GeoPolyFragment.REQUEST_GEOPOLY)) + assertThat( + result.second.getString(GeoPolyFragment.RESULT_GEOPOLY_CHANGE), + equalTo("0.0 0.0 0.0 0.0;1.0 0.0 0.0 0.0") + ) } - private fun startInput(mode: Int) { + private fun startInput(mode: Int? = null) { onView(withId(R.id.play)).perform(click()) - onView(withId(mode)).inRoot(isDialog()).perform(click()) - onView(withId(android.R.id.button1)).inRoot(isDialog()).perform(click()) + + if (mode != null) { + onView(withId(mode)).inRoot(isDialog()).perform(click()) + onView(withId(android.R.id.button1)).inRoot(isDialog()).perform(click()) + } } } diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index a8daed41412..81cbee101bf 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -12,7 +12,7 @@ import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription import kotlin.random.Random -class FakeMapFragment : Fragment(), MapFragment { +class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragment { private var clickListener: PointListener? = null private var gpsLocationListener: PointListener? = null @@ -37,6 +37,10 @@ class FakeMapFragment : Fragment(), MapFragment { errorListener: MapFragment.ErrorListener? ) { this.readyListener = readyListener + + if (ready) { + ready() + } } fun ready() { diff --git a/test-shared/src/main/java/org/odk/collect/testshared/FragmentResultRecorder.kt b/test-shared/src/main/java/org/odk/collect/testshared/FragmentResultRecorder.kt index 9177005725a..523f86320eb 100644 --- a/test-shared/src/main/java/org/odk/collect/testshared/FragmentResultRecorder.kt +++ b/test-shared/src/main/java/org/odk/collect/testshared/FragmentResultRecorder.kt @@ -5,9 +5,17 @@ import androidx.fragment.app.FragmentResultListener class FragmentResultRecorder : FragmentResultListener { - var result: Pair? = null + private val results = mutableListOf>() + + fun getAll(): List> { + return results + } + + fun clear() { + results.clear() + } override fun onFragmentResult(requestKey: String, result: Bundle) { - this.result = Pair(requestKey, result) + results.add(Pair(requestKey, result)) } }