Skip to content

Commit

Permalink
Add gesture navigation tests for Android
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Dec 31, 2023
1 parent 3566414 commit 140dc4b
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 2 deletions.
20 changes: 19 additions & 1 deletion circuitx/gesture-navigation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,25 @@ kotlin {
}

val iosMain by getting { dependencies { api(libs.compose.material.material) } }

val androidUnitTest by getting {
dependencies {
implementation(projects.internalTestUtils)

implementation(libs.robolectric)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.ui.testing.junit)
implementation(libs.androidx.compose.ui.testing.manifest)
}
}
}
}

android { namespace = "com.slack.circuitx.gesturenavigation" }
android {
namespace = "com.slack.circuitx.gesturenavigation"

defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }

testOptions { unitTests.isIncludeAndroidResources = true }
testBuildType = "release"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (C) 2023 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.gesturenavigation

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.slack.circuit.backstack.rememberSaveableBackStack
import com.slack.circuit.foundation.CircuitCompositionLocals
import com.slack.circuit.foundation.NavigableCircuitContent
import com.slack.circuit.foundation.rememberCircuitNavigator
import com.slack.circuit.test.TestContentTags.TAG_COUNT
import com.slack.circuit.test.TestContentTags.TAG_GO_NEXT
import com.slack.circuit.test.TestContentTags.TAG_INCREASE_COUNT
import com.slack.circuit.test.TestContentTags.TAG_LABEL
import com.slack.circuit.test.TestCountPresenter.RememberType
import com.slack.circuit.test.TestScreen
import com.slack.circuit.test.createTestCircuit
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@Config(minSdk = 34)
@RunWith(AndroidJUnit4::class)
class GestureNavigationRetainedStateTest {

@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@Test fun retainedStateScopedToBackstackWithKeys() = retainedStateScopedToBackstack(true)

@Test fun retainedStateScopedToBackstackWithoutKeys() = retainedStateScopedToBackstack(false)

private fun retainedStateScopedToBackstack(useKeys: Boolean) {
composeTestRule.run {
val circuit = createTestCircuit(useKeys = useKeys, rememberType = RememberType.Retained)

setContent {
CircuitCompositionLocals(circuit) {
val backstack = rememberSaveableBackStack { push(TestScreen.ScreenA) }
val navigator =
rememberCircuitNavigator(
backstack = backstack,
onRootPop = {} // no-op for tests
)
NavigableCircuitContent(
navigator = navigator,
backstack = backstack,
decoration = GestureNavigationDecoration(onBackInvoked = navigator::pop),
)
}
}

// Current: Screen A. Increase count to 1
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("A")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")
onTopNavigationRecordNodeWithTag(TAG_INCREASE_COUNT).performClick()
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")

// Navigate to Screen B. Increase count to 1
onTopNavigationRecordNodeWithTag(TAG_GO_NEXT).performClick()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("B")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")
onTopNavigationRecordNodeWithTag(TAG_INCREASE_COUNT).performClick()
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")

// Navigate to Screen C. Increase count to 1
onTopNavigationRecordNodeWithTag(TAG_GO_NEXT).performClick()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("C")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")
onTopNavigationRecordNodeWithTag(TAG_INCREASE_COUNT).performClick()
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")

// Pop to Screen B. Increase count from 1 to 2.
composeTestRule.activityRule.scenario.performBackSwipeGesture()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("B")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")
onTopNavigationRecordNodeWithTag(TAG_INCREASE_COUNT).performClick()
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("2")

// Navigate to Screen C. Assert that it's state was not retained
onTopNavigationRecordNodeWithTag(TAG_GO_NEXT).performClick()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("C")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")

// Pop to Screen B. Assert that it's state was retained
composeTestRule.activityRule.scenario.performBackSwipeGesture()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("B")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("2")

// Pop to Screen A. Assert that it's state was retained
composeTestRule.activityRule.scenario.performBackSwipeGesture()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("A")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")

// Navigate to Screen B. Assert that it's state was not retained
onTopNavigationRecordNodeWithTag(TAG_GO_NEXT).performClick()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("B")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (C) 2023 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.gesturenavigation

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.slack.circuit.backstack.rememberSaveableBackStack
import com.slack.circuit.foundation.CircuitCompositionLocals
import com.slack.circuit.foundation.NavigableCircuitContent
import com.slack.circuit.foundation.rememberCircuitNavigator
import com.slack.circuit.test.TestContentTags.TAG_COUNT
import com.slack.circuit.test.TestContentTags.TAG_GO_NEXT
import com.slack.circuit.test.TestContentTags.TAG_INCREASE_COUNT
import com.slack.circuit.test.TestContentTags.TAG_LABEL
import com.slack.circuit.test.TestCountPresenter
import com.slack.circuit.test.TestScreen
import com.slack.circuit.test.createTestCircuit
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@Config(minSdk = 34)
@RunWith(AndroidJUnit4::class)
class GestureNavigationSaveableStateTest {

@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@Test fun saveableStateScopedToBackstackWithKeys() = saveableStateScopedToBackstack(true)

@Test fun saveableStateScopedToBackstackWithoutKeys() = saveableStateScopedToBackstack(false)

private fun saveableStateScopedToBackstack(useKeys: Boolean) {
composeTestRule.run {
val circuit =
createTestCircuit(
useKeys = useKeys,
rememberType = TestCountPresenter.RememberType.Saveable
)

setContent {
CircuitCompositionLocals(circuit) {
val backstack = rememberSaveableBackStack { push(TestScreen.ScreenA) }
val navigator =
rememberCircuitNavigator(
backstack = backstack,
onRootPop = {} // no-op for tests
)
NavigableCircuitContent(
navigator = navigator,
backstack = backstack,
decoration = GestureNavigationDecoration(onBackInvoked = navigator::pop),
)
}
}

// Current: Screen A. Increase count to 1
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("A")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")
onTopNavigationRecordNodeWithTag(TAG_INCREASE_COUNT).performClick()
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")

// Navigate to Screen B. Increase count to 1
onTopNavigationRecordNodeWithTag(TAG_GO_NEXT).performClick()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("B")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")
onTopNavigationRecordNodeWithTag(TAG_INCREASE_COUNT).performClick()
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")

// Navigate to Screen C. Increase count to 1
onTopNavigationRecordNodeWithTag(TAG_GO_NEXT).performClick()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("C")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")
onTopNavigationRecordNodeWithTag(TAG_INCREASE_COUNT).performClick()
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")

// Pop to Screen B. Increase count from 1 to 2.
composeTestRule.activityRule.scenario.performBackSwipeGesture()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("B")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")
onTopNavigationRecordNodeWithTag(TAG_INCREASE_COUNT).performClick()
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("2")

// Navigate to Screen C. Assert that it's state was not saved
onTopNavigationRecordNodeWithTag(TAG_GO_NEXT).performClick()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("C")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")

// Pop to Screen B. Assert that it's state was saved
composeTestRule.activityRule.scenario.performBackSwipeGesture()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("B")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("2")

// Pop to Screen A. Assert that it's state was saved
composeTestRule.activityRule.scenario.performBackSwipeGesture()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("A")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("1")

// Navigate to Screen B. Assert that it's state was not saved
onTopNavigationRecordNodeWithTag(TAG_GO_NEXT).performClick()
onTopNavigationRecordNodeWithTag(TAG_LABEL).assertTextEquals("B")
onTopNavigationRecordNodeWithTag(TAG_COUNT).assertTextEquals("0")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (C) 2023 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.gesturenavigation

import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.onFirst
import androidx.test.core.app.ActivityScenario
import com.slack.circuit.test.TestContentTags

internal fun SemanticsNodeInteractionsProvider.onTopNavigationRecordNodeWithTag(
testTag: String,
): SemanticsNodeInteraction =
onAllNodes(hasTestTag(testTag) and hasParent(hasTestTag(TestContentTags.TAG_ROOT)), false)
// first is always on top
.onFirst()

internal fun BackEventCompat.copy(
touchX: Float = this.touchX,
touchY: Float = this.touchY,
progress: Float = this.progress,
swipeEdge: Int = this.swipeEdge,
): BackEventCompat =
BackEventCompat(
touchX = touchX,
touchY = touchY,
progress = progress,
swipeEdge = swipeEdge,
)

internal fun ActivityScenario<ComponentActivity>.performBackSwipeGesture() {
onActivity { activity ->
val event =
BackEventCompat(
touchX = 0f,
touchY = activity.window.decorView.height / 2f,
progress = 0f,
swipeEdge = BackEventCompat.EDGE_LEFT
)

with(activity.onBackPressedDispatcher) {
dispatchOnBackStarted(event)
// Now dispatch a series of back progress events up to progress == 1f
dispatchOnBackProgressed(event.copy(touchX = 10f, progress = 0.2f))
dispatchOnBackProgressed(event.copy(touchX = 20f, progress = 0.4f))
dispatchOnBackProgressed(event.copy(touchX = 30f, progress = 0.6f))
dispatchOnBackProgressed(event.copy(touchX = 40f, progress = 0.8f))
dispatchOnBackProgressed(event.copy(touchX = 60f, progress = 1f))
// Finally, trigger a 'back press' to finish the gesture
onBackPressed()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.slack.circuit.runtime.screen.Screen
import com.slack.circuit.runtime.ui.ui

object TestContentTags {
const val TAG_ROOT = "root"
const val TAG_GO_NEXT = "go"
const val TAG_POP = "pop"
const val TAG_INCREASE_COUNT = "inc"
Expand Down Expand Up @@ -55,7 +56,7 @@ sealed class TestScreen(val label: String) : Screen {

@Composable
fun TestContent(state: TestState, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Column(modifier = modifier.testTag(TestContentTags.TAG_ROOT)) {
BasicText(text = state.label, modifier = Modifier.testTag(TestContentTags.TAG_LABEL))

BasicText(text = "${state.count}", modifier = Modifier.testTag(TestContentTags.TAG_COUNT))
Expand Down

0 comments on commit 140dc4b

Please sign in to comment.