Skip to content

Commit

Permalink
Merge pull request #14 from azrael8576/test/end-to-end
Browse files Browse the repository at this point in the history
Enhance end to end test
  • Loading branch information
azrael8576 authored Oct 7, 2023
2 parents c7dafaa + 4b10552 commit 776f1e8
Show file tree
Hide file tree
Showing 30 changed files with 993 additions and 175 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/Build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
- name: Setup Gradle
uses: gradle/gradle-build-action@v2

# If the test fails, execute the following command to apply fixes.
# ./gradlew spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache
- name: Check spotless
run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache

Expand Down
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,14 @@ dependencies {
androidTestImplementation(project(":core:designsystem"))
androidTestImplementation(project(":core:datastore-test"))
androidTestImplementation(project(":core:testing"))
androidTestImplementation(libs.androidx.navigation.testing)
androidTestImplementation(libs.accompanist.testharness)
testImplementation(project(":core:datastore-test"))
testImplementation(project(":core:testing"))
testImplementation(libs.androidx.navigation.testing)
testImplementation(libs.accompanist.testharness)
debugImplementation(project(":ui-test-hilt-manifest"))
debugImplementation(libs.androidx.compose.ui.testManifest)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
Expand Down
286 changes: 286 additions & 0 deletions app/src/androidTest/java/com/wei/amazingtalker/ui/AtAppStateTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
package com.wei.amazingtalker.ui

import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.DpSize
import androidx.navigation.NavHostController
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.compose.composable
import androidx.navigation.createGraph
import androidx.navigation.testing.TestNavHostController
import androidx.window.layout.FoldingFeature
import com.google.common.truth.Truth.assertThat
import com.wei.amazingtalker.core.designsystem.ui.AtContentType
import com.wei.amazingtalker.core.designsystem.ui.AtNavigationType
import com.wei.amazingtalker.core.testing.util.TestNetworkMonitor
import com.wei.amazingtalker.utilities.COMPACT_HEIGHT
import com.wei.amazingtalker.utilities.COMPACT_WIDTH
import com.wei.amazingtalker.utilities.EXPANDED_HEIGHT
import com.wei.amazingtalker.utilities.EXPANDED_WIDTH
import com.wei.amazingtalker.utilities.FoldingDeviceUtil
import com.wei.amazingtalker.utilities.MEDIUM_HEIGHT
import com.wei.amazingtalker.utilities.MEDIUM_WIDTH
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test

/**
* Tests [AtAppState].
*
* Note: This could become an unit test if Robolectric is added to the project and the Context
* is faked.
*/
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
class AtAppStateTest {

@get:Rule
val composeTestRule = createComposeRule()

// Create the test dependencies.
private val networkMonitor = TestNetworkMonitor()

// Subject under test.
private lateinit var state: AtAppState

@Test
fun verifyCurrentDestinationIsSet_whenNavigatedToDestination() = runTest {
var currentDestination: String? = null

composeTestRule.setContent {
val navController = rememberTestNavController()
state = remember(navController) {
AtAppState(
navController = navController,
coroutineScope = backgroundScope,
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
}

// Update currentDestination whenever it changes
currentDestination = state.currentDestination?.route

// Navigate to destination b once
LaunchedEffect(Unit) {
navController.setCurrentDestination("b")
}
}

assertThat("b").isEqualTo(currentDestination)
}

@Test
fun verifyTopLevelDestinationsContainExpectedNames() = runTest {
composeTestRule.setContent {
state = rememberAtAppState(
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
}

assertThat(state.topLevelDestinations).hasSize(3)
assertThat(state.topLevelDestinations[0].name).ignoringCase().contains("schedule")
assertThat(state.topLevelDestinations[1].name).ignoringCase().contains("home")
assertThat(state.topLevelDestinations[2].name).ignoringCase().contains("contact_me")
}

@Test
fun verifyBottomNavigationDisplayed_whenWindowSizeIsCompact() = runTest {
composeTestRule.setContent {
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
assertThat(state.navigationType).isEqualTo(AtNavigationType.BOTTOM_NAVIGATION)
}
}

@Test
fun verifyNavigationRailDisplayed_whenWindowSizeIsMedium() = runTest {
composeTestRule.setContent {
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(
MEDIUM_WIDTH,
MEDIUM_HEIGHT,
),
),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
assertThat(state.navigationType).isEqualTo(AtNavigationType.NAVIGATION_RAIL)
}
}

@Test
fun verifyPermanentNavDrawerDisplayed_whenWindowSizeIsExpanded() = runTest {
composeTestRule.setContent {
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(
EXPANDED_WIDTH,
EXPANDED_HEIGHT,
),
),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
assertThat(state.navigationType).isEqualTo(AtNavigationType.PERMANENT_NAVIGATION_DRAWER)
}
}

@Test
fun verifyContentTypeIsSINGLE_PANE_whenWindowSizeIsCompact() = runTest {
composeTestRule.setContent {
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = getCompactWindowClass(),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
assertThat(state.contentType).isEqualTo(AtContentType.SINGLE_PANE)
}
}

@Test
fun verifyContentTypeIsSINGLE_PANE_whenWindowSizeIsMedium_withNormalPosture() = runTest {
composeTestRule.setContent {
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(
MEDIUM_WIDTH,
MEDIUM_HEIGHT,
),
),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
assertThat(state.contentType).isEqualTo(AtContentType.SINGLE_PANE)
}
}

@Test
fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsMedium_withBookPosture() = runTest {
composeTestRule.setContent {
val dpSize = DpSize(MEDIUM_WIDTH, MEDIUM_HEIGHT)
val foldBounds = FoldingDeviceUtil.getFoldBounds(dpSize)
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(dpSize),
networkMonitor = networkMonitor,
displayFeatures = listOf(
FoldingDeviceUtil.getFoldingFeature(
foldBounds,
FoldingFeature.State.HALF_OPENED,
),
),
)
assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE)
}
}

@Test
fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsMedium_withSeparating() = runTest {
composeTestRule.setContent {
val dpSize = DpSize(MEDIUM_WIDTH, MEDIUM_HEIGHT)
val foldBounds = FoldingDeviceUtil.getFoldBounds(dpSize)
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(dpSize),
networkMonitor = networkMonitor,
displayFeatures = listOf(
FoldingDeviceUtil.getFoldingFeature(
foldBounds,
FoldingFeature.State.FLAT,
),
),
)
assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE)
}
}

@Test
fun verifyContentTypeIsDUAL_PANE_whenWindowSizeIsExpanded() = runTest {
composeTestRule.setContent {
val dpSize = DpSize(EXPANDED_WIDTH, EXPANDED_HEIGHT)
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(dpSize),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
assertThat(state.contentType).isEqualTo(AtContentType.DUAL_PANE)
}
}

@Test
fun verifyStateIsOffline_whenNetworkMonitorReportsDisconnection() =
runTest(UnconfinedTestDispatcher()) {
val results = mutableListOf<Boolean>()

composeTestRule.setContent {
state = AtAppState(
navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope,
windowSizeClass = WindowSizeClass.calculateFromSize(
DpSize(
EXPANDED_WIDTH,
EXPANDED_HEIGHT,
),
),
networkMonitor = networkMonitor,
displayFeatures = emptyList(),
)
}

backgroundScope.launch {
state.isOffline.collect { value ->
results.add(value)
}
}

networkMonitor.setConnected(false)
assertTrue(results.contains(true))
}

private fun getCompactWindowClass() =
WindowSizeClass.calculateFromSize(DpSize(COMPACT_WIDTH, COMPACT_HEIGHT))
}

@Composable
private fun rememberTestNavController(): TestNavHostController {
val context = LocalContext.current
return remember<TestNavHostController> {
TestNavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavigator())
graph = createGraph(startDestination = "a") {
composable("a") { }
composable("b") { }
composable("c") { }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import androidx.annotation.StringRes
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.rules.ActivityScenarioRule
import com.wei.amazingtalker.MainActivity
import kotlin.properties.ReadOnlyProperty

/**
* Robot for [NavigationTest].
*
* 遵循此模型,找到測試使用者介面元素、檢查其屬性、和透過測試規則執行動作:
* composeTestRule{.finder}{.assertion}{.action}
*
* Testing cheatsheet:
* https://developer.android.com/jetpack/compose/testing-cheatsheet
*/
internal fun navigationRobot(
composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>,
func: NavigationRobot.() -> Unit,
) = NavigationRobot(composeTestRule).apply(func)

internal open class NavigationRobot(
private val composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>,
) {
private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) =
ReadOnlyProperty<Any?, String> { _, _ -> activity.getString(resId) }

// The strings used for matching in these tests
private val schedule by composeTestRule.stringResource(com.wei.amazingtalker.R.string.schedule)
private val home by composeTestRule.stringResource(com.wei.amazingtalker.R.string.home)
private val contactMe by composeTestRule.stringResource(com.wei.amazingtalker.R.string.contact_me)
private val backDescription by composeTestRule.stringResource(com.wei.amazingtalker.feature.teacherschedule.R.string.content_description_back)

private val back by lazy {
composeTestRule.onNodeWithContentDescription(
backDescription,
useUnmergedTree = true,
)
}

private val navSchedule by lazy {
composeTestRule.onNodeWithContentDescription(
schedule,
useUnmergedTree = true,
)
}
private val navHome by lazy {
composeTestRule.onNodeWithContentDescription(
home,
useUnmergedTree = true,
)
}
private val navContactMe by lazy {
composeTestRule.onNodeWithContentDescription(
contactMe,
useUnmergedTree = true,
)
}

internal fun verifyBackNotExist() {
back.assertDoesNotExist()
}

internal fun clickNavSchedule() {
navSchedule.performClick()
// 等待任何動畫完成
composeTestRule.waitForIdle()
}

private fun clickNavHome() {
navHome.performClick()
// 等待任何動畫完成
composeTestRule.waitForIdle()
}

internal fun clickNavContactMe() {
navContactMe.performClick()
// 等待任何動畫完成
composeTestRule.waitForIdle()
}
}
Loading

0 comments on commit 776f1e8

Please sign in to comment.