Skip to content

Commit

Permalink
Fix PermanentChild behaviour in test environment
Browse files Browse the repository at this point in the history
  • Loading branch information
CherryPerry committed Nov 8, 2022
1 parent 10695e5 commit d555cb2
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Pending changes

- [#268](https://github.com/bumble-tech/appyx/pull/268)**Fixed**: `PermanentChild` now does not crash in UI tests with `ComposeTestRule`.

---

Expand Down
2 changes: 1 addition & 1 deletion libraries/core/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<application>

<activity
android:name="com.bumble.appyx.core.plugin.BackPressHandlerTestActivity"
android:name="com.bumble.appyx.core.InternalAppyxTestActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />

</application>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.bumble.appyx.core.plugin
package com.bumble.appyx.core

import androidx.annotation.WorkerThread
import androidx.compose.runtime.Composable
Expand All @@ -25,18 +25,21 @@ class AppyxTestScenario<T : Node>(
) : ComposeTestRule by composeTestRule {

@get:WorkerThread
val activityScenario: ActivityScenario<BackPressHandlerTestActivity> by lazy {
val activityScenario: ActivityScenario<InternalAppyxTestActivity> by lazy {
val awaitNode = CountDownLatch(1)
AppyxTestActivity.composableView = { activity ->
decorator {
NodeHost(integrationPoint = activity.appyxIntegrationPoint, factory = { buildContext ->
node = nodeFactory.create(buildContext)
awaitNode.countDown()
node
})
NodeHost(
integrationPoint = activity.appyxIntegrationPoint,
factory = { buildContext ->
node = nodeFactory.create(buildContext)
awaitNode.countDown()
node
},
)
}
}
val scenario = ActivityScenario.launch(BackPressHandlerTestActivity::class.java)
val scenario = ActivityScenario.launch(InternalAppyxTestActivity::class.java)
require(awaitNode.await(10, TimeUnit.SECONDS)) {
"Timeout while waiting for node initialization"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.bumble.appyx.core.plugin
package com.bumble.appyx.core

import android.os.Bundle
import androidx.activity.OnBackPressedCallback
Expand All @@ -7,7 +7,7 @@ import com.bumble.appyx.testing.ui.rules.AppyxTestActivity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

class BackPressHandlerTestActivity : AppyxTestActivity() {
class InternalAppyxTestActivity : AppyxTestActivity() {

private val callback = object : OnBackPressedCallback(handleBackPress.value) {
override fun handleOnBackPressed() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.bumble.appyx.core.node

import android.os.Parcelable
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.hasTestTag
import com.bumble.appyx.core.AppyxTestScenario
import com.bumble.appyx.core.children.nodeOrNull
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.EmptyNavModel
import kotlinx.parcelize.Parcelize
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

class PermanentChildTest {

@get:Rule
val rule = AppyxTestScenario { buildContext ->
TestParentNode(buildContext)
}

@Test
fun permanent_child_is_rendered() {
rule.start()

rule.onNode(hasTestTag(TestParentNode.NavTarget::class.java.name)).assertExists()
}

@Test
fun permanent_child_is_reused_when_visibility_switched() {
rule.start()
rule.node.renderPermanentChild = false
val childNodes = rule.node.children.value.values.map { it.nodeOrNull }

rule.onNode(hasTestTag(TestParentNode.NavTarget::class.java.name)).assertDoesNotExist()

rule.node.renderPermanentChild = true

rule.onNode(hasTestTag(TestParentNode.NavTarget::class.java.name)).assertExists()
assertEquals(childNodes, rule.node.children.value.values.map { it.nodeOrNull })
}

class TestParentNode(
buildContext: BuildContext,
) : ParentNode<TestParentNode.NavTarget>(
buildContext = buildContext,
navModel = EmptyNavModel<NavTarget, Any>(),
) {

@Parcelize
object NavTarget : Parcelable

var renderPermanentChild by mutableStateOf(true)

override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node =
node(buildContext) { modifier ->
BasicText(
text = navTarget.toString(),
modifier = modifier.testTag(NavTarget::class.java.name),
)
}

@Composable
override fun View(modifier: Modifier) {
if (renderPermanentChild) {
PermanentChild(NavTarget)
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import androidx.lifecycle.Lifecycle
import androidx.test.espresso.Espresso
import androidx.test.platform.app.InstrumentationRegistry
import com.bumble.appyx.Appyx
import com.bumble.appyx.core.AppyxTestScenario
import com.bumble.appyx.core.InternalAppyxTestActivity
import com.bumble.appyx.core.children.nodeOrNull
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
Expand Down Expand Up @@ -50,7 +52,7 @@ class BackPressHandlerTest {
@After
fun after() {
Appyx.exceptionHandler = null
BackPressHandlerTestActivity.reset()
InternalAppyxTestActivity.reset()
}

@Test
Expand Down Expand Up @@ -132,7 +134,7 @@ class BackPressHandlerTest {

@Test
fun appyx_handles_back_press_before_activity_handler() {
BackPressHandlerTestActivity.handleBackPress.value = true
InternalAppyxTestActivity.handleBackPress.value = true
rule.start()
pushChildB()

Expand All @@ -145,15 +147,15 @@ class BackPressHandlerTest {

@Test
fun activity_handles_back_press_if_appyx_cant() {
BackPressHandlerTestActivity.handleBackPress.value = true
InternalAppyxTestActivity.handleBackPress.value = true
rule.start()
disablePlugin()

Espresso.pressBack()
Espresso.onIdle()
rule.waitForIdle()

assertThat(BackPressHandlerTestActivity.onBackPressedHandled, equalTo(true))
assertThat(InternalAppyxTestActivity.onBackPressedHandled, equalTo(true))
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.bumble.appyx.core.navigation.model.permanent.operation

import com.bumble.appyx.core.navigation.NavElement
import com.bumble.appyx.core.navigation.NavElements
import com.bumble.appyx.core.navigation.NavKey
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

@Parcelize
data class AddUnique<NavTarget : Any>(
private val navTarget: @RawValue NavTarget,
) : PermanentOperation<NavTarget> {

override fun isApplicable(elements: NavElements<NavTarget, Int>): Boolean =
!elements.any { it.key == navTarget }

override fun invoke(
elements: NavElements<NavTarget, Int>
): NavElements<NavTarget, Int> =
if (elements.any { it.key.navTarget == navTarget }) {
elements
} else {
elements + NavElement(
key = NavKey(navTarget),
fromState = 0,
targetState = 0,
operation = this,
)
}
}

fun <NavTarget : Any> PermanentNavModel<NavTarget>.addUnique(navTarget: NavTarget) {
accept(AddUnique(navTarget))
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
Expand All @@ -33,11 +32,13 @@ import com.bumble.appyx.core.navigation.Resolver
import com.bumble.appyx.core.navigation.isTransitioning
import com.bumble.appyx.core.navigation.model.combined.plus
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.navigation.model.permanent.operation.add
import com.bumble.appyx.core.navigation.model.permanent.operation.addUnique
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.state.MutableSavedStateMap
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -98,15 +99,19 @@ abstract class ParentNode<NavTarget : Any>(
navTarget: NavTarget,
decorator: @Composable (child: ChildRenderer) -> Unit
) {
var child by remember { mutableStateOf<ChildEntry.Initialized<*>?>(null) }
LaunchedEffect(navTarget) {
permanentNavModel.elements.collect { elements ->
val navKey = elements.find { it.key.navTarget == navTarget }?.key
?: NavKey(navTarget).also { permanentNavModel.add(it) }
child = childOrCreate(navKey)
}
permanentNavModel.addUnique(navTarget)
}

val child by remember(navTarget) {
permanentNavModel
.elements
.map { navElements ->
navElements
.find { it.key.navTarget == navTarget }
?.let { childOrCreate(it.key) }
}
.distinctUntilChanged()
}.collectAsState(initial = null)
child?.let {
decorator(child = PermanentChildRender(it.node))
}
Expand Down

0 comments on commit d555cb2

Please sign in to comment.