From 3fe529e10bf87a2feacd15c0bb8c705505f2e703 Mon Sep 17 00:00:00 2001 From: CherryPerry Date: Fri, 28 Jan 2022 12:55:49 +0000 Subject: [PATCH 1/4] Workflow should throw meaningful error on Node destruction --- .../ribs/rx2/workflows/RxWorkflowNode.kt | 57 ++++---- .../badoo/ribs/rx2/workflows/WorkflowTest.kt | 136 +++++++++++++----- 2 files changed, 127 insertions(+), 66 deletions(-) diff --git a/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt b/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt index 833103b2d..1453cadf3 100644 --- a/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt +++ b/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt @@ -1,6 +1,6 @@ package com.badoo.ribs.rx2.workflows -import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle import com.badoo.ribs.core.Node import com.badoo.ribs.core.modality.BuildParams import com.badoo.ribs.core.plugin.Plugin @@ -21,18 +21,20 @@ open class RxWorkflowNode( plugins = plugins ) { - private val childrenAttachesRelay: PublishRelay>? = PublishRelay.create() - val childrenAttaches: Observable>? = childrenAttachesRelay?.hide() - val detachSignal: BehaviorRelay = BehaviorRelay.create() + private val childrenAttachesRelay: PublishRelay> = PublishRelay.create() + private val detachSignalRelay: BehaviorRelay = BehaviorRelay.create() + + protected val childrenAttaches: Observable> = childrenAttachesRelay.hide() + protected val detachSignal: Observable = detachSignalRelay.hide() override fun onAttachChildNode(child: Node<*>) { super.onAttachChildNode(child) - childrenAttachesRelay?.accept(child) + childrenAttachesRelay.accept(child) } override fun onDestroy(isRecreating: Boolean) { super.onDestroy(isRecreating) - detachSignal.accept(Unit) + detachSignalRelay.accept(Unit) } /** @@ -42,16 +44,12 @@ open class RxWorkflowNode( */ protected inline fun executeWorkflow( crossinline action: () -> Unit - ): Single = Single.fromCallable { - action() - this as T + ): Single = Single.defer { + throwExceptionSingleIfDestroyed() ?: Single.fromCallable { + action() + this as T + } } - .takeUntil(detachSignal.firstOrError()) - - @VisibleForTesting - internal inline fun executeWorkflowInternal( - crossinline action: () -> Unit - ): Single = executeWorkflow(action) /** * Executes an action and transitions to another workflow element @@ -63,11 +61,12 @@ open class RxWorkflowNode( @SuppressWarnings("LongMethod") protected inline fun attachWorkflow( crossinline action: () -> Unit - ): Single = Single.fromCallable { + ): Single = Single.defer { + throwExceptionSingleIfDestroyed()?.also { return@defer it } action() val childNodesOfExpectedType = children.filterIsInstance() if (childNodesOfExpectedType.isEmpty()) { - Single.error( + Single.error( IllegalStateException( "Expected child of type [${T::class.java}] was not found after executing action. " + "Check that your action actually results in the expected child. " + @@ -80,13 +79,6 @@ open class RxWorkflowNode( Single.just(childNodesOfExpectedType.last()) } } - .flatMap { it } - .takeUntil(detachSignal.firstOrError()) - - @VisibleForTesting - internal inline fun attachWorkflowInternal( - crossinline action: () -> Unit - ): Single = attachWorkflow(action) /** * Waits until a certain child is attached and returns it as the expected workflow element, or @@ -95,18 +87,21 @@ open class RxWorkflowNode( * @return the child as the expected workflow element */ protected inline fun waitForChildAttached(): Single = - Single.fromCallable { + Single.defer { + throwExceptionSingleIfDestroyed()?.also { return@defer it } val childNodesOfExpectedType = children.filterIsInstance() if (childNodesOfExpectedType.isEmpty()) { - childrenAttaches?.ofType(T::class.java)?.firstOrError() + childrenAttaches.ofType(T::class.java)?.firstOrError() } else { Single.just(childNodesOfExpectedType.last()) } } - .flatMap { it } - .takeUntil(detachSignal.firstOrError()) - @VisibleForTesting - internal inline fun waitForChildAttachedInternal(): Single = - waitForChildAttached() + protected fun throwExceptionSingleIfDestroyed(): Single? = + if (lifecycle.currentState == Lifecycle.State.DESTROYED) { + Single.error(IllegalStateException("Node $this is already destroyed, further execution is meaningless")) + } else { + null + } + } diff --git a/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt b/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt index 548a7b725..3d33341f4 100644 --- a/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt +++ b/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt @@ -18,7 +18,6 @@ import com.nhaarman.mockitokotlin2.argThat import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock import io.reactivex.Single -import io.reactivex.observers.TestObserver import kotlinx.parcelize.Parcelize import org.junit.Assert import org.junit.jupiter.api.AfterEach @@ -26,7 +25,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class WorkflowTest { - private lateinit var node: RxWorkflowNode + private lateinit var node: TestRxWorkflowNode private lateinit var view: TestView private lateinit var androidView: ViewGroup private lateinit var parentView: RibView @@ -57,7 +56,7 @@ class WorkflowTest { buildParams: BuildParams = testBuildParams(), viewFactory: ViewFactory = this.viewFactory, plugins: List = emptyList() - ): RxWorkflowNode = RxWorkflowNode( + ): TestRxWorkflowNode = TestRxWorkflowNode( buildParams = buildParams, viewFactory = viewFactory, plugins = plugins @@ -89,13 +88,11 @@ class WorkflowTest { @Test fun `executeWorkflow executes action on subscribe`() { - var actionInvoked = false - val action = { actionInvoked = true } + val action = InvokableStub() val workflow: Single> = node.executeWorkflowInternal(action) - val testObserver = TestObserver>() - workflow.subscribe(testObserver) + val testObserver = workflow.test() - Assert.assertEquals(true, actionInvoked) + action.assertInvoked() testObserver.assertValue(node) testObserver.assertComplete() } @@ -104,26 +101,38 @@ class WorkflowTest { fun `executeWorkflow never executes action on lifecycle terminate before subscribe`() { node.onDestroy(isRecreating = false) - var actionInvoked = false - val action = { actionInvoked = true } + val action = InvokableStub() val workflow: Single> = node.executeWorkflowInternal(action) - val testObserver = TestObserver>() - workflow.subscribe(testObserver) + val testObserver = workflow.test() - Assert.assertEquals(false, actionInvoked) - testObserver.assertNever(node) - testObserver.assertNotComplete() + action.assertNotInvoked() + testObserver.assertNoValues() + testObserver.assertError(IllegalStateException::class.java) + } + + @Test + fun `executeWorkflow does not interrupt further actions on lifecycle terminate`() { + val action = InvokableStub() + val workflow: Single = + node + .executeWorkflowInternal>(action) + .flatMap { Single.never() } + val testObserver = workflow.test() + + node.onDestroy(isRecreating = false) + + action.assertInvoked() + testObserver.assertNoValues() + testObserver.assertNotTerminated() } @Test fun `attachWorkflow executes action on subscribe`() { - var actionInvoked = false - val action = { actionInvoked = true } + val action = InvokableStub() val workflow: Single = node.attachWorkflowInternal(action) - val testObserver = TestObserver() - workflow.subscribe(testObserver) + val testObserver = workflow.test() - Assert.assertEquals(true, actionInvoked) + action.assertInvoked() testObserver.assertValue(child3) testObserver.assertComplete() } @@ -132,27 +141,38 @@ class WorkflowTest { fun `attachWorkflow never executes action on lifecycle terminate before subscribe`() { node.onDestroy(isRecreating = false) - var actionInvoked = false - val action = { actionInvoked = true } + val action = InvokableStub() val workflow: Single = node.attachWorkflowInternal(action) - val testObserver = TestObserver() - workflow.subscribe(testObserver) + val testObserver = workflow.test() - Assert.assertEquals(false, actionInvoked) - testObserver.assertNever(child1) - testObserver.assertNever(child2) - testObserver.assertNever(child3) - testObserver.assertNotComplete() + action.assertNotInvoked() + testObserver.assertNoValues() + testObserver.assertError(IllegalStateException::class.java) + } + + @Test + fun `attachWorkflow does not interrupt further actions on lifecycle terminate`() { + val action = InvokableStub() + val workflow: Single = + node + .attachWorkflowInternal>(action) + .flatMap { Single.never() } + val testObserver = workflow.test() + + node.onDestroy(isRecreating = false) + + action.assertInvoked() + testObserver.assertNoValues() + testObserver.assertNotTerminated() } @Test fun `waitForChildAttached emits expected child immediately if it's already attached`() { - val workflow: Single = node.waitForChildAttachedInternal() - val testObserver = TestObserver() val testChildNode = TestNode2(buildParams = testBuildParams(ancestryInfo = childAncestry)) node.attachChildNode(testChildNode) - workflow.subscribe(testObserver) + val workflow: Single = node.waitForChildAttachedInternal() + val testObserver = workflow.test() testObserver.assertValue(testChildNode) testObserver.assertComplete() @@ -163,11 +183,40 @@ class WorkflowTest { node.onDestroy(isRecreating = false) val workflow: Single = node.waitForChildAttachedInternal() - val testObserver = TestObserver() - workflow.subscribe(testObserver) + val testObserver = workflow.test() testObserver.assertNoValues() - testObserver.assertNotComplete() + testObserver.assertError(IllegalStateException::class.java) + } + + @Test + fun `waitForChildAttached does not interrupt further actions on lifecycle terminate`() { + val workflow: Single = + node + .waitForChildAttachedInternal>() + .flatMap { Single.never() } + val testObserver = workflow.test() + + node.onDestroy(isRecreating = false) + + testObserver.assertNoValues() + testObserver.assertNotTerminated() + } + + private class InvokableStub : () -> Unit { + var isInvoked: Boolean = false + + override fun invoke() { + isInvoked = true + } + + fun assertInvoked() { + Assert.assertEquals("Expected to be invoked", true, isInvoked) + } + + fun assertNotInvoked() { + Assert.assertEquals("Expected to be not invoked", false, isInvoked) + } } private class TestNode( @@ -192,6 +241,23 @@ class WorkflowTest { plugins = plugins + listOf(router) ) + private class TestRxWorkflowNode( + buildParams: BuildParams<*>, + viewFactory: ViewFactory?, + plugins: List, + ) : RxWorkflowNode(buildParams, viewFactory, plugins) { + inline fun waitForChildAttachedInternal(): Single = + waitForChildAttached() + + inline fun executeWorkflowInternal( + crossinline action: () -> Unit + ): Single = executeWorkflow(action) + + inline fun attachWorkflowInternal( + crossinline action: () -> Unit + ): Single = attachWorkflow(action) + } + private class TestView : AndroidRibView() { override val androidView: ViewGroup = mock() } From 7cd9b89d6410b87678b4a9d4381a2d6d4cf17541 Mon Sep 17 00:00:00 2001 From: CherryPerry Date: Fri, 28 Jan 2022 14:00:02 +0000 Subject: [PATCH 2/4] Split inline into inline and default version --- .../ribs/rx2/workflows/RxWorkflowNode.kt | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt b/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt index 1453cadf3..783140a6c 100644 --- a/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt +++ b/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt @@ -10,6 +10,7 @@ import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.PublishRelay import io.reactivex.Observable import io.reactivex.Single +import kotlin.reflect.KClass open class RxWorkflowNode( buildParams: BuildParams<*>, @@ -42,8 +43,9 @@ open class RxWorkflowNode( * * @return the current workflow element */ - protected inline fun executeWorkflow( - crossinline action: () -> Unit + @Suppress("UNCHECKED_CAST") + protected fun executeWorkflow( + action: () -> Unit ): Single = Single.defer { throwExceptionSingleIfDestroyed() ?: Single.fromCallable { action() @@ -58,17 +60,21 @@ open class RxWorkflowNode( * * @return the child as the expected workflow element, or error if expected child was not found */ - @SuppressWarnings("LongMethod") - protected inline fun attachWorkflow( - crossinline action: () -> Unit + protected inline fun attachWorkflow( + noinline action: () -> Unit + ): Single = attachWorkflow(T::class, action) + + protected fun attachWorkflow( + clazz: KClass, + action: () -> Unit ): Single = Single.defer { throwExceptionSingleIfDestroyed()?.also { return@defer it } action() - val childNodesOfExpectedType = children.filterIsInstance() + val childNodesOfExpectedType = children.filterIsInstance(clazz.java) if (childNodesOfExpectedType.isEmpty()) { Single.error( IllegalStateException( - "Expected child of type [${T::class.java}] was not found after executing action. " + + "Expected child of type [${clazz.java}] was not found after executing action. " + "Check that your action actually results in the expected child. " + "Child count: ${children.size}. " + "Last child is: [${children.lastOrNull()}]. " + @@ -86,18 +92,23 @@ open class RxWorkflowNode( * * @return the child as the expected workflow element */ - protected inline fun waitForChildAttached(): Single = + protected inline fun waitForChildAttached(): Single = + waitForChildAttached(T::class) + + protected fun waitForChildAttached( + clazz: KClass, + ): Single = Single.defer { throwExceptionSingleIfDestroyed()?.also { return@defer it } - val childNodesOfExpectedType = children.filterIsInstance() + val childNodesOfExpectedType = children.filterIsInstance(clazz.java) if (childNodesOfExpectedType.isEmpty()) { - childrenAttaches.ofType(T::class.java)?.firstOrError() + childrenAttaches.ofType(clazz.java)?.firstOrError() } else { Single.just(childNodesOfExpectedType.last()) } } - protected fun throwExceptionSingleIfDestroyed(): Single? = + private fun throwExceptionSingleIfDestroyed(): Single? = if (lifecycle.currentState == Lifecycle.State.DESTROYED) { Single.error(IllegalStateException("Node $this is already destroyed, further execution is meaningless")) } else { From 15a2fc1ae411feeb94ef4010a6b65849d5b38db3 Mon Sep 17 00:00:00 2001 From: CherryPerry Date: Fri, 28 Jan 2022 14:07:55 +0000 Subject: [PATCH 3/4] Fix test compilation --- .../java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt b/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt index 3d33341f4..4bb04f2b2 100644 --- a/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt +++ b/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt @@ -246,15 +246,15 @@ class WorkflowTest { viewFactory: ViewFactory?, plugins: List, ) : RxWorkflowNode(buildParams, viewFactory, plugins) { - inline fun waitForChildAttachedInternal(): Single = + inline fun waitForChildAttachedInternal(): Single = waitForChildAttached() - inline fun executeWorkflowInternal( - crossinline action: () -> Unit + inline fun executeWorkflowInternal( + noinline action: () -> Unit ): Single = executeWorkflow(action) - inline fun attachWorkflowInternal( - crossinline action: () -> Unit + inline fun attachWorkflowInternal( + noinline action: () -> Unit ): Single = attachWorkflow(action) } From 2c655841b4e107647805e83e5e146fa8036fda87 Mon Sep 17 00:00:00 2001 From: CherryPerry Date: Tue, 15 Mar 2022 16:05:05 +0000 Subject: [PATCH 4/4] Split executeWorkflow into executeWorkflow/maybeExecuteWorkflow --- .../ribs/rx2/workflows/RxWorkflowNode.kt | 72 +++++++++++++++-- .../badoo/ribs/rx2/workflows/WorkflowTest.kt | 77 ++++++++++++++++--- 2 files changed, 130 insertions(+), 19 deletions(-) diff --git a/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt b/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt index 783140a6c..349c3877d 100644 --- a/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt +++ b/libraries/rib-rx2/src/main/java/com/badoo/ribs/rx2/workflows/RxWorkflowNode.kt @@ -8,6 +8,9 @@ import com.badoo.ribs.core.view.RibView import com.badoo.ribs.core.view.ViewFactory import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.PublishRelay +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable +import io.reactivex.Maybe import io.reactivex.Observable import io.reactivex.Single import kotlin.reflect.KClass @@ -26,7 +29,9 @@ open class RxWorkflowNode( private val detachSignalRelay: BehaviorRelay = BehaviorRelay.create() protected val childrenAttaches: Observable> = childrenAttachesRelay.hide() - protected val detachSignal: Observable = detachSignalRelay.hide() + + // Flowable to use in takeUntil() + protected val detachSignal: Flowable = detachSignalRelay.toFlowable(BackpressureStrategy.LATEST) override fun onAttachChildNode(child: Node<*>) { super.onAttachChildNode(child) @@ -39,9 +44,10 @@ open class RxWorkflowNode( } /** - * Executes an action and remains on the same hierarchical level + * Executes an action and remains on the same hierarchical level. * * @return the current workflow element + * @throws NodeIsNotAvailableForWorkflowException when execution is not possible */ @Suppress("UNCHECKED_CAST") protected fun executeWorkflow( @@ -54,16 +60,45 @@ open class RxWorkflowNode( } /** - * Executes an action and transitions to another workflow element + * Executes an action and remains on the same hierarchical level. + * If execution is not possible returns nothing. * - * @param action an action that's supposed to result in the attach of a child (e.g. router.push()) + * @return the current workflow element + */ + protected fun maybeExecuteWorkflow( + action: () -> Unit + ): Maybe = + executeWorkflow(action) + .toMaybe() + .onErrorComplete { it is NodeIsNotAvailableForWorkflowException } + .takeUntil(detachSignal) + + /** + * Executes an action and transitions to another workflow element. * + * @param action an action that's supposed to result in the attach of a child (e.g. router.push()) * @return the child as the expected workflow element, or error if expected child was not found + * @throws NodeIsNotAvailableForWorkflowException when execution is not possible */ protected inline fun attachWorkflow( noinline action: () -> Unit ): Single = attachWorkflow(T::class, action) + /** + * Executes an action and transitions to another workflow element. + * If execution is not possible returns nothing. + * + * @param action an action that's supposed to result in the attach of a child (e.g. router.push()) + * @return the child as the expected workflow element, or error if expected child was not found + */ + protected inline fun maybeAttachWorkflow( + noinline action: () -> Unit + ): Maybe = + attachWorkflow(T::class, action) + .toMaybe() + .onErrorComplete { it is NodeIsNotAvailableForWorkflowException } + .takeUntil(detachSignal) + protected fun attachWorkflow( clazz: KClass, action: () -> Unit @@ -87,14 +122,28 @@ open class RxWorkflowNode( } /** - * Waits until a certain child is attached and returns it as the expected workflow element, or - * returns it immediately if it's already available. + * Waits until a certain child is attached and returns it as the expected workflow element, + * or returns it immediately if it's already available. * * @return the child as the expected workflow element + * @throws NodeIsNotAvailableForWorkflowException when execution is not possible */ protected inline fun waitForChildAttached(): Single = waitForChildAttached(T::class) + /** + * Waits until a certain child is attached and returns it as the expected workflow element, + * or returns it immediately if it's already available. + * If execution is not possible returns nothing. + * + * @return the child as the expected workflow element + */ + protected inline fun maybeWaitForChildAttached(): Maybe = + waitForChildAttached(T::class) + .toMaybe() + .onErrorComplete { it is NodeIsNotAvailableForWorkflowException } + .takeUntil(detachSignal) + protected fun waitForChildAttached( clazz: KClass, ): Single = @@ -108,11 +157,20 @@ open class RxWorkflowNode( } } + private fun skipExecutionIfDestroyed(): Maybe? = + if (lifecycle.currentState > Lifecycle.State.DESTROYED) { + Maybe.empty() + } else { + null + } + private fun throwExceptionSingleIfDestroyed(): Single? = if (lifecycle.currentState == Lifecycle.State.DESTROYED) { - Single.error(IllegalStateException("Node $this is already destroyed, further execution is meaningless")) + Single.error(NodeIsNotAvailableForWorkflowException("Node $this is already destroyed, further execution is meaningless")) } else { null } + class NodeIsNotAvailableForWorkflowException(message: String) : IllegalStateException(message) + } diff --git a/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt b/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt index 4bb04f2b2..3f6230a74 100644 --- a/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt +++ b/libraries/rib-rx2/src/test/java/com/badoo/ribs/rx2/workflows/WorkflowTest.kt @@ -13,10 +13,12 @@ import com.badoo.ribs.core.view.RibView import com.badoo.ribs.core.view.ViewFactory import com.badoo.ribs.routing.Routing import com.badoo.ribs.routing.router.Router +import com.badoo.ribs.rx2.workflows.RxWorkflowNode.NodeIsNotAvailableForWorkflowException import com.badoo.ribs.util.RIBs import com.nhaarman.mockitokotlin2.argThat import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock +import io.reactivex.Maybe import io.reactivex.Single import kotlinx.parcelize.Parcelize import org.junit.Assert @@ -93,8 +95,7 @@ class WorkflowTest { val testObserver = workflow.test() action.assertInvoked() - testObserver.assertValue(node) - testObserver.assertComplete() + testObserver.assertResult(node) } @Test @@ -106,8 +107,7 @@ class WorkflowTest { val testObserver = workflow.test() action.assertNotInvoked() - testObserver.assertNoValues() - testObserver.assertError(IllegalStateException::class.java) + testObserver.assertFailure(NodeIsNotAvailableForWorkflowException::class.java) } @Test @@ -126,6 +126,18 @@ class WorkflowTest { testObserver.assertNotTerminated() } + @Test + fun `maybeExecuteWorkflow never executes action on lifecycle terminate before subscribe`() { + node.onDestroy(isRecreating = false) + + val action = InvokableStub() + val workflow: Maybe> = node.maybeExecuteWorkflowInternal(action) + val testObserver = workflow.test() + + action.assertNotInvoked() + testObserver.assertResult() + } + @Test fun `attachWorkflow executes action on subscribe`() { val action = InvokableStub() @@ -133,8 +145,7 @@ class WorkflowTest { val testObserver = workflow.test() action.assertInvoked() - testObserver.assertValue(child3) - testObserver.assertComplete() + testObserver.assertResult(child3) } @Test @@ -146,8 +157,7 @@ class WorkflowTest { val testObserver = workflow.test() action.assertNotInvoked() - testObserver.assertNoValues() - testObserver.assertError(IllegalStateException::class.java) + testObserver.assertFailure(NodeIsNotAvailableForWorkflowException::class.java) } @Test @@ -166,6 +176,18 @@ class WorkflowTest { testObserver.assertNotTerminated() } + @Test + fun `maybeAttachWorkflow never executes action on lifecycle terminate before subscribe`() { + node.onDestroy(isRecreating = false) + + val action = InvokableStub() + val workflow: Maybe = node.maybeAttachWorkflowInternal(action) + val testObserver = workflow.test() + + action.assertNotInvoked() + testObserver.assertResult() + } + @Test fun `waitForChildAttached emits expected child immediately if it's already attached`() { val testChildNode = TestNode2(buildParams = testBuildParams(ancestryInfo = childAncestry)) @@ -174,8 +196,18 @@ class WorkflowTest { val workflow: Single = node.waitForChildAttachedInternal() val testObserver = workflow.test() - testObserver.assertValue(testChildNode) - testObserver.assertComplete() + testObserver.assertResult(testChildNode) + } + + @Test + fun `waitForChildAttached emits expected child after it is attached`() { + val testChildNode = TestNode2(buildParams = testBuildParams(ancestryInfo = childAncestry)) + val workflow: Single = node.waitForChildAttachedInternal() + val testObserver = workflow.test() + + node.attachChildNode(testChildNode) + + testObserver.assertResult(testChildNode) } @Test @@ -185,8 +217,7 @@ class WorkflowTest { val workflow: Single = node.waitForChildAttachedInternal() val testObserver = workflow.test() - testObserver.assertNoValues() - testObserver.assertError(IllegalStateException::class.java) + testObserver.assertFailure(NodeIsNotAvailableForWorkflowException::class.java) } @Test @@ -203,6 +234,16 @@ class WorkflowTest { testObserver.assertNotTerminated() } + @Test + fun `maybeWaitForChildAttached never executes action on lifecycle terminate before subscribe`() { + node.onDestroy(isRecreating = false) + + val workflow: Maybe = node.maybeWaitForChildAttachedInternal() + val testObserver = workflow.test() + + testObserver.assertResult() + } + private class InvokableStub : () -> Unit { var isInvoked: Boolean = false @@ -249,13 +290,25 @@ class WorkflowTest { inline fun waitForChildAttachedInternal(): Single = waitForChildAttached() + inline fun maybeWaitForChildAttachedInternal(): Maybe = + maybeWaitForChildAttached() + inline fun executeWorkflowInternal( noinline action: () -> Unit ): Single = executeWorkflow(action) + inline fun maybeExecuteWorkflowInternal( + noinline action: () -> Unit + ): Maybe = maybeExecuteWorkflow(action) + inline fun attachWorkflowInternal( noinline action: () -> Unit ): Single = attachWorkflow(action) + + inline fun maybeAttachWorkflowInternal( + noinline action: () -> Unit + ): Maybe = maybeAttachWorkflow(action) + } private class TestView : AndroidRibView() {