From 406eca9df28bb608de3e6d6e7054e72870be8aee Mon Sep 17 00:00:00 2001 From: Veyndan Stuart Date: Tue, 7 Nov 2023 13:00:28 +0100 Subject: [PATCH] Add tests for `BackHandler` composable --- .../cash/redwood/compose/BackHandlerTest.kt | 181 ++++++++++++++++++ .../redwood/testing/TestRedwoodComposition.kt | 20 +- .../tooling/codegen/testingGeneration.kt | 7 +- .../app/cash/redwood/tooling/codegen/types.kt | 2 + 4 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/BackHandlerTest.kt diff --git a/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/BackHandlerTest.kt b/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/BackHandlerTest.kt new file mode 100644 index 0000000000..574adc08e2 --- /dev/null +++ b/redwood-compose/src/commonTest/kotlin/app/cash/redwood/compose/BackHandlerTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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 + * + * http://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 app.cash.redwood.compose + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import app.cash.redwood.layout.compose.Box +import app.cash.redwood.testing.TestRedwoodComposition +import app.cash.redwood.testing.WidgetValue +import app.cash.redwood.ui.Cancellable +import app.cash.redwood.ui.OnBackPressedCallback +import app.cash.redwood.ui.OnBackPressedDispatcher +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.single +import com.example.redwood.testing.compose.Text +import com.example.redwood.testing.widget.TestSchemaTester +import com.example.redwood.testing.widget.TextValue +import kotlin.test.Test +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.test.runTest + +private val throwingOnBack = { error("Only the innermost enabled back handler should be invoked.") } + +class BackHandlerTest { + @Test + fun enabledBackHandler() = runTest { + val onBackPressedDispatcher = FakeOnBackPressedDispatcher() + TestSchemaTester(onBackPressedDispatcher) { + setContent { + var backCounter by remember { mutableStateOf(0) } + BackHandler { + backCounter++ + } + Text(backCounter.toString()) + } + + assertThat(awaitSnapshot()).single().isEqualTo(TextValue(text = "0")) + onBackPressedDispatcher.onBackPressed() + assertThat(awaitSnapshot()).single().isEqualTo(TextValue(text = "1")) + } + } + + @Test + fun disabledBackHandler() = runTest { + val onBackPressedDispatcher = FakeOnBackPressedDispatcher() + TestSchemaTester(onBackPressedDispatcher) { + setContent { + val backCounter by remember { mutableStateOf(0) } + BackHandler(enabled = false, throwingOnBack) + Text(backCounter.toString()) + } + + assertThat(awaitSnapshot()).single().isEqualTo(TextValue(text = "0")) + onBackPressedDispatcher.onBackPressed() + assertNoSnapshot() + } + } + + @Test + fun outermostEnabledAndInnermostEnabledBackHandlers() = runTest { + val onBackPressedDispatcher = FakeOnBackPressedDispatcher() + TestSchemaTester(onBackPressedDispatcher) { + setContent { + var backCounter by remember { mutableStateOf(0) } + BackHandler(enabled = false, throwingOnBack) + Box { + BackHandler { + backCounter += 1 + } + } + Text(backCounter.toString()) + } + + assertThat(awaitSnapshot()).contains(TextValue(text = "0")) + onBackPressedDispatcher.onBackPressed() + assertThat(awaitSnapshot()).contains(TextValue(text = "1")) + } + } + + @Test + fun outermostEnabledAndInnermostDisabledBackHandlers() = runTest { + val onBackPressedDispatcher = FakeOnBackPressedDispatcher() + TestSchemaTester(onBackPressedDispatcher) { + setContent { + var backCounter by remember { mutableStateOf(0) } + BackHandler { + backCounter += 1 + } + Box { + BackHandler(enabled = false, throwingOnBack) + } + Text(backCounter.toString()) + } + + assertThat(awaitSnapshot()).contains(TextValue(text = "0")) + onBackPressedDispatcher.onBackPressed() + assertThat(awaitSnapshot()).contains(TextValue(text = "1")) + } + } + + @Test + fun outermostDisabledAndInnermostEnabledBackHandlers() = runTest { + val onBackPressedDispatcher = FakeOnBackPressedDispatcher() + TestSchemaTester(onBackPressedDispatcher) { + setContent { + var backCounter by remember { mutableStateOf(0) } + BackHandler(enabled = false, throwingOnBack) + Box { + BackHandler { + backCounter += 1 + } + } + Text(backCounter.toString()) + } + + assertThat(awaitSnapshot()).contains(TextValue(text = "0")) + onBackPressedDispatcher.onBackPressed() + assertThat(awaitSnapshot()).contains(TextValue(text = "1")) + } + } + + @Test + fun outermostDisabledAndInnermostDisabledBackHandlers() = runTest { + val onBackPressedDispatcher = FakeOnBackPressedDispatcher() + TestSchemaTester(onBackPressedDispatcher) { + setContent { + val backCounter by remember { mutableStateOf(0) } + BackHandler(enabled = false, throwingOnBack) + Box { + BackHandler(enabled = false, throwingOnBack) + } + Text(backCounter.toString()) + } + + assertThat(awaitSnapshot()).contains(TextValue(text = "0")) + onBackPressedDispatcher.onBackPressed() + assertNoSnapshot() + } + } +} + +private suspend fun TestRedwoodComposition>.assertNoSnapshot() { + assertFailure { awaitSnapshot() }.isInstanceOf() +} + +private class FakeOnBackPressedDispatcher : OnBackPressedDispatcher { + private val onBackPressedCallbacks = ArrayDeque() + + override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable { + onBackPressedCallbacks += onBackPressedCallback + return object : Cancellable { + override fun cancel() { + onBackPressedCallbacks -= onBackPressedCallback + } + } + } + + fun onBackPressed() { + val callbackToNotify = onBackPressedCallbacks.lastOrNull { it.isEnabled } + callbackToNotify?.handleOnBackPressed() + } +} diff --git a/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/TestRedwoodComposition.kt b/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/TestRedwoodComposition.kt index 59315f4468..6a9c45c6f4 100644 --- a/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/TestRedwoodComposition.kt +++ b/redwood-testing/src/commonMain/kotlin/app/cash/redwood/testing/TestRedwoodComposition.kt @@ -44,6 +44,7 @@ public fun TestRedwoodComposition( scope: CoroutineScope, provider: Widget.Provider, container: Widget.Children, + onBackPressedDispatcher: OnBackPressedDispatcher = NoOpOnBackPressedDispatcher, savedState: TestSavedState? = null, initialUiConfiguration: UiConfiguration = UiConfiguration(), createSnapshot: () -> S, @@ -52,6 +53,7 @@ public fun TestRedwoodComposition( scope, provider, container, + onBackPressedDispatcher, savedState, initialUiConfiguration, createSnapshot, @@ -87,6 +89,7 @@ private class RealTestRedwoodComposition( scope: CoroutineScope, provider: Widget.Provider, container: Widget.Children, + onBackPressedDispatcher: OnBackPressedDispatcher, savedState: TestSavedState?, initialUiConfiguration: UiConfiguration, createSnapshot: () -> S, @@ -110,18 +113,13 @@ private class RealTestRedwoodComposition( }, canBeSaved = { true }, ) + override fun saveState() = MapBasedTestSavedState(savedStateRegistry.performSave()) private val composition = RedwoodComposition( scope = scope + clock, container = container, - onBackPressedDispatcher = object : OnBackPressedDispatcher { - override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable { - return object : Cancellable { - override fun cancel() = Unit - } - } - }, + onBackPressedDispatcher = onBackPressedDispatcher, saveableStateRegistry = savedStateRegistry, uiConfigurations = uiConfigurations, provider = provider, @@ -167,3 +165,11 @@ private class RealTestRedwoodComposition( composition.cancel() } } + +public object NoOpOnBackPressedDispatcher : OnBackPressedDispatcher { + override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable { + return object : Cancellable { + override fun cancel() = Unit + } + } +} diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt index 3cb98e1081..de8d72ec13 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/testingGeneration.kt @@ -76,6 +76,11 @@ internal fun generateTester(schemaSet: SchemaSet): FileSpec { FunSpec.builder(testerFunction) .optIn(Redwood.RedwoodCodegenApi) .addModifiers(SUSPEND) + .addParameter( + ParameterSpec.builder("onBackPressedDispatcher", Redwood.OnBackPressedDispatcher) + .defaultValue("%T", RedwoodTesting.NoOpOnBackPressedDispatcher) + .build(), + ) .addParameter( ParameterSpec.builder("savedState", RedwoodTesting.TestSavedState.copy(nullable = true)) .defaultValue("null") @@ -98,7 +103,7 @@ internal fun generateTester(schemaSet: SchemaSet): FileSpec { } .addCode("⇤)\n") .addStatement("val container = %T<%T>()", RedwoodWidget.MutableListChildren, RedwoodTesting.WidgetValue) - .beginControlFlow("val tester = %T(this, factories, container, savedState, uiConfiguration)", RedwoodTesting.TestRedwoodComposition) + .beginControlFlow("val tester = %T(this, factories, container, onBackPressedDispatcher, savedState, uiConfiguration)", RedwoodTesting.TestRedwoodComposition) .addStatement("container.map { it.value }") .endControlFlow() .beginControlFlow("try") diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt index 5130746f29..10ce0425c7 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/types.kt @@ -63,10 +63,12 @@ internal object Redwood { val ModifierElement = Modifier.nestedClass("Element") val LayoutScopeMarker = ClassName("app.cash.redwood", "LayoutScopeMarker") val RedwoodCodegenApi = ClassName("app.cash.redwood", "RedwoodCodegenApi") + val OnBackPressedDispatcher = ClassName("app.cash.redwood.ui", "OnBackPressedDispatcher") val UiConfiguration = ClassName("app.cash.redwood.ui", "UiConfiguration") } internal object RedwoodTesting { + val NoOpOnBackPressedDispatcher = ClassName("app.cash.redwood.testing", "NoOpOnBackPressedDispatcher") val TestRedwoodComposition = ClassName("app.cash.redwood.testing", "TestRedwoodComposition") val TestSavedState = ClassName("app.cash.redwood.testing", "TestSavedState") val WidgetValue = ClassName("app.cash.redwood.testing", "WidgetValue")