Skip to content

Commit

Permalink
Updated enro-test to prefer "assert" methods in all cases, added func…
Browse files Browse the repository at this point in the history
…tionality to deliver a result directly to a particular navigation key, and a few other minor documentation and function naming updates
  • Loading branch information
isaac-udy committed Nov 15, 2024
1 parent fadc10b commit 0d1bf86
Show file tree
Hide file tree
Showing 22 changed files with 662 additions and 109 deletions.
1 change: 1 addition & 0 deletions enro-test/src/main/java/dev/enro/test/EnroTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ object EnroTest {

fun uninstallNavigationController() {
EnroViewModelNavigationHandleProvider.clearAllForTest()
TestNavigationHandle.allInstructions.clear()
navigationController?.apply {
setConfig(
config.copy(
Expand Down
83 changes: 81 additions & 2 deletions enro-test/src/main/java/dev/enro/test/EnroTestAssertions.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.enro.test

import dev.enro.core.NavigationKey

class EnroTestAssertionException(message: String) : AssertionError(message)

@PublishedApi
Expand All @@ -13,23 +15,100 @@ data class EnroAssertionContext(
)

@PublishedApi
internal fun Any?.shouldBeEqualTo(expected: Any?, message: EnroAssertionContext.() -> String) {
internal fun <T> T.shouldBeEqualTo(expected: Any?, message: EnroAssertionContext.() -> String): T {
if (this != expected) {
val assertionContext = EnroAssertionContext(
expected = expected,
actual = this
)
throw EnroTestAssertionException(message(assertionContext))
}
return this
}

@PublishedApi
internal fun Any?.shouldNotBeEqualTo(expected: Any?, message: EnroAssertionContext.() -> String) {
internal fun <T> T.shouldNotBeEqualTo(expected: Any?, message: EnroAssertionContext.() -> String): T {
if (this == expected) {
val assertionContext = EnroAssertionContext(
expected = expected,
actual = this
)
throw EnroTestAssertionException(message(assertionContext))
}
return this
}

@PublishedApi
internal fun <T> T.shouldMatchPredicate(predicate: (T) -> Boolean, message: EnroAssertionContext.() -> String): T {
val predicateResult = predicate(this)
if (!predicateResult) {
val assertionContext = EnroAssertionContext(
expected = null,
actual = this
)
throw EnroTestAssertionException(message(assertionContext))
}
return this
}

@PublishedApi
internal fun <T> T.shouldNotMatchPredicate(predicate: (T) -> Boolean, message: EnroAssertionContext.() -> String): T {
val predicateResult = predicate(this)
if (predicateResult) {
val assertionContext = EnroAssertionContext(
expected = null,
actual = this
)
throw EnroTestAssertionException(message(assertionContext))
}
return this
}

@PublishedApi
internal fun <T: Any> T?.shouldMatchPredicateNotNull(predicate: (T) -> Boolean, message: EnroAssertionContext.() -> String): T {
if (this == null) {
throw EnroTestAssertionException("Expected a non-null value, but was null.")
}

val predicateResult = predicate(this)
if (!predicateResult) {
val assertionContext = EnroAssertionContext(
expected = null,
actual = this
)
throw EnroTestAssertionException(message(assertionContext))
}
return this
}

@PublishedApi
internal inline fun <reified T> Any?.shouldBeInstanceOf(): T {
if (this == null) {
throw EnroTestAssertionException("Expected a non-null value, but was null.")
}

val isCorrectType = this is T
if (!isCorrectType) {
val assertionContext = EnroAssertionContext(
expected = T::class,
actual = this::class
)
throw EnroTestAssertionException("Expected type ${T::class.simpleName}, but was ${this::class.simpleName}")
}
return this as T
}

@PublishedApi
internal fun <T : NavigationKey> Any?.shouldBeInstanceOf(
cls: Class<T>,
) : T {
if (this == null) {
throw EnroTestAssertionException("Expected a non-null value, but was null.")
}

val isCorrectType = cls.isAssignableFrom(this::class.java)
if (!isCorrectType) {
throw EnroTestAssertionException("Expected type ${cls.simpleName}, but was ${this::class.simpleName}")
}
return this as T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package dev.enro.test

import dev.enro.core.NavigationInstruction
import dev.enro.core.NavigationKey
import dev.enro.core.result.EnroResult
import dev.enro.core.result.internal.PendingResult

/**
* Given a NavigationInstruction.Open, this function will deliver a result to the instruction. This is useful for testing
* the behavior of a screen/ViewModel that expects a result.
*/
fun <T : Any> NavigationInstruction.Open<*>.deliverResultForTest(type: Class<T>, result: T) {
val navigationController = EnroTest.getCurrentNavigationController()
val resultId = internal.resultId!!

val navigationKey = internal.resultKey ?: navigationKey

val pendingResult = PendingResult.Result(
resultChannelId = resultId,
instruction = this,
navigationKey = navigationKey as NavigationKey.WithResult<T>,
resultType = type.kotlin,
result = result
)
EnroResult
.from(navigationController)
.addPendingResult(pendingResult)
}

/**
* Given a NavigationInstruction.Open, this function will deliver a result to the instruction. This is useful for testing
* the behavior of a screen/ViewModel that expects a result.
*/
inline fun <reified T : Any> NavigationInstruction.Open<*>.deliverResultForTest(result: T) {
deliverResultForTest(T::class.java, result)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package dev.enro.test

import dev.enro.core.NavigationInstruction
import dev.enro.core.NavigationKey
import dev.enro.core.result.EnroResult
import dev.enro.core.result.internal.PendingResult

/**
* Given a NavigationKey.WithResult, this function will attempt to find the related NavigationInstruction that opened that
* NavigationKey, and deliver a result to that instruction. This is useful for testing the behavior of a screen/ViewModel
* that expects a result.
*/
fun <T : Any> NavigationKey.WithResult<T>.deliverResultForTest(
type: Class<T>,
result: T,
) {
val exactInstruction = TestNavigationHandle.allInstructions
.filterIsInstance<NavigationInstruction.Open<*>>()
.firstOrNull {
System.identityHashCode(it.navigationKey) == System.identityHashCode(this)
}
val fuzzyInstructions = TestNavigationHandle.allInstructions
.filterIsInstance<NavigationInstruction.Open<*>>()
.filter {
it.navigationKey == this
}
if (fuzzyInstructions.isEmpty()) {
throw EnroTestAssertionException("No instruction was found for NavigationKey $this")
}
val instruction = when {
exactInstruction != null -> exactInstruction
fuzzyInstructions.size == 1 -> fuzzyInstructions.first()
else -> {
throw EnroTestAssertionException("No instruction was found for NavigationKey $this")
}
}
val navigationController = EnroTest.getCurrentNavigationController()
val resultId = instruction.internal.resultId!!
val navigationKey = instruction.internal.resultKey ?: instruction.navigationKey

@Suppress("UNCHECKED_CAST")
val pendingResult = PendingResult.Result(
resultChannelId = resultId,
instruction = instruction,
navigationKey = navigationKey as NavigationKey.WithResult<T>,
resultType = type.kotlin,
result = result
)
EnroResult
.from(navigationController)
.addPendingResult(pendingResult)
}

/**
* Given a NavigationKey.WithResult, this function will attempt to find the related NavigationInstruction that opened that
* NavigationKey, and deliver a result to that instruction. This is useful for testing the behavior of a screen/ViewModel
* that expects a result.
*/
inline fun <reified T : Any> NavigationKey.WithResult<T>.deliverResultForTest(result: T) {
deliverResultForTest(T::class.java, result)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,58 @@ import dev.enro.core.container.NavigationContainerContext
* NavigationInstruction [instruction]
*/
fun NavigationContainerContext.assertActive(
instruction: NavigationInstruction.Open<*>
instruction: NavigationInstruction.Open<*>,
) {
backstack.active.shouldBeEqualTo(instruction) {
"Active NavigationInstruction does not match expected value.\n\tExpected: $expected\n\tActual: $actual"
}
backstack.active
.shouldBeEqualTo(instruction) {
"Active NavigationInstruction does not match expected value.\n\tExpected: $expected\n\tActual: $actual"
}
}

/**
* Asserts that the active NavigationInstruction in the NavigationContainerContext matches the provided predicate
*
* @return The active NavigationInstruction that matches the predicate
*/
fun NavigationContainerContext.assertActive(
predicate: (NavigationInstruction.Open<*>) -> Boolean,
): NavigationInstruction.Open<*> {
backstack.active
.shouldMatchPredicateNotNull(predicate) {
"Active NavigationInstruction does not match predicate.\n\tWas: $actual"
}
.let { return it }
}

/**
* Asserts that the active NavigationInstruction in the NavigationContainerContext has a NavigationKey that is equal to
* the provided NavigationKey [key]
*/
fun NavigationContainerContext.assertActive(
key: NavigationKey
key: NavigationKey,
) {
backstack.active?.navigationKey.shouldBeEqualTo(key) {
"Active NavigationInstruction's NavigationKey does not match expected value.\n\tExpected: $expected\n\tActual: $actual"
}
}

/**
* Asserts that the active NavigationInstruction in the NavigationContainerContext has a NavigationKey that matches the
* provided type T and the provided predicate
*
* @return The active NavigationInstruction's NavigationKey that matches the predicate
*/
inline fun <reified T : NavigationKey> NavigationContainerContext.assertActive(
noinline predicate: (T) -> Boolean = { true }
) : T {
backstack.active?.navigationKey
.shouldBeInstanceOf<T>()
.shouldMatchPredicateNotNull(predicate) {
"Active NavigationInstruction's NavigationKey does not match predicate.\n\tWas: $actual"
}
.let { return it }
}

/**
* Asserts that the active NavigationInstruction in the NavigationContainerContext is not equal to the
* provided NavigationInstruction [instruction]
Expand All @@ -40,6 +73,17 @@ fun NavigationContainerContext.assertNotActive(
}
}

/**
* Asserts that the active NavigationInstruction in the NavigationContainerContext does not match the provided predicate
*/
fun NavigationContainerContext.assertInstructionNotActive(
predicate: (NavigationInstruction.Open<*>) -> Boolean
) {
backstack.active.shouldNotBeEqualTo(predicate) {
"Active NavigationInstruction should not be active.\n\tActive: $expected"
}
}

/**
* Asserts that the active NavigationInstruction in the NavigationContainerContext has a NavigationKey that is not equal
* to the provided NavigationKey [key]
Expand All @@ -50,4 +94,18 @@ fun NavigationContainerContext.assertNotActive(
backstack.active?.navigationKey.shouldNotBeEqualTo(key) {
"Active NavigationInstruction's NavigationKey should not be active.\n\tActive: $expected"
}
}

/**
* Asserts that the active NavigationInstruction in the NavigationContainerContext has a NavigationKey that does not match the
* provided type T and the provided predicate
*/
inline fun <reified T : NavigationKey> NavigationContainerContext.assertNotActive(
noinline predicate: (T) -> Boolean = { true }
) {
val activeKey = backstack.active?.navigationKey
if (activeKey !is T) return
activeKey.shouldNotMatchPredicate(predicate) {
"Active NavigationInstruction's NavigationKey should not match predicate.\n\tWas: $actual"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,16 @@ fun NavigationContainerContext.assertBackstackEquals(
}
}
}

/**
* Asserts that the NavigationContainerContext's backstack matches the provided predicate
*/
fun NavigationContainerContext.assertBackstackMatches(
predicate: (NavigationBackstack) -> Boolean
) {
val actualBackstack = this.backstack

actualBackstack.shouldMatchPredicateNotNull(predicate) {
"NavigationContainer's backstack did not match predicate\n\tActual backstack: $actualBackstack"
}
}
Loading

0 comments on commit 0d1bf86

Please sign in to comment.