From 1d57b5d6c6bec272f940ef695919a32cce74b3a6 Mon Sep 17 00:00:00 2001 From: Zachary Klippenstein Date: Mon, 15 Apr 2019 17:51:25 -0700 Subject: [PATCH] Pass the workflow's scope into initialState. Closes #288. --- .../sample/authworkflow/AuthWorkflow.kt | 6 +- .../sample/gameworkflow/RunGameWorkflow.kt | 4 +- .../sample/gameworkflow/TakeTurnsWorkflow.kt | 4 +- .../sample/mainworkflow/MainWorkflow.kt | 6 +- .../com/squareup/workflow/StatefulWorkflow.kt | 9 +- .../squareup/workflow/StatelessWorkflow.kt | 4 +- .../com/squareup/workflow/WorkflowContext.kt | 1 + .../workflow/internal/WorkflowNode.kt | 4 +- .../internal/RealWorkflowContextTest.kt | 6 +- .../workflow/internal/SubtreeManagerTest.kt | 4 +- .../workflow/internal/WorkflowNodeTest.kt | 39 ++++++--- .../workflow/rx2/SubscriptionsTest.kt | 4 +- .../workflow/rx2/WorkflowContextsTest.kt | 4 +- .../ChannelSubscriptionsIntegrationTest.kt | 4 +- .../workflow/CompositionIntegrationTest.kt | 82 ++++++++++++++++++- .../com/squareup/workflow/TreeWorkflow.kt | 4 +- .../squareup/workflow/WorkflowTesterTest.kt | 10 ++- 17 files changed, 160 insertions(+), 35 deletions(-) diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt index a317c07ce..925f48004 100644 --- a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt +++ b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt @@ -25,7 +25,6 @@ import com.squareup.sample.authworkflow.AuthState.SecondFactorPrompt import com.squareup.sample.authworkflow.LoginScreen.SubmitLogin import com.squareup.sample.authworkflow.SecondFactorScreen.Event.CancelSecondFactor import com.squareup.sample.authworkflow.SecondFactorScreen.Event.SubmitSecondFactor -import com.squareup.workflow.ui.BackStackScreen import com.squareup.workflow.Snapshot import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.Workflow @@ -33,6 +32,8 @@ import com.squareup.workflow.WorkflowAction.Companion.emitOutput import com.squareup.workflow.WorkflowAction.Companion.enterState import com.squareup.workflow.WorkflowContext import com.squareup.workflow.rx2.onSuccess +import com.squareup.workflow.ui.BackStackScreen +import kotlinx.coroutines.CoroutineScope /** * We define this otherwise redundant typealias to keep composite workflows @@ -55,7 +56,8 @@ class RealAuthWorkflow(private val authService: AuthService) : AuthWorkflow, override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): AuthState = LoginPrompt() override fun compose( diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt index 65e36b05b..39bf4075b 100644 --- a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt +++ b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt @@ -51,6 +51,7 @@ import com.squareup.workflow.WorkflowAction.Companion.enterState import com.squareup.workflow.WorkflowContext import com.squareup.workflow.invoke import com.squareup.workflow.rx2.onSuccess +import kotlinx.coroutines.CoroutineScope enum class RunGameResult { CanceledStart, @@ -79,7 +80,8 @@ class RealRunGameWorkflow( override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): RunGameState = snapshot?.let { RunGameState.fromSnapshot(snapshot.bytes) } ?: NewGame() diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt index e6144c53a..1196f3eae 100644 --- a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt +++ b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt @@ -28,6 +28,7 @@ import com.squareup.workflow.WorkflowAction.Companion.emitOutput import com.squareup.workflow.WorkflowAction.Companion.enterState import com.squareup.workflow.WorkflowAction.Companion.noop import com.squareup.workflow.WorkflowContext +import kotlinx.coroutines.CoroutineScope typealias TakeTurnsWorkflow = Workflow @@ -43,7 +44,8 @@ class RealTakeTurnsWorkflow : TakeTurnsWorkflow, override fun initialState( input: PlayerInfo, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): Turn = Turn() override fun compose( diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainWorkflow.kt b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainWorkflow.kt index ca4967ec5..2e126e352 100644 --- a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainWorkflow.kt +++ b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainWorkflow.kt @@ -23,13 +23,14 @@ import com.squareup.sample.gameworkflow.RunGameWorkflow import com.squareup.sample.mainworkflow.MainState.Authenticating import com.squareup.sample.mainworkflow.MainState.RunningGame import com.squareup.sample.panel.asPanelOver -import com.squareup.workflow.ui.AlertContainerScreen import com.squareup.workflow.Snapshot import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.Workflow import com.squareup.workflow.WorkflowAction.Companion.enterState import com.squareup.workflow.WorkflowContext import com.squareup.workflow.composeChild +import com.squareup.workflow.ui.AlertContainerScreen +import kotlinx.coroutines.CoroutineScope /** * Application specific root [Workflow], and demonstration of workflow composition. @@ -49,7 +50,8 @@ class MainWorkflow( override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): MainState = snapshot?.let { MainState.fromSnapshot(snapshot.bytes) } ?: Authenticating diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt index 9c5a69255..527ab3951 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt @@ -15,6 +15,8 @@ */ package com.squareup.workflow +import kotlinx.coroutines.CoroutineScope + /** * A composable, stateful object that can [handle events][WorkflowContext.onEvent], * [delegate to children][WorkflowContext.composeChild], [subscribe][onReceive] to arbitrary streams from @@ -71,10 +73,15 @@ abstract class StatefulWorkflow< * If the workflow is being restored from a [Snapshot], [snapshot] will be the last value * returned from [snapshotState], and implementations that return something other than * [Snapshot.EMPTY] should create their initial state by parsing their snapshot. + * @param scope + * The [CoroutineScope] in which this workflow lives. The scope will be cancelled when the + * workflow is being torn down, so this scope can be used to start coroutines to track the + * lifetime of the workflow "session". */ abstract fun initialState( input: InputT, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): StateT /** diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt index 842aab88f..b39c95742 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt @@ -16,6 +16,7 @@ package com.squareup.workflow import com.squareup.workflow.WorkflowAction.Companion.emitOutput +import kotlinx.coroutines.CoroutineScope /** * Minimal implementation of [Workflow] that maintains no state of its own. @@ -40,7 +41,8 @@ abstract class StatelessWorkflow private val statefulWorkflow = object : StatefulWorkflow() { override fun initialState( input: InputT, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ) = Unit @Suppress("UNCHECKED_CAST") diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowContext.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowContext.kt index f96214ac4..fc677dd79 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowContext.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowContext.kt @@ -143,6 +143,7 @@ interface WorkflowContext { * Teardown handlers should be non-blocking and execute quickly, since they are invoked * synchronously during the compose pass. */ + @Deprecated("Use the CoroutineScope parameter to initialState.") fun onTeardown(handler: () -> Unit) } diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt b/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt index 9e4f64ebd..04430e32a 100644 --- a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt +++ b/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt @@ -90,7 +90,7 @@ internal class WorkflowNode val stateSnapshot = source.readByteStringWithLength() val childrenSnapshot = source.readByteString() - val state = workflow.initialState(input, Snapshot.of(stateSnapshot)) + val state = workflow.initialState(input, Snapshot.of(stateSnapshot), this@WorkflowNode) return Pair(state, Snapshot.of(childrenSnapshot)) } } diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealWorkflowContextTest.kt b/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealWorkflowContextTest.kt index 027ee4435..778cab4f5 100644 --- a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealWorkflowContextTest.kt +++ b/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealWorkflowContextTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:Suppress("EXPERIMENTAL_API_USAGE") +@file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION") package com.squareup.workflow.internal @@ -28,6 +28,7 @@ import com.squareup.workflow.internal.Behavior.WorkflowOutputCase import com.squareup.workflow.internal.RealWorkflowContext.Composer import com.squareup.workflow.internal.RealWorkflowContextTest.TestComposer.Rendering import com.squareup.workflow.stateless +import kotlinx.coroutines.CoroutineScope import kotlin.reflect.full.starProjectedType import kotlin.test.Test import kotlin.test.assertEquals @@ -62,7 +63,8 @@ class RealWorkflowContextTest { private class TestWorkflow : StatefulWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String = fail() override fun compose( diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt b/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt index d4450d27f..da5808da5 100644 --- a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt +++ b/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt @@ -24,6 +24,7 @@ import com.squareup.workflow.WorkflowAction.Companion.emitOutput import com.squareup.workflow.WorkflowContext import com.squareup.workflow.internal.Behavior.WorkflowOutputCase import com.squareup.workflow.internal.SubtreeManagerTest.TestWorkflow.Rendering +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking @@ -47,7 +48,8 @@ class SubtreeManagerTest { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String { started++ return "initialState:$input" diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt b/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt index 168931b29..c8139475b 100644 --- a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt +++ b/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:Suppress("EXPERIMENTAL_API_USAGE") +@file:Suppress("EXPERIMENTAL_API_USAGE", "DEPRECATION") package com.squareup.workflow.internal @@ -34,6 +34,7 @@ import com.squareup.workflow.util.ChannelUpdate import com.squareup.workflow.util.ChannelUpdate.Closed import com.squareup.workflow.util.ChannelUpdate.Value import com.squareup.workflow.writeUtf8WithLength +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Unconfined import kotlinx.coroutines.TimeoutCancellationException @@ -63,7 +64,8 @@ class WorkflowNodeTest { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String { assertNull(snapshot) return "starting:$input" @@ -132,7 +134,8 @@ class WorkflowNodeTest { val workflow = object : StringWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String { assertNull(snapshot) return input @@ -166,7 +169,8 @@ class WorkflowNodeTest { val workflow = object : StringWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String { assertNull(snapshot) return input @@ -205,7 +209,8 @@ class WorkflowNodeTest { val workflow = object : StringWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String { assertNull(snapshot) return input @@ -262,7 +267,8 @@ class WorkflowNodeTest { val workflow = object : StringWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String { assertNull(snapshot) return input @@ -307,7 +313,8 @@ class WorkflowNodeTest { val workflow = object : StringWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String { assertNull(snapshot) return input @@ -358,7 +365,8 @@ class WorkflowNodeTest { val workflow = object : StatefulWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String = snapshot?.bytes?.parse { it.readUtf8WithLength() .removePrefix("state:") @@ -401,7 +409,8 @@ class WorkflowNodeTest { val workflow = object : StatefulWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String = if (snapshot != null) "restored" else input override fun compose( @@ -443,7 +452,8 @@ class WorkflowNodeTest { val childWorkflow = object : StatefulWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String = snapshot?.bytes?.parse { it.readUtf8WithLength() .removePrefix("child state:") @@ -465,7 +475,8 @@ class WorkflowNodeTest { val parentWorkflow = object : StatefulWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String = snapshot?.bytes?.parse { it.readUtf8WithLength() .removePrefix("parent state:") @@ -519,7 +530,8 @@ class WorkflowNodeTest { val workflow = object : StatefulWorkflow() { override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ) { if (snapshot != null) { restoreCalls++ @@ -568,7 +580,8 @@ class WorkflowNodeTest { val workflow = object : StatefulWorkflow() { override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String = snapshot?.bytes?.parse { // Tags the restored state with the input so we can check it. val deserialized = it.readUtf8WithLength() diff --git a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/SubscriptionsTest.kt b/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/SubscriptionsTest.kt index 0ffd22b28..b69d475b0 100644 --- a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/SubscriptionsTest.kt +++ b/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/SubscriptionsTest.kt @@ -26,6 +26,7 @@ import com.squareup.workflow.util.ChannelUpdate.Closed import com.squareup.workflow.util.ChannelUpdate.Value import io.reactivex.Observable import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException import java.io.IOException import kotlin.test.Test @@ -59,7 +60,8 @@ class SubscriptionsTest { override fun initialState( input: Boolean, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): Boolean = input override fun compose( diff --git a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/WorkflowContextsTest.kt b/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/WorkflowContextsTest.kt index 0a70668b9..178879efd 100644 --- a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/WorkflowContextsTest.kt +++ b/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/WorkflowContextsTest.kt @@ -27,6 +27,7 @@ import com.squareup.workflow.invoke import com.squareup.workflow.stateless import com.squareup.workflow.testing.testFromStart import io.reactivex.subjects.SingleSubject +import kotlinx.coroutines.CoroutineScope import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -73,7 +74,8 @@ class WorkflowContextsTest { val workflow = object : StatefulWorkflow() { override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): Boolean = true override fun compose( diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/ChannelSubscriptionsIntegrationTest.kt b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/ChannelSubscriptionsIntegrationTest.kt index 0074176a1..43bc866aa 100644 --- a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/ChannelSubscriptionsIntegrationTest.kt +++ b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/ChannelSubscriptionsIntegrationTest.kt @@ -24,6 +24,7 @@ import com.squareup.workflow.util.ChannelUpdate import com.squareup.workflow.util.ChannelUpdate.Closed import com.squareup.workflow.util.ChannelUpdate.Value import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel @@ -60,7 +61,8 @@ class ChannelSubscriptionsIntegrationTest { override fun initialState( input: Boolean, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): Boolean = input override fun compose( diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/CompositionIntegrationTest.kt b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/CompositionIntegrationTest.kt index 923ed7bb0..e265318ae 100644 --- a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/CompositionIntegrationTest.kt +++ b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/CompositionIntegrationTest.kt @@ -17,6 +17,10 @@ package com.squareup.workflow import com.squareup.workflow.WorkflowAction.Companion.enterState import com.squareup.workflow.testing.testFromStart +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -107,7 +111,8 @@ class CompositionIntegrationTest { val root = object : StatefulWorkflow Unit>() { override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): Boolean = true override fun compose( @@ -150,7 +155,8 @@ class CompositionIntegrationTest { val root = object : StatefulWorkflow Unit>() { override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): Boolean = true override fun compose( @@ -177,4 +183,76 @@ class CompositionIntegrationTest { } } } + + @Test fun `childrens' initialState scope is run during child session`() { + var starts = 0 + var cancels = 0 + val child = object : StatefulWorkflow() { + override fun initialState( + input: Unit, + snapshot: Snapshot?, + scope: CoroutineScope + ) { + scope.launch { + starts++ + try { + suspendCancellableCoroutine { } + } catch (e: CancellationException) { + cancels++ + } + } + } + + override fun compose( + input: Unit, + state: Unit, + context: WorkflowContext + ) { + } + + override fun snapshotState(state: Unit) = Snapshot.EMPTY + } + + // A workflow that will render child until its rendering is invoked, at which point + // it will compose neither of them, which should trigger the scope to be cancelled. + val root = object : StatefulWorkflow Unit>() { + override fun initialState( + input: Unit, + snapshot: Snapshot?, + scope: CoroutineScope + ): Boolean = false + + override fun compose( + input: Unit, + state: Boolean, + context: WorkflowContext + ): (Boolean) -> Unit { + if (state) { + context.composeChild(child) + } + return context.onEvent { enterState(it) }::invoke + } + + override fun snapshotState(state: Boolean): Snapshot = Snapshot.EMPTY + } + + root.testFromStart { tester -> + tester.withNextRendering { runChildren -> + assertEquals(0, starts) + assertEquals(0, cancels) + + runChildren(true) + } + + tester.withNextRendering { runChildren -> + assertEquals(1, starts) + assertEquals(0, cancels) + + runChildren(false) + + assertEquals(1, starts) + assertEquals(1, cancels) + } + } + } } diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt index 0d891506f..488acb889 100644 --- a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt +++ b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt @@ -16,6 +16,7 @@ package com.squareup.workflow import com.squareup.workflow.TreeWorkflow.Rendering +import kotlinx.coroutines.CoroutineScope /** * A [Workflow] that has a simple string state and can be configured with children at construction. @@ -45,7 +46,8 @@ internal class TreeWorkflow( override fun initialState( input: String, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ): String = snapshot?.bytes?.parse { it.readUtf8WithLength() } ?: input diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowTesterTest.kt b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowTesterTest.kt index c68b2637a..d9251a0fe 100644 --- a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowTesterTest.kt +++ b/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowTesterTest.kt @@ -18,6 +18,7 @@ package com.squareup.workflow import com.squareup.workflow.testing.testFromStart import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlin.test.Test import kotlin.test.assertFailsWith @@ -75,7 +76,8 @@ class WorkflowTesterTest { val workflow = object : StatefulWorkflow() { override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ) { assertNull(snapshot) throw ExpectedException() @@ -103,7 +105,8 @@ class WorkflowTesterTest { val workflow = object : StatefulWorkflow() { override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ) { assertNull(snapshot) // Noop @@ -131,7 +134,8 @@ class WorkflowTesterTest { val workflow = object : StatefulWorkflow() { override fun initialState( input: Unit, - snapshot: Snapshot? + snapshot: Snapshot?, + scope: CoroutineScope ) { if (snapshot != null) { throw ExpectedException()