Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import kotlin.reflect.KClass
* This workflow must not be an [ImpostorWorkflow], or this property will throw an
* [IllegalArgumentException].
*/
@OptIn(ExperimentalStdlibApi::class)
@get:TestOnly
public val KClass<out Workflow<*, *, *>>.workflowIdentifier: WorkflowIdentifier
get() {
Expand Down
7 changes: 7 additions & 0 deletions workflow-testing/api/workflow-testing.api
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public abstract interface class com/squareup/workflow1/testing/RenderTestResult
}

public abstract class com/squareup/workflow1/testing/RenderTester {
public static final field Companion Lcom/squareup/workflow1/testing/RenderTester$Companion;
public static final field VERIFY_ALL_LEVELS I
public fun <init> ()V
public abstract fun expectRemember (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester;
public static synthetic fun expectRemember$default (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
Expand All @@ -51,6 +53,9 @@ public final class com/squareup/workflow1/testing/RenderTester$ChildWorkflowMatc
public static final field INSTANCE Lcom/squareup/workflow1/testing/RenderTester$ChildWorkflowMatch$NotMatched;
}

public final class com/squareup/workflow1/testing/RenderTester$Companion {
}

public final class com/squareup/workflow1/testing/RenderTester$RememberInvocation {
public fun <init> (Ljava/lang/String;Lkotlin/reflect/KType;Ljava/util/List;)V
public final fun getInputs ()Ljava/util/List;
Expand All @@ -68,6 +73,8 @@ public final class com/squareup/workflow1/testing/RenderTester$RenderChildInvoca
}

public final class com/squareup/workflow1/testing/RenderTesterKt {
public static final fun expectCovariantWorkflow (Lcom/squareup/workflow1/testing/RenderTester;Lkotlin/reflect/KClass;Lkotlin/reflect/KType;ILkotlin/reflect/KType;ILjava/lang/Object;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester;
public static synthetic fun expectCovariantWorkflow$default (Lcom/squareup/workflow1/testing/RenderTester;Lkotlin/reflect/KClass;Lkotlin/reflect/KType;ILkotlin/reflect/KType;ILjava/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 final fun expectRemember (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;Lkotlin/reflect/KType;[Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/testing/RenderTester;
public static synthetic fun expectRemember$default (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;Lkotlin/reflect/KType;[Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/testing/RenderTester;
public static final fun expectSideEffect (Lcom/squareup/workflow1/testing/RenderTester;Ljava/lang/String;)Lcom/squareup/workflow1/testing/RenderTester;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,14 +460,25 @@ internal fun createRenderChildInvocation(
* Returns true iff this identifier's [WorkflowIdentifier.getRealIdentifierType] is the same type as
* or a subtype of [expected]'s.
*/
internal fun WorkflowIdentifier.realTypeMatchesExpectation(
internal fun WorkflowIdentifier.realTypeMatchesClassExpectation(
expected: WorkflowIdentifier
): Boolean {
val expectedType = expected.realType
val actualType = realType
return actualType.matchesExpectation(expectedType)
}

/**
* Returns true iff this identifier's [WorkflowIdentifier.getRealIdentifierType] has the same
* class (or is a subtype) of the [expectedKClass].
*/
internal fun WorkflowIdentifier.realTypeMatchesClassExpectation(
expectedKClass: KClass<*>
): Boolean {
val actualType = realType
return actualType.matchesClassExpectation(expectedKClass)
}

internal fun WorkflowIdentifierType.matchesExpectation(expected: WorkflowIdentifierType): Boolean {
return when {
this is Snapshottable && expected is Snapshottable -> matchesSnapshottable(expected)
Expand All @@ -476,6 +487,18 @@ internal fun WorkflowIdentifierType.matchesExpectation(expected: WorkflowIdentif
}
}

internal fun WorkflowIdentifierType.matchesClassExpectation(expectedKClass: KClass<*>): Boolean {
return when (this) {
is Snapshottable -> kClass?.let { actualKClass ->
expectedKClass.isSuperclassOf(actualKClass) || actualKClass.isJavaMockOf(expectedKClass)
} == true
is Unsnapshottable -> (kType.classifier as? KClass<*>)?.let { actualKClass ->
expectedKClass.isSuperclassOf(actualKClass) || actualKClass.isJavaMockOf(expectedKClass)
} == true
else -> false
}
}

private fun Snapshottable.matchesSnapshottable(expected: Snapshottable): Boolean =
kClass?.let { actualKClass ->
expected.kClass?.let { expectedKClass ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.squareup.workflow1.WorkflowOutput
import com.squareup.workflow1.config.JvmTestRuntimeConfigTools
import com.squareup.workflow1.identifier
import com.squareup.workflow1.testing.RenderTester.ChildWorkflowMatch
import com.squareup.workflow1.testing.RenderTester.Companion
import com.squareup.workflow1.workflowIdentifier
import kotlinx.coroutines.CoroutineScope
import kotlin.reflect.KClass
Expand Down Expand Up @@ -378,6 +379,10 @@ public abstract class RenderTester<PropsT, StateT, OutputT, RenderingT> {
public val output: WorkflowOutput<*>? = null
) : ChildWorkflowMatch()
}

public companion object {
public const val VERIFY_ALL_LEVELS: Int = -1
}
}

/**
Expand All @@ -389,6 +394,13 @@ public abstract class RenderTester<PropsT, StateT, OutputT, RenderingT> {
* concrete class, your render tests can pass the class of the interface to this method instead of
* the actual class that implements it.
*
* Note that Workflow<Int, String, Int> is *not* a sub-type of Workflow<Int, Object, Int> because
* it is not covariant for the [OutputT] generic (the same is true for [PropsT]). This means that
* you cannot use the [WorkflowIdentifier] or [KClass] of a Workflow class whose [OutputT] or
* [PropsT] are supertypes to the one you want to match. If this is the only reasonable class
* definition you have access to, then consider using [expectCovariantWorkflow] and specifying
* those types explicitly.
*
* ## Expecting impostor workflows
*
* If the workflow-under-test renders an
Expand Down Expand Up @@ -448,6 +460,13 @@ public inline fun <ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
* concrete class, your render tests can pass the class of the interface to this method instead of
* the actual class that implements it.
*
* Note that Workflow<Int, String, Int> is *not* a sub-type of Workflow<Int, Object, Int> because
* it is not covariant for the [OutputT] generic (the same is true for [PropsT]). This means that
* you cannot use the [WorkflowIdentifier] or [KClass] of a Workflow class whose [OutputT] or
* [PropsT] are supertypes to the one you want to match. If this is the only reasonable class
* definition you have access to, then consider using [expectCovariantWorkflow] and specifying
* those types explicitly.
*
* ## Expecting impostor workflows
*
* If the workflow-under-test renders an
Expand Down Expand Up @@ -509,7 +528,7 @@ public fun <ChildOutputT, ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
"output=$output"
}
) { invocation ->
if (invocation.workflow.identifier.realTypeMatchesExpectation(identifier) &&
if (invocation.workflow.identifier.realTypeMatchesClassExpectation(identifier) &&
invocation.renderKey == key
) {
assertProps(invocation.props)
Expand All @@ -528,6 +547,13 @@ public fun <ChildOutputT, ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
* concrete class, your render tests can pass the class of the interface to this method instead of
* the actual class that implements it.
*
* Note that Workflow<Int, String, Int> is *not* a sub-type of Workflow<Int, Object, Int> because
* it is not covariant for the [OutputT] generic (the same is true for [PropsT]). This means that
* you cannot use the [WorkflowIdentifier] or [KClass] of a Workflow class whose [OutputT] or
* [PropsT] are supertypes to the one you want to match. If this is the only reasonable class
* definition you have access to, then consider using [expectCovariantWorkflow] and specifying
* those types explicitly.
*
* ## Expecting impostor workflows
*
* If the workflow-under-test renders an
Expand All @@ -549,7 +575,8 @@ public fun <ChildOutputT, ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
*
* @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.
* injects a fake. See note above about covariance with [PropsT] and [OutputT] and how these cannot
* help with supertypes.
*
* @param rendering The rendering to return from
* [renderChild][com.squareup.workflow1.BaseRenderContext.renderChild] when this workflow is
Expand Down Expand Up @@ -589,6 +616,143 @@ public inline fun <ChildPropsT, ChildOutputT, ChildRenderingT, PropsT, StateT, O
}
)

/**
* @see [expectWorkflow] for more on this expectation.
*
* This is a special use version for when the only reasonable [KClass] you have to verify against
* is the definition of a workflow whose [OutputT] and [RenderingT] are supertypes of the [OutputT]
* and [RenderingT] of the child workflow type you expect to be rendered.
* In other words, the expected workflow is covariant with the class you have to pass to the
* expectation. (There is a slight nuance here, in that if the [OutputT] is a supertype of the
* expected child's [OutputT], then those workflow's are not actually covariant since [OutputT]
* is an invariant generic type. This is not important for the use of this expectation, however.)
*
* The most common need for this is when you are using a generic factory to construct Workflow
* instances that you then wish to expect in your test.
*
* In that case, use this expectation and provide the [KClass] of the Workflow type, along with the
* [KType] of the [OutputT] and [RenderingT]. The [PropsT] can simply be verified for type safety
* inside [assertProps] by casting the [Any?] into the expected [PropsT].
*
* Note that this implementation does not handle [ImpostorWorkflow][com.squareup.workflow1.ImpostorWorkflow]s
* (for proxied identifiers) like the other versions do.
*
* @param childWorkflowClass The [KClass] of the expected workflow or one of its supertypes,
* including covariant supertypes. E.g. if the workflow type is an interface and the
* workflow-under-test injects a fake.
*
* @param childOutputType The [KType] of the [OutputT] of the expected child workflow.
*
* @param outputTypeVerificationLevel The number of 'levels' of generic arguments to verify in
* the [OutputT], e.g., for `Wrapper<*>` and level 1 only `Wrapper` would be checked, whereas for
* level 2, `Wrapper` and `*` (the star projection) would be checked against the
* [RenderChildInvocation].
*
* @param childRenderingType The [KType] of the [RenderingT] of the expected child workflow.
*
* @param renderingTypeVerificationLevel The number of 'levels' of generic arguments to verify in
* the [OutputT], e.g., for `Wrapper<*>` and level 1 only `Wrapper` would be checked, whereas for
* level 2, `Wrapper` and `*` (the star projection) would be checked against the
* [RenderChildInvocation].
*
* @param rendering The rendering to return from
* [renderChild][com.squareup.workflow1.BaseRenderContext.renderChild] when this workflow is
* rendered.
*
* @param key The key passed to [renderChild][com.squareup.workflow1.BaseRenderContext.renderChild]
* when rendering this workflow.
*
* @param assertProps A function that performs assertions on the props passed to
* [renderChild][com.squareup.workflow1.BaseRenderContext.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.
*/
public fun <ChildOutputT, ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
RenderTester<PropsT, StateT, OutputT, RenderingT>.expectCovariantWorkflow(
childWorkflowClass: KClass<*>,
childOutputType: KType,
outputTypeVerificationLevel: Int = RenderTester.VERIFY_ALL_LEVELS,
childRenderingType: KType,
renderingTypeVerificationLevel: Int = Companion.VERIFY_ALL_LEVELS,
rendering: ChildRenderingT,
output: WorkflowOutput<ChildOutputT>? = null,
key: String = "",
description: String = "",
assertProps: (props: Any?) -> Unit = {}
): RenderTester<PropsT, StateT, OutputT, RenderingT> = expectWorkflow(
exactMatch = true,
description = description.ifBlank {
"workflow " +
"workflowClass=$childWorkflowClass, " +
"childOutputType=$childOutputType, " +
"childRenderingType=$childRenderingType, " +
"key=$key, " +
"rendering=$rendering, " +
"output=$output"
}
) { invocation ->
// Recursive function to verify #n levels of types.
fun verifyTypesToLevel(
levels: Int,
type1: KType,
type2: KType
): Boolean {
if (levels < 1) return true
if (levels == 1) {
// We are at the last level of verification, ignore any further generic type arguments.
return type1.classifier?.equals(type2.classifier) == true
} else {
if (type1.arguments.size != type2.arguments.size) return false
var acc = true
type1.arguments.forEachIndexed { index, kTypeProjection1 ->
val kTypeProjection2 = type2.arguments[index]
if (kTypeProjection1.type == null || kTypeProjection2.type == null) return false
acc =
acc && verifyTypesToLevel(levels - 1, kTypeProjection1.type!!, kTypeProjection2.type!!)
}
return acc
}
}

val childClassTypeMatches =
invocation.workflow.identifier.realTypeMatchesClassExpectation(childWorkflowClass)
val keyMatches = invocation.renderKey == key
val outputTypeMatches = invocation.outputType.type?.equals(childOutputType) == true ||
(
(outputTypeVerificationLevel > 0 && invocation.outputType.type != null) &&
verifyTypesToLevel(
outputTypeVerificationLevel,
invocation.outputType.type!!,
childOutputType
)
)
val renderingTypeMatchers = invocation.renderingType.type?.equals(childRenderingType) == true ||
(
(renderingTypeVerificationLevel > 0 && invocation.renderingType.type != null) &&
verifyTypesToLevel(
renderingTypeVerificationLevel,
invocation.renderingType.type!!,
childRenderingType
)
)

if (childClassTypeMatches &&
keyMatches &&
outputTypeMatches &&
renderingTypeMatchers
) {
assertProps(invocation.props)
ChildWorkflowMatch.Matched(rendering, output)
} else {
ChildWorkflowMatch.NotMatched
}
}

/**
* Specifies that this render pass is expected to run a particular side effect.
*
Expand Down
Loading
Loading