Skip to content

Commit d65746e

Browse files
committed
Add awaitNextSnapshot and awaitNextOutput to WorkflowTurbine
1 parent b9e854e commit d65746e

File tree

2 files changed

+398
-19
lines changed

2 files changed

+398
-19
lines changed

workflow-testing/src/main/java/com/squareup/workflow1/testing/WorkflowTurbine.kt

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package com.squareup.workflow1.testing
22

33
import app.cash.turbine.ReceiveTurbine
44
import app.cash.turbine.test
5+
import app.cash.turbine.testIn
6+
import app.cash.turbine.turbineScope
57
import com.squareup.workflow1.RuntimeConfig
8+
import com.squareup.workflow1.TreeSnapshot
69
import com.squareup.workflow1.Workflow
710
import com.squareup.workflow1.WorkflowInterceptor
811
import com.squareup.workflow1.config.JvmTestRuntimeConfigTools
@@ -11,11 +14,15 @@ import com.squareup.workflow1.testing.WorkflowTurbine.Companion.WORKFLOW_TEST_DE
1114
import kotlinx.coroutines.CoroutineScope
1215
import kotlinx.coroutines.ExperimentalCoroutinesApi
1316
import kotlinx.coroutines.cancel
17+
import kotlinx.coroutines.channels.Channel
1418
import kotlinx.coroutines.flow.MutableStateFlow
19+
import kotlinx.coroutines.flow.receiveAsFlow
1520
import kotlinx.coroutines.flow.StateFlow
1621
import kotlinx.coroutines.flow.asStateFlow
22+
import kotlinx.coroutines.flow.SharingStarted
1723
import kotlinx.coroutines.flow.drop
1824
import kotlinx.coroutines.flow.map
25+
import kotlinx.coroutines.flow.shareIn
1926
import kotlinx.coroutines.test.UnconfinedTestDispatcher
2027
import kotlinx.coroutines.test.runTest
2128
import 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

Comments
 (0)