Skip to content

Commit c43729f

Browse files
Add testRender with CoroutineScope for SessionWorkflow
Fixes #1138
1 parent a504d79 commit c43729f

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

workflow-testing/api/workflow-testing.api

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public final class com/squareup/workflow1/testing/RenderTesterKt {
6464
public static synthetic fun expectWorkflow$default (Lcom/squareup/workflow1/testing/RenderTester;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
6565
public static synthetic fun expectWorkflow$default (Lcom/squareup/workflow1/testing/RenderTester;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
6666
public static synthetic fun expectWorkflow$default (Lcom/squareup/workflow1/testing/RenderTester;Lkotlin/reflect/KClass;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
67+
public static final fun testRender (Lcom/squareup/workflow1/SessionWorkflow;Ljava/lang/Object;Lkotlinx/coroutines/CoroutineScope;)Lcom/squareup/workflow1/testing/RenderTester;
6768
public static final fun testRender (Lcom/squareup/workflow1/StatefulWorkflow;Ljava/lang/Object;Ljava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
6869
public static final fun testRender (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
6970
}

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package com.squareup.workflow1.testing
22

33
import com.squareup.workflow1.ActionApplied
4+
import com.squareup.workflow1.SessionWorkflow
45
import com.squareup.workflow1.StatefulWorkflow
56
import com.squareup.workflow1.Workflow
67
import com.squareup.workflow1.WorkflowAction
8+
import com.squareup.workflow1.WorkflowExperimentalApi
79
import com.squareup.workflow1.WorkflowIdentifier
810
import com.squareup.workflow1.WorkflowOutput
911
import com.squareup.workflow1.identifier
1012
import com.squareup.workflow1.testing.RenderTester.ChildWorkflowMatch
1113
import com.squareup.workflow1.workflowIdentifier
14+
import kotlinx.coroutines.CoroutineScope
1215
import kotlin.reflect.KClass
1316
import kotlin.reflect.KType
1417
import kotlin.reflect.KTypeProjection
@@ -19,14 +22,40 @@ import kotlin.reflect.KTypeProjection
1922
*
2023
* See [RenderTester] for usage documentation.
2124
*/
25+
@OptIn(WorkflowExperimentalApi::class) // Opt-in is only for the argument check.
2226
@Suppress("UNCHECKED_CAST")
2327
public fun <PropsT, OutputT, RenderingT> Workflow<PropsT, OutputT, RenderingT>.testRender(
2428
props: PropsT
2529
): RenderTester<PropsT, *, OutputT, RenderingT> {
2630
val statefulWorkflow = asStatefulWorkflow() as StatefulWorkflow<PropsT, Any?, OutputT, RenderingT>
2731
return statefulWorkflow.testRender(
2832
props = props,
29-
initialState = statefulWorkflow.initialState(props, null)
33+
initialState = run {
34+
require(this !is SessionWorkflow<PropsT, *, OutputT, RenderingT>) {
35+
"Called testRender on a SessionWorkflow without a CoroutineScope. Use the version that passes a CoroutineScope."
36+
}
37+
statefulWorkflow.initialState(props, null)
38+
}
39+
) as RenderTester<PropsT, Nothing, OutputT, RenderingT>
40+
}
41+
42+
/**
43+
* Create a [RenderTester] to unit test an individual render pass of this [SessionWorkflow],
44+
* using the workflow's [initial state][StatefulWorkflow.initialState], in the [workflowScope].
45+
*
46+
* See [RenderTester] for usage documentation.
47+
*/
48+
@OptIn(WorkflowExperimentalApi::class)
49+
@Suppress("UNCHECKED_CAST")
50+
public fun <PropsT, OutputT, RenderingT> SessionWorkflow<PropsT, *, OutputT, RenderingT>.testRender(
51+
props: PropsT,
52+
workflowScope: CoroutineScope
53+
): RenderTester<PropsT, *, OutputT, RenderingT> {
54+
val sessionWorkflow: SessionWorkflow<PropsT, Any?, OutputT, RenderingT> =
55+
asStatefulWorkflow() as SessionWorkflow<PropsT, Any?, OutputT, RenderingT>
56+
return sessionWorkflow.testRender(
57+
props = props,
58+
initialState = sessionWorkflow.initialState(props, null, workflowScope)
3059
) as RenderTester<PropsT, Nothing, OutputT, RenderingT>
3160
}
3261

workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt

+94
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.squareup.workflow1.Worker
1010
import com.squareup.workflow1.Workflow
1111
import com.squareup.workflow1.WorkflowAction
1212
import com.squareup.workflow1.WorkflowAction.Companion.noAction
13+
import com.squareup.workflow1.WorkflowExperimentalApi
1314
import com.squareup.workflow1.WorkflowIdentifier
1415
import com.squareup.workflow1.WorkflowOutput
1516
import com.squareup.workflow1.action
@@ -19,14 +20,18 @@ import com.squareup.workflow1.identifier
1920
import com.squareup.workflow1.renderChild
2021
import com.squareup.workflow1.rendering
2122
import com.squareup.workflow1.runningWorker
23+
import com.squareup.workflow1.sessionWorkflow
2224
import com.squareup.workflow1.stateful
2325
import com.squareup.workflow1.stateless
2426
import com.squareup.workflow1.testing.RenderTester.ChildWorkflowMatch.Matched
2527
import com.squareup.workflow1.unsnapshottableIdentifier
2628
import com.squareup.workflow1.workflowIdentifier
2729
import io.mockk.mockk
30+
import kotlinx.coroutines.CoroutineScope
2831
import kotlinx.coroutines.flow.Flow
2932
import kotlinx.coroutines.flow.emptyFlow
33+
import kotlinx.coroutines.sync.Mutex
34+
import kotlinx.coroutines.test.runTest
3035
import org.mockito.kotlin.mock
3136
import kotlin.reflect.typeOf
3237
import kotlin.test.Test
@@ -1263,6 +1268,95 @@ internal class RealRenderTesterTest {
12631268
assertEquals(2, renderCount)
12641269
}
12651270

1271+
@OptIn(WorkflowExperimentalApi::class)
1272+
@Test
1273+
fun `testRender with SessionWorkflow throws exception`() {
1274+
class TestAction : WorkflowAction<Unit, String, String>() {
1275+
override fun Updater.apply() {
1276+
state = "new state"
1277+
setOutput("output")
1278+
}
1279+
}
1280+
1281+
val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
1282+
initialState = { _, _: CoroutineScope -> "initial" },
1283+
render = { _, _ ->
1284+
actionSink.contraMap { it }
1285+
}
1286+
)
1287+
1288+
val exception = assertFailsWith<IllegalArgumentException> {
1289+
workflow.testRender(Unit)
1290+
.render { sink ->
1291+
sink.send(TestAction())
1292+
}
1293+
}
1294+
1295+
assertEquals(
1296+
exception.message,
1297+
"Called testRender on a SessionWorkflow without a CoroutineScope. Use the version that passes a CoroutineScope."
1298+
)
1299+
}
1300+
1301+
@OptIn(WorkflowExperimentalApi::class)
1302+
@Test
1303+
fun `testRender with CoroutineScope works for SessionWorkflow`() = runTest {
1304+
class TestAction : WorkflowAction<Unit, String, String>() {
1305+
override fun Updater.apply() {
1306+
state = "new state"
1307+
setOutput("output")
1308+
}
1309+
}
1310+
1311+
val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
1312+
initialState = { _, _: CoroutineScope -> "initial" },
1313+
render = { _, _ ->
1314+
actionSink.contraMap { it }
1315+
}
1316+
)
1317+
1318+
val testResult = workflow.testRender(Unit, this)
1319+
.render { sink ->
1320+
sink.send(TestAction())
1321+
}
1322+
1323+
testResult.verifyActionResult { state, output ->
1324+
assertEquals("new state", state)
1325+
assertEquals("output", output?.value)
1326+
}
1327+
}
1328+
1329+
@OptIn(WorkflowExperimentalApi::class)
1330+
@Test
1331+
fun `testRender with CoroutineScope uses the correct scope`() = runTest {
1332+
val signalMutex = Mutex(locked = true)
1333+
class TestAction : WorkflowAction<Unit, String, String>() {
1334+
override fun Updater.apply() {
1335+
state = "new state"
1336+
setOutput("output")
1337+
}
1338+
}
1339+
1340+
val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
1341+
initialState = { _, workflowScope: CoroutineScope ->
1342+
assertEquals(workflowScope, this@runTest)
1343+
signalMutex.unlock()
1344+
"initial"
1345+
},
1346+
render = { _, _ ->
1347+
actionSink.contraMap { it }
1348+
}
1349+
)
1350+
1351+
workflow.testRender(Unit, this)
1352+
.render { sink ->
1353+
sink.send(TestAction())
1354+
}
1355+
1356+
// Assertion happens in the `initialState` call above.
1357+
signalMutex.lock()
1358+
}
1359+
12661360
@Test fun `createRenderChildInvocation() for Workflow-stateless{}`() {
12671361
val workflow = Workflow.stateless<String, Int, Unit> {}
12681362
val invocation = createRenderChildInvocation(workflow, "props", "key")

0 commit comments

Comments
 (0)