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

Enhance End To End Test #14

Merged
merged 6 commits into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
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