@@ -389,6 +389,13 @@ public abstract class RenderTester<PropsT, StateT, OutputT, RenderingT> {
389
389
* concrete class, your render tests can pass the class of the interface to this method instead of
390
390
* the actual class that implements it.
391
391
*
392
+ * Note that Workflow<Int, String, Int> is *not* a sub-type of Workflow<Int, Object, Int> because
393
+ * it is not covariant for the [OutputT] generic (the same is true for [PropsT]). This means that
394
+ * you cannot use the [WorkflowIdentifier] or [KClass] of a Workflow class whose [OutputT] or
395
+ * [PropsT] are supertypes to the one you want to match. If this is the only reasonable class
396
+ * definition you have access to, then consider using [expectCovariantWorkflow] and specifying
397
+ * those types explicitly.
398
+ *
392
399
* ## Expecting impostor workflows
393
400
*
394
401
* If the workflow-under-test renders an
@@ -448,6 +455,13 @@ public inline fun <ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
448
455
* concrete class, your render tests can pass the class of the interface to this method instead of
449
456
* the actual class that implements it.
450
457
*
458
+ * Note that Workflow<Int, String, Int> is *not* a sub-type of Workflow<Int, Object, Int> because
459
+ * it is not covariant for the [OutputT] generic (the same is true for [PropsT]). This means that
460
+ * you cannot use the [WorkflowIdentifier] or [KClass] of a Workflow class whose [OutputT] or
461
+ * [PropsT] are supertypes to the one you want to match. If this is the only reasonable class
462
+ * definition you have access to, then consider using [expectCovariantWorkflow] and specifying
463
+ * those types explicitly.
464
+ *
451
465
* ## Expecting impostor workflows
452
466
*
453
467
* If the workflow-under-test renders an
@@ -509,7 +523,7 @@ public fun <ChildOutputT, ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
509
523
" output=$output "
510
524
}
511
525
) { invocation ->
512
- if (invocation.workflow.identifier.realTypeMatchesExpectation (identifier) &&
526
+ if (invocation.workflow.identifier.realTypeMatchesClassExpectation (identifier) &&
513
527
invocation.renderKey == key
514
528
) {
515
529
assertProps(invocation.props)
@@ -528,6 +542,13 @@ public fun <ChildOutputT, ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
528
542
* concrete class, your render tests can pass the class of the interface to this method instead of
529
543
* the actual class that implements it.
530
544
*
545
+ * Note that Workflow<Int, String, Int> is *not* a sub-type of Workflow<Int, Object, Int> because
546
+ * it is not covariant for the [OutputT] generic (the same is true for [PropsT]). This means that
547
+ * you cannot use the [WorkflowIdentifier] or [KClass] of a Workflow class whose [OutputT] or
548
+ * [PropsT] are supertypes to the one you want to match. If this is the only reasonable class
549
+ * definition you have access to, then consider using [expectCovariantWorkflow] and specifying
550
+ * those types explicitly.
551
+ *
531
552
* ## Expecting impostor workflows
532
553
*
533
554
* If the workflow-under-test renders an
@@ -549,7 +570,8 @@ public fun <ChildOutputT, ChildRenderingT, PropsT, StateT, OutputT, RenderingT>
549
570
*
550
571
* @param workflowType The [KClass] of the expected workflow. May also be any of the supertypes
551
572
* of the expected workflow, e.g. if the workflow type is an interface and the workflow-under-test
552
- * injects a fake.
573
+ * injects a fake. See note above about covariance with [PropsT] and [OutputT] and how these cannot
574
+ * help with supertypes.
553
575
*
554
576
* @param rendering The rendering to return from
555
577
* [renderChild][com.squareup.workflow1.BaseRenderContext.renderChild] when this workflow is
@@ -589,6 +611,120 @@ public inline fun <ChildPropsT, ChildOutputT, ChildRenderingT, PropsT, StateT, O
589
611
}
590
612
)
591
613
614
+ /* *
615
+ * @see [expectWorkflow] for the full documentation of [expectWorkflow].
616
+ *
617
+ * This is a special use version for when the only reasonable [KClass] you have to verify against
618
+ * is the definition of a workflow whose [OutputT] and [PropsT] are supertypes of the child you
619
+ * expect to be rendered. This can happen in the case that you are using a generic factory to
620
+ * construct child instances which may have more specific [PropsT] and [OutputT] types than the
621
+ * range of what the factory can produce.
622
+ *
623
+ * Use this expectation then and provide the [KClass] of the Workflow type, along with the [KType]
624
+ * of the [OutputT] and [RenderingT], the [PropsT] can simply be verified for type safety inside
625
+ * [assertProps].
626
+ *
627
+ * Note that this implementation does not handle [ImpostorWorkflow][com.squareup.workflow1.ImpostorWorkflow]s
628
+ * (for proxied identifiers) like the other versions do.
629
+ *
630
+ * @param childWorkflowClass The [KClass] of the expected workflow. May also be any of the supertypes
631
+ * of the expected workflow, e.g. if the workflow type is an interface and the workflow-under-test
632
+ * injects a fake.
633
+ *
634
+ * @param childOutputType The [KType] of the [OutputT] of the expected child workflow.
635
+ *
636
+ * @param outputTypeRecursiveVerification The number of 'levels' of generic arguments to verify in
637
+ * the [OutputT], e.g., for Wrapper<*> and level 1 only Wrapper would be checked.
638
+ *
639
+ * @param childRenderingType The [KType] of the [RenderingT] of the expected child workflow.
640
+ *
641
+ * @param renderingTypeRecursiveVerification The number of 'levels' of generic arguments to verify
642
+ * in the [RenderingT], e.g., for Wrapper<*> and level 1 only Wrapper would be checked.
643
+ *
644
+ * @param rendering The rendering to return from
645
+ * [renderChild][com.squareup.workflow1.BaseRenderContext.renderChild] when this workflow is
646
+ * rendered.
647
+ *
648
+ * @param key The key passed to [renderChild][com.squareup.workflow1.BaseRenderContext.renderChild]
649
+ * when rendering this workflow.
650
+ *
651
+ * @param assertProps A function that performs assertions on the props passed to
652
+ * [renderChild][com.squareup.workflow1.BaseRenderContext.renderChild].
653
+ *
654
+ * @param output If non-null, [WorkflowOutput.value] will be "emitted" when this workflow is
655
+ * rendered. The [WorkflowAction] used to handle this output can be verified using methods on
656
+ * [RenderTestResult].
657
+ *
658
+ * @param description Optional string that will be used to describe this expectation in error
659
+ * messages.
660
+ */
661
+ public fun <ChildOutputT , ChildRenderingT , PropsT , StateT , OutputT , RenderingT >
662
+ RenderTester <PropsT , StateT , OutputT , RenderingT >.expectCovariantWorkflow (
663
+ childWorkflowClass : KClass <* >,
664
+ childOutputType : KType ,
665
+ outputTypeRecursiveVerification : Int = -1,
666
+ childRenderingType : KType ,
667
+ renderingTypeRecursiveVerification : Int = -1,
668
+ rendering : ChildRenderingT ,
669
+ output : WorkflowOutput <ChildOutputT >? ,
670
+ key : String = "",
671
+ description : String = "",
672
+ assertProps : (props: Any? ) -> Unit = {}
673
+ ): RenderTester <PropsT , StateT , OutputT , RenderingT > = expectWorkflow(
674
+ exactMatch = true ,
675
+ description = description.ifBlank {
676
+ " workflow " +
677
+ " workflowClass=$childWorkflowClass , " +
678
+ " childOutputType=$childOutputType , " +
679
+ " childRenderingType=$childRenderingType , " +
680
+ " key=$key , " +
681
+ " rendering=$rendering , " +
682
+ " output=$output "
683
+ }
684
+ ) { invocation ->
685
+ fun verifyTypesToLevel (levels : Int , type1 : KType , type2 : KType ): Boolean {
686
+ if (levels < 1 ) return true
687
+ if (levels == 1 ) {
688
+ // ignore arguments
689
+ return type1.classifier?.equals(type2.classifier) == true
690
+ } else {
691
+ if (type1.arguments.size != type2.arguments.size) return false
692
+ var acc = true
693
+ type1.arguments.forEachIndexed { index, kTypeProjection1 ->
694
+ val kTypeProjection2 = type2.arguments[index]
695
+ if (kTypeProjection1.type == null || kTypeProjection2.type == null ) return false
696
+ acc = acc && verifyTypesToLevel(levels - 1 , kTypeProjection1.type!! , kTypeProjection2.type!! )
697
+ }
698
+ return acc
699
+ }
700
+ }
701
+
702
+ val childClassTypeMatches =
703
+ invocation.workflow.identifier.realTypeMatchesClassExpectation(childWorkflowClass)
704
+ val keyMatches = invocation.renderKey == key
705
+ val outputTypeMatches = invocation.outputType.type?.equals(childOutputType) == true ||
706
+ (
707
+ (outputTypeRecursiveVerification > 0 && invocation.outputType.type != null ) &&
708
+ verifyTypesToLevel(outputTypeRecursiveVerification, invocation.outputType.type!! , childOutputType)
709
+ )
710
+ val renderingTypeMatchers = invocation.renderingType.type?.equals(childRenderingType) == true ||
711
+ (
712
+ (renderingTypeRecursiveVerification > 0 && invocation.renderingType.type != null ) &&
713
+ verifyTypesToLevel(renderingTypeRecursiveVerification, invocation.renderingType.type!! , childRenderingType)
714
+ )
715
+
716
+ if (childClassTypeMatches &&
717
+ keyMatches &&
718
+ outputTypeMatches &&
719
+ renderingTypeMatchers
720
+ ) {
721
+ assertProps(invocation.props)
722
+ ChildWorkflowMatch .Matched (rendering, output)
723
+ } else {
724
+ ChildWorkflowMatch .NotMatched
725
+ }
726
+ }
727
+
592
728
/* *
593
729
* Specifies that this render pass is expected to run a particular side effect.
594
730
*
0 commit comments