diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index 79ee28de6c..5b81c5648b 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -1,14 +1,10 @@ public abstract interface class com/squareup/workflow1/BaseRenderContext { public abstract fun getActionSink ()Lcom/squareup/workflow1/Sink; - public abstract fun makeActionSink ()Lcom/squareup/workflow1/Sink; - public abstract fun onEvent (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; public abstract fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public abstract fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V } public final class com/squareup/workflow1/BaseRenderContext$DefaultImpls { - public static fun makeActionSink (Lcom/squareup/workflow1/BaseRenderContext;)Lcom/squareup/workflow1/Sink; - public static fun onEvent (Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; public static synthetic fun renderChild$default (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; } @@ -99,8 +95,6 @@ public abstract class com/squareup/workflow1/StatefulWorkflow : com/squareup/wor public final class com/squareup/workflow1/StatefulWorkflow$RenderContext : com/squareup/workflow1/BaseRenderContext { public fun getActionSink ()Lcom/squareup/workflow1/Sink; - public fun makeActionSink ()Lcom/squareup/workflow1/Sink; - public fun onEvent (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; public fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V } @@ -113,8 +107,6 @@ public abstract class com/squareup/workflow1/StatelessWorkflow : com/squareup/wo public final class com/squareup/workflow1/StatelessWorkflow$RenderContext : com/squareup/workflow1/BaseRenderContext { public fun getActionSink ()Lcom/squareup/workflow1/Sink; - public fun makeActionSink ()Lcom/squareup/workflow1/Sink; - public fun onEvent (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; public fun renderChild (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V } @@ -231,6 +223,7 @@ public final class com/squareup/workflow1/Workflows { public static final fun invoke (Lcom/squareup/workflow1/EventHandler;)V public static final fun makeEventSink (Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow1/Sink; public static final fun mapRendering (Lcom/squareup/workflow1/Workflow;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/Workflow; + public static final fun onEvent (Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; public static final fun renderChild (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; public static final fun renderChild (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/String;)Ljava/lang/Object; public static final fun renderChild (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; diff --git a/workflow-core/src/main/java/com/squareup/workflow1/RenderContext.kt b/workflow-core/src/main/java/com/squareup/workflow1/RenderContext.kt index 770a5da7a7..cd00d90d29 100644 --- a/workflow-core/src/main/java/com/squareup/workflow1/RenderContext.kt +++ b/workflow-core/src/main/java/com/squareup/workflow1/RenderContext.kt @@ -19,6 +19,7 @@ package com.squareup.workflow1 +import com.squareup.workflow1.StatefulWorkflow.RenderContext import com.squareup.workflow1.WorkflowAction.Companion.noAction import com.squareup.workflow1.WorkflowAction.Updater import kotlin.reflect.KType @@ -63,26 +64,6 @@ interface BaseRenderContext { */ val actionSink: Sink> - @Deprecated("Use RenderContext.actionSink.") - @Suppress("DEPRECATION") - fun onEvent( - handler: (EventT) -> WorkflowAction - ): (EventT) -> Unit = EventHandler { event -> - // Run the handler synchronously, so we only have to emit the resulting action and don't - // need the update channel to be generic on each event type. - val action = handler(event) - actionSink.send(action) - } - - /** - * Creates a sink that will accept a single [WorkflowAction] of the given type. - * Invokes that action by calling [WorkflowAction.apply] to update the current - * state, and optionally emits the returned output value if it is non-null. - */ - @Suppress("UNCHECKED_CAST", "DeprecatedCallableAddReplaceWith") - @Deprecated("Use RenderContext.actionSink.") - fun > makeActionSink(): Sink = actionSink - /** * Ensures [child] is running as a child of this workflow, and returns the result of its * `render` method. @@ -137,6 +118,17 @@ interface BaseRenderContext { ) } +@Deprecated("Use RenderContext.actionSink.") +@Suppress("DEPRECATION") +fun BaseRenderContext.onEvent( + handler: (EventT) -> WorkflowAction +): (EventT) -> Unit = EventHandler { event -> + // Run the handler synchronously, so we only have to emit the resulting action and don't + // need the update channel to be generic on each event type. + val action = handler(event) + actionSink.send(action) +} + /** * Convenience alias of [RenderContext.renderChild] for workflows that don't take props. */ diff --git a/workflow-runtime/src/test/java/com/squareup/workflow1/internal/RealRenderContextTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow1/internal/RealRenderContextTest.kt index 061b39ea2d..c59e65f7fa 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow1/internal/RealRenderContextTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow1/internal/RealRenderContextTest.kt @@ -32,6 +32,7 @@ import com.squareup.workflow1.internal.RealRenderContext.Renderer import com.squareup.workflow1.internal.RealRenderContext.SideEffectRunner import com.squareup.workflow1.internal.RealRenderContextTest.TestRenderer.Rendering import com.squareup.workflow1.makeEventSink +import com.squareup.workflow1.onEvent import com.squareup.workflow1.renderChild import com.squareup.workflow1.stateless import kotlinx.coroutines.channels.Channel diff --git a/workflow-testing/api/workflow-testing.api b/workflow-testing/api/workflow-testing.api index 75d7e97f15..1d83b2fe20 100644 --- a/workflow-testing/api/workflow-testing.api +++ b/workflow-testing/api/workflow-testing.api @@ -14,9 +14,7 @@ public abstract interface class com/squareup/workflow1/testing/RenderTestResult public abstract interface class com/squareup/workflow1/testing/RenderTester { public abstract fun expectSideEffect (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester; - public abstract fun expectWorkflow (Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;)Lcom/squareup/workflow1/testing/RenderTester; public abstract fun expectWorkflow (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester; - public abstract fun expectWorkflow (Lkotlin/reflect/KClass;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;)Lcom/squareup/workflow1/testing/RenderTester; public abstract fun render (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTestResult; } @@ -36,11 +34,7 @@ public final class com/squareup/workflow1/testing/RenderTester$ChildWorkflowMatc public final class com/squareup/workflow1/testing/RenderTester$DefaultImpls { public static synthetic fun expectSideEffect$default (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester; - public static fun expectWorkflow (Lcom/squareup/workflow1/testing/RenderTester;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;)Lcom/squareup/workflow1/testing/RenderTester; - public static fun expectWorkflow (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;)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;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester; public static synthetic fun expectWorkflow$default (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;ZLkotlin/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 synthetic fun render$default (Lcom/squareup/workflow1/testing/RenderTester;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTestResult; } @@ -55,6 +49,12 @@ public final class com/squareup/workflow1/testing/RenderTester$RenderChildInvoca public final class com/squareup/workflow1/testing/RenderTesterKt { public static final fun expectSideEffect (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;)Lcom/squareup/workflow1/testing/RenderTester; + public static final fun expectWorkflow (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;)Lcom/squareup/workflow1/testing/RenderTester; + public static final fun expectWorkflow (Lcom/squareup/workflow1/testing/RenderTester;Lcom/squareup/workflow1/WorkflowIdentifier;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester; + public static final fun expectWorkflow (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;)Lcom/squareup/workflow1/testing/RenderTester; + 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 renderTester (Lcom/squareup/workflow1/StatefulWorkflow;Ljava/lang/Object;Ljava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester; public static final fun renderTester (Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;)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; diff --git a/workflow-testing/src/main/java/com/squareup/workflow1/testing/RenderTester.kt b/workflow-testing/src/main/java/com/squareup/workflow1/testing/RenderTester.kt index c90c0c59b1..9a0483e08c 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow1/testing/RenderTester.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow1/testing/RenderTester.kt @@ -22,6 +22,7 @@ import com.squareup.workflow1.WorkflowAction 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 kotlin.reflect.KClass import kotlin.reflect.KType @@ -222,143 +223,6 @@ fun */ interface RenderTester { - /** - * Specifies that this render pass is expected to render a particular child workflow. - * - * Workflow identifiers are compared taking the type hierarchy into account. When a workflow is - * rendered, it will match any expectation that specifies the type of that workflow, or any of - * its supertypes. This means that if you have a workflow that is split into an interface and a - * concrete class, your render tests can pass the class of the interface to this method instead of - * the actual class that implements it. - * - * ## Expecting impostor workflows - * - * If the workflow-under-test renders an - * [ImpostorWorkflow][com.squareup.workflow1.ImpostorWorkflow], the match will not be performed - * using the impostor type, but rather the - * [real identifier][WorkflowIdentifier.getRealIdentifierType] of the impostor's - * [WorkflowIdentifier]. This will be the last identifier in the chain of impostor workflows' - * [realIdentifier][com.squareup.workflow1.ImpostorWorkflow.realIdentifier]s. - * - * A workflow that is wrapped multiple times by various operators will be matched on the upstream - * workflow, so for example the following expectation would succeed: - * - * ``` - * val workflow = Workflow.stateless<…> { - * renderChild( - * childWorkflow.mapRendering { … } - * .mapOutput { … } - * ) - * } - * - * workflow.testRender(…) - * .expectWorkflow(childWorkflow::class, …) - * ``` - * - * @param identifier The [WorkflowIdentifier] of the expected workflow. May identify any supertype - * of the actual rendered workflow, e.g. if the workflow type is an interface and the - * workflow-under-test injects a fake. - * @param rendering The rendering to return from - * [renderChild][com.squareup.workflow1.RenderContext.renderChild] when this workflow is rendered. - * @param key The key passed to [renderChild][com.squareup.workflow1.RenderContext.renderChild] - * when rendering this workflow. - * @param assertProps A function that performs assertions on the props passed to - * [renderChild][com.squareup.workflow1.RenderContext.renderChild]. - * @param output If non-null, [WorkflowOutput.value] will be "emitted" when this workflow is - * rendered. The [WorkflowAction] used to handle this output can be verified using methods on - * [RenderTestResult]. - * @param description Optional string that will be used to describe this expectation in error - * messages. - */ - @ExperimentalWorkflowApi - fun expectWorkflow( - identifier: WorkflowIdentifier, - rendering: ChildRenderingT, - key: String = "", - assertProps: (props: Any?) -> Unit = {}, - output: WorkflowOutput? = null, - description: String = "" - ): RenderTester = expectWorkflow( - exactMatch = true, - description = description.ifBlank { - "workflow " + - "identifier=$identifier, " + - "key=$key, " + - "rendering=$rendering, " + - "output=$output" - } - ) { - if (it.workflow.identifier.realTypeMatchesExpectation(identifier) && - it.renderKey == key - ) { - assertProps(it.props) - ChildWorkflowMatch.Matched(rendering, output) - } else { - ChildWorkflowMatch.NotMatched - } - } - - /** - * Specifies that this render pass is expected to render a particular child workflow. - * - * Workflow identifiers are compared taking the type hierarchy into account. When a workflow is - * rendered, it will match any expectation that specifies the type of that workflow, or any of - * its supertypes. This means that if you have a workflow that is split into an interface and a - * concrete class, your render tests can pass the class of the interface to this method instead of - * the actual class that implements it. - * - * ## Expecting impostor workflows - * - * If the workflow-under-test renders an - * [ImpostorWorkflow][com.squareup.workflow1.ImpostorWorkflow], the match will not be performed - * using the impostor type, but rather the - * [real identifier][WorkflowIdentifier.getRealIdentifierType] of the impostor's - * [WorkflowIdentifier]. This will be the last identifier in the chain of impostor workflows' - * [realIdentifier][com.squareup.workflow1.ImpostorWorkflow.realIdentifier]s. - * - * A workflow that is wrapped multiple times by various operators will be matched on the upstream - * workflow, so for example the following expectation would succeed: - * - * ``` - * val workflow = Workflow.stateless<…> { - * renderChild(childWorkflow.mapRendering { … }) - * } - * - * workflow.testRender(…) - * .expectWorkflow(childWorkflow::class, …) - * ``` - * - * @param workflowType The [KClass] of the expected workflow. May also be any of the supertypes - * of the expected workflow, e.g. if the workflow type is an interface and the workflow-under-test - * injects a fake. - * @param rendering The rendering to return from - * [renderChild][com.squareup.workflow1.RenderContext.renderChild] when this workflow is rendered. - * @param key The key passed to [renderChild][com.squareup.workflow1.RenderContext.renderChild] - * when rendering this workflow. - * @param assertProps A function that performs assertions on the props passed to - * [renderChild][com.squareup.workflow1.RenderContext.renderChild]. - * @param output If non-null, [WorkflowOutput.value] will be "emitted" when this workflow is - * rendered. The [WorkflowAction] used to handle this output can be verified using methods on - * [RenderTestResult]. - * @param description Optional string that will be used to describe this expectation in error - * messages. - */ - @OptIn(ExperimentalWorkflowApi::class) - fun expectWorkflow( - workflowType: KClass>, - rendering: ChildRenderingT, - key: String = "", - assertProps: (props: ChildPropsT) -> Unit = {}, - output: WorkflowOutput? = null, - description: String = "" - ): RenderTester = - expectWorkflow( - workflowType.workflowIdentifier, rendering, key, output = output, description = description, - assertProps = { - @Suppress("UNCHECKED_CAST") - assertProps(it as ChildPropsT) - }) - /** * Specifies that this render pass is expected to render a particular child workflow. * @@ -465,6 +329,211 @@ interface RenderTester { } } +/** + * Specifies that this render pass is expected to render a particular child workflow. + * + * Workflow identifiers are compared taking the type hierarchy into account. When a workflow is + * rendered, it will match any expectation that specifies the type of that workflow, or any of + * its supertypes. This means that if you have a workflow that is split into an interface and a + * concrete class, your render tests can pass the class of the interface to this method instead of + * the actual class that implements it. + * + * ## Expecting impostor workflows + * + * If the workflow-under-test renders an + * [ImpostorWorkflow][com.squareup.workflow1.ImpostorWorkflow], the match will not be performed + * using the impostor type, but rather the + * [real identifier][WorkflowIdentifier.getRealIdentifierType] of the impostor's + * [WorkflowIdentifier]. This will be the last identifier in the chain of impostor workflows' + * [realIdentifier][com.squareup.workflow1.ImpostorWorkflow.realIdentifier]s. + * + * A workflow that is wrapped multiple times by various operators will be matched on the upstream + * workflow, so for example the following expectation would succeed: + * + * ``` + * val workflow = Workflow.stateless<…> { + * renderChild( + * childWorkflow.mapRendering { … } + * .mapOutput { … } + * ) + * } + * + * workflow.testRender(…) + * .expectWorkflow(childWorkflow::class, …) + * ``` + * + * @param identifier The [WorkflowIdentifier] of the expected workflow. May identify any supertype + * of the actual rendered workflow, e.g. if the workflow type is an interface and the + * workflow-under-test injects a fake. + * @param rendering The rendering to return from + * [renderChild][com.squareup.workflow1.RenderContext.renderChild] when this workflow is rendered. + * @param key The key passed to [renderChild][com.squareup.workflow1.RenderContext.renderChild] + * when rendering this workflow. + * @param assertProps A function that performs assertions on the props passed to + * [renderChild][com.squareup.workflow1.RenderContext.renderChild]. + * @param output If non-null, [WorkflowOutput.value] will be "emitted" when this workflow is + * rendered. The [WorkflowAction] used to handle this output can be verified using methods on + * [RenderTestResult]. + * @param description Optional string that will be used to describe this expectation in error + * messages. + */ +@Suppress("NOTHING_TO_INLINE") +@ExperimentalWorkflowApi +/* ktlint-disable parameter-list-wrapping */ +inline fun + RenderTester.expectWorkflow( + identifier: WorkflowIdentifier, + rendering: ChildRenderingT, + key: String = "", + description: String = "", + noinline assertProps: (props: Any?) -> Unit = {} +): RenderTester = + expectWorkflow(identifier, rendering, null as WorkflowOutput<*>?, key, description, assertProps) + +/** + * Specifies that this render pass is expected to render a particular child workflow. + * + * Workflow identifiers are compared taking the type hierarchy into account. When a workflow is + * rendered, it will match any expectation that specifies the type of that workflow, or any of + * its supertypes. This means that if you have a workflow that is split into an interface and a + * concrete class, your render tests can pass the class of the interface to this method instead of + * the actual class that implements it. + * + * ## Expecting impostor workflows + * + * If the workflow-under-test renders an + * [ImpostorWorkflow][com.squareup.workflow1.ImpostorWorkflow], the match will not be performed + * using the impostor type, but rather the + * [real identifier][WorkflowIdentifier.getRealIdentifierType] of the impostor's + * [WorkflowIdentifier]. This will be the last identifier in the chain of impostor workflows' + * [realIdentifier][com.squareup.workflow1.ImpostorWorkflow.realIdentifier]s. + * + * A workflow that is wrapped multiple times by various operators will be matched on the upstream + * workflow, so for example the following expectation would succeed: + * + * ``` + * val workflow = Workflow.stateless<…> { + * renderChild( + * childWorkflow.mapRendering { … } + * .mapOutput { … } + * ) + * } + * + * workflow.testRender(…) + * .expectWorkflow(childWorkflow::class, …) + * ``` + * + * @param identifier The [WorkflowIdentifier] of the expected workflow. May identify any supertype + * of the actual rendered workflow, e.g. if the workflow type is an interface and the + * workflow-under-test injects a fake. + * @param rendering The rendering to return from + * [renderChild][com.squareup.workflow1.RenderContext.renderChild] when this workflow is rendered. + * @param key The key passed to [renderChild][com.squareup.workflow1.RenderContext.renderChild] + * when rendering this workflow. + * @param assertProps A function that performs assertions on the props passed to + * [renderChild][com.squareup.workflow1.RenderContext.renderChild]. + * @param output If non-null, [WorkflowOutput.value] will be "emitted" when this workflow is + * rendered. The [WorkflowAction] used to handle this output can be verified using methods on + * [RenderTestResult]. + * @param description Optional string that will be used to describe this expectation in error + * messages. + */ +@ExperimentalWorkflowApi +/* ktlint-disable parameter-list-wrapping */ +fun + RenderTester.expectWorkflow( + identifier: WorkflowIdentifier, + rendering: ChildRenderingT, + output: WorkflowOutput?, + key: String = "", + description: String = "", + assertProps: (props: Any?) -> Unit = {} +): RenderTester = expectWorkflow( +/* ktlint-enable parameter-list-wrapping */ + exactMatch = true, + description = description.ifBlank { + "workflow " + + "identifier=$identifier, " + + "key=$key, " + + "rendering=$rendering, " + + "output=$output" + } +) { + if (it.workflow.identifier.realTypeMatchesExpectation(identifier) && + it.renderKey == key + ) { + assertProps(it.props) + ChildWorkflowMatch.Matched(rendering, output) + } else { + ChildWorkflowMatch.NotMatched + } +} + +/** + * Specifies that this render pass is expected to render a particular child workflow. + * + * Workflow identifiers are compared taking the type hierarchy into account. When a workflow is + * rendered, it will match any expectation that specifies the type of that workflow, or any of + * its supertypes. This means that if you have a workflow that is split into an interface and a + * concrete class, your render tests can pass the class of the interface to this method instead of + * the actual class that implements it. + * + * ## Expecting impostor workflows + * + * If the workflow-under-test renders an + * [ImpostorWorkflow][com.squareup.workflow1.ImpostorWorkflow], the match will not be performed + * using the impostor type, but rather the + * [real identifier][WorkflowIdentifier.getRealIdentifierType] of the impostor's + * [WorkflowIdentifier]. This will be the last identifier in the chain of impostor workflows' + * [realIdentifier][com.squareup.workflow1.ImpostorWorkflow.realIdentifier]s. + * + * A workflow that is wrapped multiple times by various operators will be matched on the upstream + * workflow, so for example the following expectation would succeed: + * + * ``` + * val workflow = Workflow.stateless<…> { + * renderChild(childWorkflow.mapRendering { … }) + * } + * + * workflow.testRender(…) + * .expectWorkflow(childWorkflow::class, …) + * ``` + * + * @param workflowType The [KClass] of the expected workflow. May also be any of the supertypes + * of the expected workflow, e.g. if the workflow type is an interface and the workflow-under-test + * injects a fake. + * @param rendering The rendering to return from + * [renderChild][com.squareup.workflow1.RenderContext.renderChild] when this workflow is rendered. + * @param key The key passed to [renderChild][com.squareup.workflow1.RenderContext.renderChild] + * when rendering this workflow. + * @param assertProps A function that performs assertions on the props passed to + * [renderChild][com.squareup.workflow1.RenderContext.renderChild]. + * @param output If non-null, [WorkflowOutput.value] will be "emitted" when this workflow is + * rendered. The [WorkflowAction] used to handle this output can be verified using methods on + * [RenderTestResult]. + * @param description Optional string that will be used to describe this expectation in error + * messages. + */ +@OptIn(ExperimentalWorkflowApi::class) +/* ktlint-disable parameter-list-wrapping */ +inline fun + RenderTester.expectWorkflow( + workflowType: KClass>, + rendering: ChildRenderingT, + key: String = "", + crossinline assertProps: (props: ChildPropsT) -> Unit = {}, + output: WorkflowOutput? = null, + description: String = "" +): RenderTester = +/* ktlint-enable parameter-list-wrapping */ + expectWorkflow( + workflowType.workflowIdentifier, rendering, key = key, output = output, + description = description, + assertProps = { + @Suppress("UNCHECKED_CAST") + assertProps(it as ChildPropsT) + }) + /** * Specifies that this render pass is expected to run a particular side effect. * diff --git a/workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt b/workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt index 7621199b5d..bf124e0a14 100644 --- a/workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt +++ b/workflow-testing/src/test/java/com/squareup/workflow1/testing/RealRenderTesterTest.kt @@ -715,7 +715,7 @@ class RealRenderTesterTest { renderChild(TestImpostor(TestWorkflow())) } workflow.testRender(Unit) - .expectWorkflow( + .expectWorkflow( TestImpostor(TestWorkflow()).identifier, Unit ) @@ -748,7 +748,7 @@ class RealRenderTesterTest { val actualId = TestImpostor(TestWorkflowActual()).identifier val tester = workflow.testRender(Unit) - .expectWorkflow(expectedId, Unit) + .expectWorkflow(expectedId, Unit) val error = assertFailsWith { tester.render {} @@ -785,7 +785,7 @@ class RealRenderTesterTest { val expectedId = TestImpostorExpected(TestWorkflow()).identifier workflow.testRender(Unit) - .expectWorkflow(expectedId, Unit) + .expectWorkflow(expectedId, Unit) .render {} }