@@ -2,7 +2,10 @@ package com.squareup.workflow1.testing
22
33import app.cash.turbine.ReceiveTurbine
44import app.cash.turbine.test
5+ import app.cash.turbine.testIn
6+ import app.cash.turbine.turbineScope
57import com.squareup.workflow1.RuntimeConfig
8+ import com.squareup.workflow1.TreeSnapshot
69import com.squareup.workflow1.Workflow
710import com.squareup.workflow1.WorkflowInterceptor
811import com.squareup.workflow1.config.JvmTestRuntimeConfigTools
@@ -11,11 +14,15 @@ import com.squareup.workflow1.testing.WorkflowTurbine.Companion.WORKFLOW_TEST_DE
1114import kotlinx.coroutines.CoroutineScope
1215import kotlinx.coroutines.ExperimentalCoroutinesApi
1316import kotlinx.coroutines.cancel
17+ import kotlinx.coroutines.channels.Channel
1418import kotlinx.coroutines.flow.MutableStateFlow
19+ import kotlinx.coroutines.flow.receiveAsFlow
1520import kotlinx.coroutines.flow.StateFlow
1621import kotlinx.coroutines.flow.asStateFlow
22+ import kotlinx.coroutines.flow.SharingStarted
1723import kotlinx.coroutines.flow.drop
1824import kotlinx.coroutines.flow.map
25+ import kotlinx.coroutines.flow.shareIn
1926import kotlinx.coroutines.test.UnconfinedTestDispatcher
2027import kotlinx.coroutines.test.runTest
2128import kotlin.coroutines.CoroutineContext
@@ -46,7 +53,7 @@ public fun <PropsT, OutputT, RenderingT> Workflow<PropsT, OutputT, RenderingT>.r
4653 runtimeConfig : RuntimeConfig = JvmTestRuntimeConfigTools .getTestRuntimeConfig(),
4754 onOutput : suspend (OutputT ) -> Unit = {},
4855 testTimeout : Long = WORKFLOW_TEST_DEFAULT_TIMEOUT_MS ,
49- testCase : suspend WorkflowTurbine <RenderingT >.() -> Unit
56+ testCase : suspend WorkflowTurbine <RenderingT , OutputT >.() -> Unit
5057) {
5158 val workflow = this
5259
@@ -57,28 +64,59 @@ public fun <PropsT, OutputT, RenderingT> Workflow<PropsT, OutputT, RenderingT>.r
5764 // We use a sub-scope so that we can cancel the Workflow runtime when we are done with it so that
5865 // tests don't all have to do that themselves.
5966 val workflowRuntimeScope = CoroutineScope (coroutineContext)
67+
68+ // Capture outputs in a channel
69+ val outputsChannel = Channel <OutputT >(Channel .UNLIMITED )
70+
6071 val renderings = renderWorkflowIn(
6172 workflow = workflow,
6273 props = props,
6374 scope = workflowRuntimeScope,
6475 interceptors = interceptors,
6576 runtimeConfig = runtimeConfig,
66- onOutput = onOutput
77+ onOutput = { output ->
78+ outputsChannel.send(output)
79+ onOutput(output)
80+ }
6781 )
6882
6983 val firstRendering = renderings.value.rendering
84+ val firstSnapshot = renderings.value.snapshot
85+
86+ // Share the RenderingAndSnapshot flow so multiple subscribers can collect from it
87+ // Use workflowRuntimeScope so it's cancelled when the workflow is cancelled
88+ val sharedRenderings = renderings.drop(1 )
89+ .shareIn(
90+ scope = workflowRuntimeScope,
91+ started = SharingStarted .Eagerly ,
92+ replay = 0
93+ )
94+
95+ // Use turbineScope to test multiple flows
96+ turbineScope {
97+ // Map the shared flow to extract renderings and snapshots separately
98+ val renderingTurbine = sharedRenderings.map { it.rendering }
99+ .testIn(backgroundScope, timeout = testTimeout.milliseconds, name = " renderings" )
100+ val snapshotTurbine = sharedRenderings.map { it.snapshot }
101+ .testIn(backgroundScope, timeout = testTimeout.milliseconds, name = " snapshots" )
102+ val outputTurbine = outputsChannel.receiveAsFlow()
103+ .testIn(backgroundScope, timeout = testTimeout.milliseconds, name = " outputs" )
70104
71- // Drop one as its provided separately via `firstRendering`.
72- renderings.drop(1 ).map {
73- it.rendering
74- }.test {
75105 val workflowTurbine = WorkflowTurbine (
76- firstRendering,
77- this
106+ firstRendering = firstRendering,
107+ firstSnapshot = firstSnapshot,
108+ renderingTurbine = renderingTurbine,
109+ snapshotTurbine = snapshotTurbine,
110+ outputTurbine = outputTurbine
78111 )
79112 workflowTurbine.testCase()
80- cancelAndIgnoreRemainingEvents()
113+
114+ // Cancel all turbines
115+ renderingTurbine.cancel()
116+ snapshotTurbine.cancel()
117+ outputTurbine.cancel()
81118 }
119+
82120 workflowRuntimeScope.cancel()
83121 }
84122}
@@ -94,7 +132,7 @@ public fun <OutputT, RenderingT> Workflow<Unit, OutputT, RenderingT>.renderForTe
94132 runtimeConfig : RuntimeConfig = JvmTestRuntimeConfigTools .getTestRuntimeConfig(),
95133 onOutput : suspend (OutputT ) -> Unit = {},
96134 testTimeout : Long = WORKFLOW_TEST_DEFAULT_TIMEOUT_MS ,
97- testCase : suspend WorkflowTurbine <RenderingT >.() -> Unit
135+ testCase : suspend WorkflowTurbine <RenderingT , OutputT >.() -> Unit
98136): Unit = renderForTest(
99137 props = MutableStateFlow (Unit ).asStateFlow(),
100138 coroutineContext = coroutineContext,
@@ -111,12 +149,18 @@ public fun <OutputT, RenderingT> Workflow<Unit, OutputT, RenderingT>.renderForTe
111149 *
112150 * @property firstRendering The first rendering of the Workflow runtime is made synchronously. This is
113151 * provided separately if any assertions or operations are needed from it.
152+ * @property firstSnapshot The first snapshot of the Workflow runtime is made synchronously. This is
153+ * provided separately if any assertions or operations are needed from it.
114154 */
115- public class WorkflowTurbine <RenderingT >(
155+ public class WorkflowTurbine <RenderingT , OutputT >(
116156 public val firstRendering : RenderingT ,
117- private val receiveTurbine : ReceiveTurbine <RenderingT >
157+ public val firstSnapshot : TreeSnapshot ,
158+ private val renderingTurbine : ReceiveTurbine <RenderingT >,
159+ private val snapshotTurbine : ReceiveTurbine <TreeSnapshot >,
160+ private val outputTurbine : ReceiveTurbine <OutputT >,
118161) {
119- private var usedFirst = false
162+ private var usedFirstRendering = false
163+ private var usedFirstSnapshot = false
120164
121165 /* *
122166 * Suspend waiting for the next rendering to be produced by the Workflow runtime. Note this includes
@@ -125,23 +169,44 @@ public class WorkflowTurbine<RenderingT>(
125169 * @return the rendering.
126170 */
127171 public suspend fun awaitNextRendering (): RenderingT {
128- if (! usedFirst ) {
129- usedFirst = true
172+ if (! usedFirstRendering ) {
173+ usedFirstRendering = true
130174 return firstRendering
131175 }
132- return receiveTurbine.awaitItem()
176+ return renderingTurbine.awaitItem()
177+ }
178+
179+ /* *
180+ * Suspend waiting for the next output to be produced by the Workflow runtime.
181+ *
182+ * @return the output.
183+ */
184+ public suspend fun awaitNextOutput (): OutputT = outputTurbine.awaitItem()
185+
186+ /* *
187+ * Suspend waiting for the next snapshot to be produced by the Workflow runtime. Note this includes
188+ * the first (synchronously made) snapshot.
189+ *
190+ * @return the snapshot.
191+ */
192+ public suspend fun awaitNextSnapshot (): TreeSnapshot {
193+ if (! usedFirstSnapshot) {
194+ usedFirstSnapshot = true
195+ return firstSnapshot
196+ }
197+ return snapshotTurbine.awaitItem()
133198 }
134199
135200 public suspend fun skipRenderings (count : Int ) {
136- val skippedCount = if (! usedFirst ) {
137- usedFirst = true
201+ val skippedCount = if (! usedFirstRendering ) {
202+ usedFirstRendering = true
138203 count - 1
139204 } else {
140205 count
141206 }
142207
143208 if (skippedCount > 0 ) {
144- receiveTurbine .skipItems(skippedCount)
209+ renderingTurbine .skipItems(skippedCount)
145210 }
146211 }
147212
0 commit comments