Skip to content

Commit

Permalink
Added tests for nested results in sibling containers, updated Default…
Browse files Browse the repository at this point in the history
…ContainerExecutor to correctly support this as would be expected.
  • Loading branch information
isaac-udy committed May 2, 2024
1 parent f43cb07 commit a735521
Show file tree
Hide file tree
Showing 10 changed files with 512 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.enro.core.container

import androidx.activity.ComponentActivity
import dev.enro.compatability.Compatibility
import dev.enro.core.AnyOpenInstruction
import dev.enro.core.ExecutorArgs
import dev.enro.core.NavigationContainerKey
Expand All @@ -10,7 +11,6 @@ import dev.enro.core.NavigationExecutor
import dev.enro.core.NavigationKey
import dev.enro.core.activity
import dev.enro.core.activity.ActivityNavigationContainer
import dev.enro.compatability.Compatibility
import dev.enro.core.getNavigationHandle
import dev.enro.core.navigationContext
import dev.enro.core.parentContainer
Expand Down Expand Up @@ -67,6 +67,7 @@ internal object DefaultContainerExecutor : NavigationExecutor<Any, Any, Navigati
private fun findContainerFor(
fromContext: NavigationContext<*>?,
instruction: AnyOpenInstruction,
alreadyVisitedContainerKeys: Set<String> = emptySet()
): NavigationContainer? {
if (fromContext == null) return null
if (instruction.navigationDirection == NavigationDirection.ReplaceRoot) {
Expand All @@ -77,29 +78,85 @@ internal object DefaultContainerExecutor : NavigationExecutor<Any, Any, Navigati
.containers
.firstOrNull { it.key == NavigationContainerKey.FromId(android.R.id.content) }

val container = containerManager.activeContainer?.takeIf {
it.isVisible && it.accept(instruction) && it != defaultFragmentContainer
} ?: containerManager.containers
.filter { it.isVisible }
.filterNot { it == defaultFragmentContainer }
.firstOrNull { it.accept(instruction) }
.let {
val useDefaultFragmentContainer = it == null &&
fromContext.parentContext == null &&
defaultFragmentContainer != null &&
defaultFragmentContainer.accept(instruction)

val useActivityContainer = it == null &&
fromContext.parentContext == null &&
instruction.navigationDirection != NavigationDirection.Push

when {
useDefaultFragmentContainer -> defaultFragmentContainer
useActivityContainer -> ActivityNavigationContainer(fromContext.activity.navigationContext)
else -> it
}
val visited = alreadyVisitedContainerKeys.toMutableSet()
val container = containerManager
.getActiveChildContainers(exclude = visited)
.onEach { visited.add(it.key.name) }
.firstOrNull {
it.isVisible && it.accept(instruction) && it != defaultFragmentContainer
}
?: containerManager.getChildContainers(exclude = visited)
.onEach { visited.add(it.key.name) }
.filter { it.isVisible }
.filterNot { it == defaultFragmentContainer }
.firstOrNull { it.accept(instruction) }
.let {
val useDefaultFragmentContainer = it == null &&
fromContext.parentContext == null &&
defaultFragmentContainer != null &&
defaultFragmentContainer.accept(instruction)

val useActivityContainer = it == null &&
fromContext.parentContext == null &&
instruction.navigationDirection != NavigationDirection.Push

return container ?: findContainerFor(fromContext.parentContext, instruction)
when {
useDefaultFragmentContainer -> defaultFragmentContainer
useActivityContainer -> ActivityNavigationContainer(fromContext.activity.navigationContext)
else -> it
}
}

return container ?: findContainerFor(
fromContext = fromContext.parentContext,
instruction = instruction,
alreadyVisitedContainerKeys = visited,
)
}
}

/**
* Returns a list of active child containers down from a particular NavigationContainerManager, the results in the list
* should be in descending distance from the container manager that this was invoked on. This means that the first result will
* be the active container for this container manager, and the next result will be the active container for that container manager,
* and so on. This method also takes an "exclude" parameter, which will exclude any containers with the given keys from the results,
* including their children.
*/
private fun NavigationContainerManager.getActiveChildContainers(
exclude: Set<String>,
): List<NavigationContainer> {
var activeContainer = activeContainer
val result = mutableListOf<NavigationContainer>()
while (activeContainer != null) {
if (exclude.contains(activeContainer.key.name)) {
break
}
result.add(activeContainer)
activeContainer = activeContainer.childContext?.containerManager?.activeContainer
}
return result
}

/**
* Returns a list of all child containers down from a particular NavigationContainerManager, the results in the list
* should be in descending distance from the container manager that this was invoked on. This is a breadth first search,
* and doesn't take into account the active container. This method also takes an "exclude" parameter, which will exclude any
* results with the given keys from the results, including the children of containers which are excluded.
*/
private fun NavigationContainerManager.getChildContainers(
exclude: Set<String>,
): List<NavigationContainer> {
val toVisit = mutableListOf<NavigationContainer>()
toVisit.addAll(containers)

val result = mutableListOf<NavigationContainer>()
while (toVisit.isNotEmpty()) {
val next = toVisit.removeAt(0)
if (exclude.contains(next.key.name)) {
continue
}
result.add(next)
toVisit.addAll(next.childContext?.containerManager?.containers.orEmpty())
}
}
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public sealed class EmptyBehavior {
* "true", then the action is considered to have consumed the request to become empty, and the container
* will not close the last navigation destination. When the action function returns "false", the default
* behaviour will happen, and the container will become empty.
*
* @returns true to keep the destination in the container, false to allow the container to become empty
*/
public class Action(
public val onEmpty: () -> Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import java.io.Closeable
* is recreated, they must be bound every time a NavigationHandle object is created. For example, in the `onOpened`
* method of an EnroPlugin.
*
* Extras which extend java.io.Closeable will automatically have their close method called when the NavigationHandle is
* destroyed.
* Extras which extend java.io.Closeable will automatically have their close method called when the NavigationHandle is destroyed.
*/
public class NavigationHandleExtras : Closeable {
internal val extras: SnapshotStateMap<String, Any> = mutableStateMapOf()
Expand All @@ -28,5 +27,15 @@ public class NavigationHandleExtras : Closeable {
}
}

/**
* Access the extras map on a NavigationHandle.
*
* NavigationHandle.extras can be used for storing additional information or state with a NavigationHandle. Extras are not
* recreated when a NavigationHandle is recreated, and must be bound every time a NavigationHandle object is created.
*
* Extras which extend java.io.Closeable will automatically have their close method called when the NavigationHandle is destroyed.
*
* @see [NavigationHandleExtras]
*/
public val NavigationHandle.extras: MutableMap<String, Any>
get() = dependencyScope.get<NavigationHandleExtras>().extras
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.enro.test.application.compose.FindContextRobot
import dev.enro.test.application.compose.LegacyBottomSheetsRobot
import dev.enro.test.application.compose.SyntheticViewModelAccessRobot
import dev.enro.test.application.compose.results.ComposeEmbeddedResultFlowRobot
import dev.enro.test.application.compose.results.ComposeNestedResultsRobot
import dev.enro.test.application.fragment.UnboundBottomSheetRobot
import dev.enro.tests.application.SelectDestination

Expand Down Expand Up @@ -117,4 +118,14 @@ class SelectDestinationRobot(

return ComposeEmbeddedResultFlowRobot(composeRule)
}

fun openComposeNestedResults(): ComposeNestedResultsRobot {
composeRule.onNode(hasText("Compose Nested Results"))
.performScrollTo()
.onSiblings()
.filterToOne(hasText("Push"))
.performClick()

return ComposeNestedResultsRobot(composeRule)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package dev.enro.test.application.compose.results

import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.espresso.Espresso
import dev.enro.test.application.waitForNavigationHandle
import dev.enro.tests.application.compose.results.ComposeNestedResults

class ComposeNestedResultsRobot(
private val composeRule: ComposeTestRule
) {
init {
composeRule.waitForNavigationHandle {
it.key is ComposeNestedResults
}
}

fun getReceiver() : ReceiverRobot {
return ReceiverRobot(composeRule)
}

class ReceiverRobot(private val composeRule: ComposeTestRule) {
init {
composeRule.waitForNavigationHandle {
it.key is ComposeNestedResults.Receiver
}
}

fun assertCurrentResult(result: String) : ReceiverRobot {
composeRule.onNodeWithText("Current Result: $result")
.assertExists()
return this
}

fun assertSenderContainerIsVisible() : ReceiverRobot {
composeRule.onNodeWithText("Nested Sender Container")
.assertExists()
return this
}

fun assertSenderContainerNotVisible() : ReceiverRobot {
composeRule.onNodeWithText("Nested Sender Container")
.assertDoesNotExist()
return this
}

fun openNestedSenderContainer(): ReceiverRobot {
composeRule.onNodeWithText("Open Nested Sender Container")
.performScrollTo()
.performClick()

NestedSenderContainerRobot(composeRule)
return this
}

fun openSender(): SenderRobot {
composeRule.onNodeWithText("Get Result")
.performScrollTo()
.performClick()

return SenderRobot(composeRule)
}

}

class NestedSenderContainerRobot(private val composeRule: ComposeTestRule) {
init {
composeRule.waitForNavigationHandle {
it.key is ComposeNestedResults.NestedSenderContainer
}
}
}

class SenderRobot(private val composeRule: ComposeTestRule) {
init {
composeRule.waitForNavigationHandle {
it.key is ComposeNestedResults.Sender
}
}

fun assertIsInsideContainer(): SenderRobot {
composeRule.onNodeWithText("Nested Sender Container")
.assertExists()
return this
}

fun assertIsNotInContainer(): SenderRobot {
composeRule.onNodeWithText("Nested Sender Container")
.assertDoesNotExist()
return this
}

fun sendA(): ReceiverRobot {
composeRule.onNodeWithText("Send A")
.performClick()

return ReceiverRobot(composeRule)
}

fun sendB(): ReceiverRobot {
composeRule.onNodeWithText("Send B")
.performClick()

return ReceiverRobot(composeRule)
}

fun closeWithButton(): ReceiverRobot {
composeRule.onNodeWithText("Close")
.performClick()

return ReceiverRobot(composeRule)
}

fun closeWithBackPress(): ReceiverRobot {
Espresso.pressBack()
return ReceiverRobot(composeRule)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package dev.enro.test.application.compose.results

import androidx.compose.ui.test.junit4.createAndroidComposeRule
import dev.enro.test.application.SelectDestinationRobot
import dev.enro.tests.application.TestActivity
import org.junit.Rule
import org.junit.Test

class ComposeNestedResultsTest {
@get:Rule
val composeRule = createAndroidComposeRule<TestActivity>()

@Test
fun test() {
SelectDestinationRobot(composeRule)
.openComposeNestedResults()
.getReceiver()
.assertCurrentResult("(None)")

.openSender()
.assertIsNotInContainer()
.sendA()
.assertCurrentResult("A")
.assertSenderContainerNotVisible()

.openSender()
.assertIsNotInContainer()
.closeWithButton()
.assertCurrentResult("Closed")
.assertSenderContainerNotVisible()

.openSender()
.assertIsNotInContainer()
.sendB()
.assertCurrentResult("B")
.assertSenderContainerNotVisible()

.openSender()
.assertIsNotInContainer()
.closeWithBackPress()
.assertCurrentResult("Closed")
.assertSenderContainerNotVisible()

.openNestedSenderContainer()
.assertSenderContainerIsVisible()
.openSender()
.assertIsInsideContainer()
.sendB()
.assertCurrentResult("B")
.assertSenderContainerNotVisible()

.openNestedSenderContainer()
.assertSenderContainerIsVisible()
.openSender()
.assertIsInsideContainer()
.closeWithButton()
.assertCurrentResult("Closed")
.assertSenderContainerNotVisible()

.openNestedSenderContainer()
.assertSenderContainerIsVisible()
.openSender()
.assertIsInsideContainer()
.sendA()
.assertCurrentResult("A")
.assertSenderContainerNotVisible()

.openNestedSenderContainer()
.assertSenderContainerIsVisible()
.openSender()
.assertIsInsideContainer()
.closeWithBackPress()
.assertCurrentResult("Closed")
.assertSenderContainerNotVisible()

}
}
Loading

0 comments on commit a735521

Please sign in to comment.