forked from slackhq/circuit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This PR introduces `TestEventSink` to simplify and streamline efforts to assert events emitted by a UI under test
- Loading branch information
Showing
9 changed files
with
438 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
199 changes: 199 additions & 0 deletions
199
circuit-test/src/commonMain/kotlin/com/slack/circuit/test/TestEventSink.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
// Copyright (C) 2023 Slack Technologies, LLC | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package com.slack.circuit.test | ||
|
||
import com.slack.circuit.runtime.CircuitUiEvent | ||
import kotlin.time.Duration | ||
import kotlin.time.DurationUnit | ||
import kotlin.time.toDuration | ||
|
||
/** | ||
* A test event sink that records events from a Circuit UI and allows making assertions about them. | ||
* | ||
* Note: this class was heavily influenced by RxJava3's | ||
* [BaseTestConsumer][https://github.com/ReactiveX/RxJava/blob/v3.1.6/src/main/java/io/reactivex/rxjava3/observers/BaseTestConsumer.java]. | ||
* | ||
* @see CircuitUiEvent | ||
*/ | ||
public class TestEventSink<UiEvent : CircuitUiEvent> : (UiEvent) -> Unit { | ||
private val receivedEvents = mutableListOf<UiEvent>() | ||
|
||
/** | ||
* Sends the specified [event] to this TestEventSink. | ||
* | ||
* @param event the [UiEvent] being added to the sink | ||
*/ | ||
public override fun invoke(event: UiEvent) { | ||
receivedEvents.add(event) | ||
} | ||
|
||
/** | ||
* Assert that this TestEventSink has received [count] events. | ||
* | ||
* @param count the number of expected events | ||
* @return this | ||
*/ | ||
public fun assertEventCount(count: Int): TestEventSink<UiEvent> { | ||
if (receivedEvents.size != count) { | ||
throw AssertionError( | ||
""" | ||
expected: $count | ||
got: ${receivedEvents.size}; Value counts differ | ||
""" | ||
.trimIndent() | ||
) | ||
} | ||
|
||
return this | ||
} | ||
|
||
/** | ||
* Assert that this TestEventSink has not received any events. | ||
* | ||
* @return this | ||
*/ | ||
public fun assertNoEvents(): TestEventSink<UiEvent> = assertEventCount(0) | ||
|
||
/** | ||
* Assert that this TestEventSink as received exactly one [UiEvent] and that it equals [event]. | ||
* | ||
* @param event the expected [UiEvent] | ||
* @return this | ||
*/ | ||
public fun assertEvent(event: UiEvent): TestEventSink<UiEvent> { | ||
if (receivedEvents.size != 1) { | ||
throw AssertionError( | ||
""" | ||
expected: ${valueAndClass(event)} | ||
got: $receivedEvents | ||
""" | ||
.trimIndent() | ||
) | ||
} | ||
|
||
val received = receivedEvents[0] | ||
if (received != event) { | ||
throw AssertionError( | ||
""" | ||
expected: ${valueAndClass(event)} | ||
got: ${valueAndClass(received)} | ||
""" | ||
.trimIndent() | ||
) | ||
} | ||
|
||
return this | ||
} | ||
|
||
/** | ||
* Assert that this TestEventSink has received exactly one [UiEvent] and that running [predicate] | ||
* with that event returns true. | ||
* | ||
* @param predicate the function used to determine equality with the received event | ||
* @return this | ||
*/ | ||
public fun assertEvent(predicate: (UiEvent) -> Boolean): TestEventSink<UiEvent> { | ||
assertEventAt(0, predicate) | ||
|
||
if (receivedEvents.size > 1) { | ||
throw AssertionError( | ||
"The first value passed the predicate but this sink received more than one event" | ||
) | ||
} | ||
|
||
return this | ||
} | ||
|
||
/** | ||
* Assert that this TestEventSink received an event at [index] which is equal to the given | ||
* [event]. | ||
* | ||
* @param index the index of the received event to be asserted | ||
* @param event the expected [UiEvent] to be compared to the received event at [index] | ||
* @return this | ||
*/ | ||
public fun assertEventAt(index: Int, event: UiEvent): TestEventSink<UiEvent> = | ||
assertEventAt(index) { received -> received == event } | ||
|
||
/** | ||
* Assert this TestEventSink received an event at [index] and that running [predicate] with that | ||
* event returns true. | ||
* | ||
* @param index the index of the received event to be asserted | ||
* @param predicate the function used to determine equality with the received event at [index] | ||
* @return this | ||
*/ | ||
public fun assertEventAt(index: Int, predicate: (UiEvent) -> Boolean): TestEventSink<UiEvent> { | ||
if (receivedEvents.isEmpty()) throw AssertionError("No events") | ||
|
||
if (index < 0 || receivedEvents.size <= index) { | ||
throw AssertionError("Index $index is out of range [0, ${receivedEvents.size})") | ||
} | ||
|
||
val received = receivedEvents[index] | ||
if (!predicate(received)) { | ||
throw AssertionError( | ||
"Value ${valueAndClass(received)} at index $index did not pass the predicate check" | ||
) | ||
} | ||
|
||
return this | ||
} | ||
|
||
/** | ||
* Assert that this TestEventSink received only the specified [events] in the specified order. | ||
* | ||
* @param events the list of expected [events][UiEvent] that is compared to the received events | ||
* @return this | ||
*/ | ||
public fun assertEvents(vararg events: UiEvent): TestEventSink<UiEvent> { | ||
if (receivedEvents.size != events.size) { | ||
throw AssertionError( | ||
""" | ||
expected: ${events.size} ${events.contentToString()} | ||
got: ${receivedEvents.size} $receivedEvents; Value count differs | ||
""" | ||
.trimIndent() | ||
) | ||
} | ||
|
||
receivedEvents.forEachIndexed { i, received -> | ||
val event = events[i] | ||
if (received != event) { | ||
throw AssertionError( | ||
""" | ||
expected: ${valueAndClass(event)} | ||
got: ${valueAndClass(received)}; Values at index $i differ | ||
""" | ||
.trimIndent() | ||
) | ||
} | ||
} | ||
|
||
return this | ||
} | ||
|
||
/** | ||
* Assert that [predicate] returns true for each event received by this TestEventSink. | ||
* | ||
* @param predicate the function used to determine equality with each received event | ||
* @return this | ||
*/ | ||
public fun assertEvents(predicate: (Int, UiEvent) -> Boolean): TestEventSink<UiEvent> { | ||
receivedEvents.forEachIndexed { i, received -> | ||
if (!predicate(i, received)) { | ||
throw AssertionError( | ||
"Value ${valueAndClass(received)} at index $i did not pass the predicate check" | ||
) | ||
} | ||
} | ||
|
||
return this | ||
} | ||
|
||
public companion object { | ||
public val DEFAULT_TIMEOUT: Duration = 10.toDuration(DurationUnit.SECONDS) | ||
|
||
private fun valueAndClass(obj: Any): String = "$obj (${obj.javaClass.simpleName})" | ||
} | ||
} |
Oops, something went wrong.