Skip to content

Add testRender with CoroutineScope for SessionWorkflow #1141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions workflow-testing/api/workflow-testing.api
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public final class com/squareup/workflow1/testing/RenderTesterKt {
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;
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;
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;
public static final fun testRender (Lcom/squareup/workflow1/SessionWorkflow;Ljava/lang/Object;Lkotlinx/coroutines/CoroutineScope;)Lcom/squareup/workflow1/testing/RenderTester;
public static final fun testRender (Lcom/squareup/workflow1/StatefulWorkflow;Ljava/lang/Object;Ljava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
public static final fun testRender (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.squareup.workflow1.testing

import com.squareup.workflow1.ActionApplied
import com.squareup.workflow1.SessionWorkflow
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.WorkflowAction
import com.squareup.workflow1.WorkflowExperimentalApi
import com.squareup.workflow1.WorkflowIdentifier
import com.squareup.workflow1.WorkflowOutput
import com.squareup.workflow1.identifier
import com.squareup.workflow1.testing.RenderTester.ChildWorkflowMatch
import com.squareup.workflow1.workflowIdentifier
import kotlinx.coroutines.CoroutineScope
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection
Expand All @@ -19,14 +22,40 @@ import kotlin.reflect.KTypeProjection
*
* See [RenderTester] for usage documentation.
*/
@OptIn(WorkflowExperimentalApi::class) // Opt-in is only for the argument check.
@Suppress("UNCHECKED_CAST")
public fun <PropsT, OutputT, RenderingT> Workflow<PropsT, OutputT, RenderingT>.testRender(
props: PropsT
): RenderTester<PropsT, *, OutputT, RenderingT> {
val statefulWorkflow = asStatefulWorkflow() as StatefulWorkflow<PropsT, Any?, OutputT, RenderingT>
return statefulWorkflow.testRender(
props = props,
initialState = statefulWorkflow.initialState(props, null)
initialState = run {
require(this !is SessionWorkflow<PropsT, *, OutputT, RenderingT>) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

"Called testRender on a SessionWorkflow without a CoroutineScope. Use the version that passes a CoroutineScope."
}
statefulWorkflow.initialState(props, null)
}
) as RenderTester<PropsT, Nothing, OutputT, RenderingT>
}

/**
* Create a [RenderTester] to unit test an individual render pass of this [SessionWorkflow],
* using the workflow's [initial state][StatefulWorkflow.initialState], in the [workflowScope].
*
* See [RenderTester] for usage documentation.
*/
@OptIn(WorkflowExperimentalApi::class)
@Suppress("UNCHECKED_CAST")
public fun <PropsT, OutputT, RenderingT> SessionWorkflow<PropsT, *, OutputT, RenderingT>.testRender(
props: PropsT,
workflowScope: CoroutineScope
): RenderTester<PropsT, *, OutputT, RenderingT> {
val sessionWorkflow: SessionWorkflow<PropsT, Any?, OutputT, RenderingT> =
asStatefulWorkflow() as SessionWorkflow<PropsT, Any?, OutputT, RenderingT>
return sessionWorkflow.testRender(
props = props,
initialState = sessionWorkflow.initialState(props, null, workflowScope)
) as RenderTester<PropsT, Nothing, OutputT, RenderingT>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.squareup.workflow1.Worker
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.WorkflowAction
import com.squareup.workflow1.WorkflowAction.Companion.noAction
import com.squareup.workflow1.WorkflowExperimentalApi
import com.squareup.workflow1.WorkflowIdentifier
import com.squareup.workflow1.WorkflowOutput
import com.squareup.workflow1.action
Expand All @@ -19,14 +20,18 @@ import com.squareup.workflow1.identifier
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.rendering
import com.squareup.workflow1.runningWorker
import com.squareup.workflow1.sessionWorkflow
import com.squareup.workflow1.stateful
import com.squareup.workflow1.stateless
import com.squareup.workflow1.testing.RenderTester.ChildWorkflowMatch.Matched
import com.squareup.workflow1.unsnapshottableIdentifier
import com.squareup.workflow1.workflowIdentifier
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.test.runTest
import org.mockito.kotlin.mock
import kotlin.reflect.typeOf
import kotlin.test.Test
Expand Down Expand Up @@ -1263,6 +1268,95 @@ internal class RealRenderTesterTest {
assertEquals(2, renderCount)
}

@OptIn(WorkflowExperimentalApi::class)
@Test
fun `testRender with SessionWorkflow throws exception`() {
class TestAction : WorkflowAction<Unit, String, String>() {
override fun Updater.apply() {
state = "new state"
setOutput("output")
}
}

val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
initialState = { _, _: CoroutineScope -> "initial" },
render = { _, _ ->
actionSink.contraMap { it }
}
)

val exception = assertFailsWith<IllegalArgumentException> {
workflow.testRender(Unit)
.render { sink ->
sink.send(TestAction())
}
}

assertEquals(
exception.message,
"Called testRender on a SessionWorkflow without a CoroutineScope. Use the version that passes a CoroutineScope."
)
}

@OptIn(WorkflowExperimentalApi::class)
@Test
fun `testRender with CoroutineScope works for SessionWorkflow`() = runTest {
class TestAction : WorkflowAction<Unit, String, String>() {
override fun Updater.apply() {
state = "new state"
setOutput("output")
}
}

val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
initialState = { _, _: CoroutineScope -> "initial" },
render = { _, _ ->
actionSink.contraMap { it }
}
)

val testResult = workflow.testRender(Unit, this)
.render { sink ->
sink.send(TestAction())
}

testResult.verifyActionResult { state, output ->
assertEquals("new state", state)
assertEquals("output", output?.value)
}
}

@OptIn(WorkflowExperimentalApi::class)
@Test
fun `testRender with CoroutineScope uses the correct scope`() = runTest {
val signalMutex = Mutex(locked = true)
class TestAction : WorkflowAction<Unit, String, String>() {
override fun Updater.apply() {
state = "new state"
setOutput("output")
}
}

val workflow = Workflow.sessionWorkflow<Unit, String, String, Sink<TestAction>>(
initialState = { _, workflowScope: CoroutineScope ->
assertEquals(workflowScope, this@runTest)
signalMutex.unlock()
"initial"
},
render = { _, _ ->
actionSink.contraMap { it }
}
)

workflow.testRender(Unit, this)
.render { sink ->
sink.send(TestAction())
}

// Assertion happens in the `initialState` call above.
signalMutex.lock()
}

@Test fun `createRenderChildInvocation() for Workflow-stateless{}`() {
val workflow = Workflow.stateless<String, Int, Unit> {}
val invocation = createRenderChildInvocation(workflow, "props", "key")
Expand Down