Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix PermanentChild behaviour in test environment #268

Merged
merged 3 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this expected to be called by library users? otherwise internal? if it should be public, add some Javadoc? Maybe add some either way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is more useful than regular Add.

private val navTarget: @RawValue NavTarget,
) : PermanentOperation<NavTarget> {

override fun isApplicable(elements: NavElements<NavTarget, Int>): Boolean =
!elements.any { it.key.navTarget == 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 ->
CherryPerry marked this conversation as resolved.
Show resolved Hide resolved
navElements
.find { it.key.navTarget == navTarget }
?.let { childOrCreate(it.key) }
}
.distinctUntilChanged()
}.collectAsState(initial = null)
CherryPerry marked this conversation as resolved.
Show resolved Hide resolved
child?.let {
decorator(child = PermanentChildRender(it.node))
}
Expand Down