Skip to content

Commit

Permalink
Fix broken actions #28 (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergej Shafarenka authored Mar 7, 2020
1 parent ec328a2 commit 740ac8c
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 22 deletions.
14 changes: 10 additions & 4 deletions knot/src/main/kotlin/de/halfbit/knot/KnotBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,11 @@ internal constructor(
* ```
* actions {
* perform<Action.Load> {
* flatMapSingle<Payload> { api.load() }
* .map<Change> { Change.Load.Success(it) }
* .onErrorReturn { Change.Load.Failure(it) }
* flatMapSingle {
* loadData()
* .map<Change> { Change.Load.Success(it) }
* .onErrorReturn { Change.Load.Failure(it) }
* }
* }
* }
* ```
Expand Down Expand Up @@ -282,7 +284,7 @@ internal class TypedActionTransformer<Action : Any, Change : Any, A : Action>(
private val transform: ActionTransformer<A, Change>
) : ActionTransformer<Action, Change> {
override fun invoke(action: Observable<Action>): Observable<Change> =
action.ofType(type).flatMap { transform(Observable.just(it)) }
transform(action.ofType(type)).doAfterTerminate { error(ACTIONS_MUST_NEVER_TERMINATE) }
}

internal class WatchingInterceptor<T>(
Expand Down Expand Up @@ -311,3 +313,7 @@ internal class TypedWatcher<Type : Any, T : Type>(
}
}
}

private const val ACTIONS_MUST_NEVER_TERMINATE =
"Action must never terminate. For more information see: " +
"https://github.com/beworker/knot/wiki/Terminal-events-in-Actions-section"
138 changes: 134 additions & 4 deletions knot/src/test/kotlin/de/halfbit/knot/KnotSingleActionTest.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package de.halfbit.knot

import com.google.common.truth.Truth.assertThat
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.TestScheduler
import org.junit.Test

class KnotSingleActionTest {
Expand Down Expand Up @@ -137,16 +140,18 @@ class KnotSingleActionTest {
reduce { change ->
when (change) {
is Change.Load -> copy(value = "loading") + Action.Load
is Change.Load.Success -> copy(value = change.payload).only
is Change.Load.Success -> unexpected(change)
is Change.Load.Failure -> copy(value = "failed").only
}
}
}
actions {
perform<Action.Load> {
flatMapSingle<String> { Single.error(Exception()) }
.map<Change> { Change.Load.Success(it) }
.onErrorReturn { Change.Load.Failure(it) }
flatMapSingle {
Single.error<String>(Exception())
.map<Change> { Change.Load.Success(it) }
.onErrorReturn { Change.Load.Failure(it) }
}
}
}
}
Expand All @@ -167,6 +172,131 @@ class KnotSingleActionTest {
)
}

@Test
fun `actions replace when switchMapSingle is used`() {
val scheduler = TestScheduler()
val knot = knot<State, Change, Action> {
state {
initial = State("empty")
}
changes {
reduce { change ->
when (change) {
is Change.Load -> copy(value = "loading") + Action.Load
is Change.Load.Success -> copy(value = change.payload).only
is Change.Load.Failure -> unexpected(change)
}
}
}
actions {
perform<Action.Load> {
switchMapSingle {
Single.just("data")
.subscribeOn(scheduler)
.map<Change> { Change.Load.Success(it) }
.onErrorReturn { Change.Load.Failure(it) }
}
}
}
}

val observer = knot.state.test()
knot.change.accept(Change.Load)
knot.change.accept(Change.Load)
knot.change.accept(Change.Load)
scheduler.triggerActions()

observer.assertValues(
State("empty"),
State("loading"),
State("loading"),
State("loading"),
State("data")
)
}

@Test
fun `actions stack when flatMapSingle is used`() {
val scheduler = TestScheduler()
val knot = knot<State, Change, Action> {
state {
initial = State("empty")
}
changes {
reduce { change ->
when (change) {
is Change.Load -> copy(value = "loading") + Action.Load
is Change.Load.Success -> copy(value = change.payload).only
is Change.Load.Failure -> unexpected(change)
}
}
}
actions {
perform<Action.Load> {
flatMapSingle {
Single.just("data")
.subscribeOn(scheduler)
.map<Change> { Change.Load.Success(it) }
.onErrorReturn { Change.Load.Failure(it) }
}
}
}
}

val observer = knot.state.test()
knot.change.accept(Change.Load)
knot.change.accept(Change.Load)
knot.change.accept(Change.Load)
scheduler.triggerActions()

observer.assertValues(
State("empty"),
State("loading"),
State("loading"),
State("loading"),
State("data"),
State("data"),
State("data")
)
}

@Test
fun `actions crash when terminated`() {

var exception: Throwable? = null
RxJavaPlugins.setErrorHandler { exception = it }

val knot = knot<State, Change, Action> {
state {
initial = State("empty")
}
changes {
reduce { change ->
when (change) {
is Change.Load -> copy(value = "loading") + Action.Load
is Change.Load.Success -> copy(value = change.payload).only
is Change.Load.Failure -> only
}
}
}
actions {
perform<Action.Load> {
flatMapSingle { Single.error<String>(Exception()) }
.map<Change> { Change.Load.Success(it) }
.onErrorReturn { Change.Load.Failure(it) }
}
}
}

knot.state.test()
knot.change.accept(Change.Load)
RxJavaPlugins.reset()

assertThat(exception)
.hasMessageThat()
.contains("Action must never terminate")
}

@Test
fun `Execute Action with multiple changes`() {
val knot = knot<State, Change, Action> {
Expand Down
10 changes: 3 additions & 7 deletions knot/src/test/kotlin/de/halfbit/knot/KnotTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ package de.halfbit.knot
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import io.reactivex.schedulers.TestScheduler
import io.reactivex.subjects.PublishSubject
import org.junit.Test

Expand Down Expand Up @@ -82,7 +80,7 @@ class KnotTest {
}

@Test
fun `Action transformer is not invoked on initialization`() {
fun `Action transformer is invoked on initialization`() {
val actionTransformer: ActionTransformer<Action, Change> = mock {
on { invoke(any()) }.thenAnswer { Observable.just(Change) }
}
Expand All @@ -98,7 +96,7 @@ class KnotTest {
perform(actionTransformer)
}
}
verify(actionTransformer, never()).invoke(any())
verify(actionTransformer).invoke(any())
}

@Test
Expand Down Expand Up @@ -242,8 +240,7 @@ class KnotTest {
}

@Test
fun `Disposed Knot disposes subscribed actions`() {
val scheduler = TestScheduler()
fun `Disposed Knot disposes actions`() {
val actions = PublishSubject.create<Unit>()
var isDisposed = false
val knot = knot<State, Change, Action> {
Expand All @@ -252,7 +249,6 @@ class KnotTest {
actions {
perform<Action> {
actions
.subscribeOn(scheduler)
.doOnDispose { isDisposed = true }
.map { Change }
}
Expand Down
5 changes: 1 addition & 4 deletions knot/src/test/kotlin/de/halfbit/knot/PrimeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package de.halfbit.knot
import com.google.common.truth.Truth.assertThat
import de.halfbit.knot.utils.SchedulerTester
import io.reactivex.schedulers.Schedulers
import io.reactivex.schedulers.TestScheduler
import io.reactivex.subjects.PublishSubject
import org.junit.Test

Expand Down Expand Up @@ -1136,8 +1135,7 @@ class PrimeTest {
}

@Test
fun `Disposed Knot disposes subscribed actions`() {
val scheduler = TestScheduler()
fun `Disposed Knot disposes actions`() {
val actions = PublishSubject.create<Unit>()
var isDisposed = false
val knot = compositeKnot<State> {
Expand All @@ -1148,7 +1146,6 @@ class PrimeTest {
actions {
perform<Action.A> {
actions
.subscribeOn(scheduler)
.doOnDispose { isDisposed = true }
.map { Change.A }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ class TestCompositeKnotTest {
}

@Test
fun `Disposed Knot disposes subscribed actions`() {
val scheduler = TestScheduler()
fun `Disposed Knot disposes actions`() {
val actions = PublishSubject.create<Unit>()
var isDisposed = false
val knot = testCompositeKnot<State> {
Expand All @@ -158,7 +157,6 @@ class TestCompositeKnotTest {
actions {
perform<Action> {
actions
.subscribeOn(scheduler)
.doOnDispose { isDisposed = true }
.map { Change("change") }
}
Expand Down

0 comments on commit 740ac8c

Please sign in to comment.