Add tests of BrowseActivity

Bug: 256582545
Test: unit-test & local build gallery
Change-Id: If2c258b307250ac0e77fb7e0978eeef6fecac3bc
This commit is contained in:
Zekan Qian
2022-11-25 14:28:45 +08:00
parent c9d48ffda4
commit 5387ee15f5
5 changed files with 192 additions and 83 deletions

View File

@@ -19,6 +19,7 @@ package com.android.settingslib.spa.framework
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
@@ -37,6 +38,7 @@ import androidx.navigation.compose.rememberNavController
import com.android.settingslib.spa.R
import com.android.settingslib.spa.framework.common.LogCategory
import com.android.settingslib.spa.framework.common.SettingsPage
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.LocalNavController
@@ -74,80 +76,13 @@ open class BrowseActivity : ComponentActivity() {
setContent {
SettingsTheme {
MainContent()
}
}
}
@Composable
private fun MainContent() {
val sppRepository by spaEnvironment.pageProviderRepository
val navController = rememberNavController()
val nullPage = SettingsPage.createNull()
CompositionLocalProvider(navController.localNavController()) {
NavHost(
navController = navController,
startDestination = nullPage.sppName,
) {
composable(nullPage.sppName) {}
for (spp in sppRepository.getAllProviders()) {
composable(
route = spp.name + spp.parameter.navRoute(),
arguments = spp.parameter,
) { navBackStackEntry ->
PageLogger(remember(navBackStackEntry.arguments) {
spp.createSettingsPage(arguments = navBackStackEntry.arguments)
})
spp.Page(navBackStackEntry.arguments)
}
}
}
InitialDestinationNavigator()
}
}
@Composable
private fun PageLogger(settingsPage: SettingsPage) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
settingsPage.enterPage()
} else if (event == Lifecycle.Event.ON_STOP) {
settingsPage.leavePage()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
@Composable
private fun InitialDestinationNavigator() {
val sppRepository by spaEnvironment.pageProviderRepository
val destinationNavigated = rememberSaveable { mutableStateOf(false) }
if (destinationNavigated.value) return
destinationNavigated.value = true
val controller = LocalNavController.current as NavControllerWrapperImpl
LaunchedEffect(Unit) {
val destination =
intent?.getStringExtra(KEY_DESTINATION) ?: sppRepository.getDefaultStartPage()
val highlightEntryId = intent?.getStringExtra(KEY_HIGHLIGHT_ENTRY)
if (destination.isNotEmpty()) {
controller.highlightId = highlightEntryId
val navController = controller.navController
navController.navigate(destination) {
popUpTo(navController.graph.findStartDestination().id) {
inclusive = true
}
}
val sppRepository by spaEnvironment.pageProviderRepository
BrowseContent(
allProviders = sppRepository.getAllProviders(),
initialDestination = intent?.getStringExtra(KEY_DESTINATION)
?: sppRepository.getDefaultStartPage(),
initialEntryId = intent?.getStringExtra(KEY_HIGHLIGHT_ENTRY)
)
}
}
}
@@ -157,3 +92,81 @@ open class BrowseActivity : ComponentActivity() {
const val KEY_HIGHLIGHT_ENTRY = "highlightEntry"
}
}
@VisibleForTesting
@Composable
fun BrowseContent(
allProviders: Collection<SettingsPageProvider>,
initialDestination: String,
initialEntryId: String?
) {
val navController = rememberNavController()
CompositionLocalProvider(navController.localNavController()) {
val controller = LocalNavController.current as NavControllerWrapperImpl
controller.NavContent(allProviders)
controller.InitialDestination(initialDestination, initialEntryId)
}
}
@Composable
private fun SettingsPageProvider.PageEvents(arguments: Bundle? = null) {
val page = remember(arguments) { createSettingsPage(arguments) }
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
page.enterPage()
} else if (event == Lifecycle.Event.ON_STOP) {
page.leavePage()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
@Composable
private fun NavControllerWrapperImpl.NavContent(allProvider: Collection<SettingsPageProvider>) {
val nullPage = SettingsPage.createNull()
NavHost(
navController = navController,
startDestination = nullPage.sppName,
) {
composable(nullPage.sppName) {}
for (spp in allProvider) {
composable(
route = spp.name + spp.parameter.navRoute(),
arguments = spp.parameter,
) { navBackStackEntry ->
spp.PageEvents(navBackStackEntry.arguments)
spp.Page(navBackStackEntry.arguments)
}
}
}
}
@Composable
private fun NavControllerWrapperImpl.InitialDestination(
destination: String,
highlightEntryId: String?
) {
val destinationNavigated = rememberSaveable { mutableStateOf(false) }
if (destinationNavigated.value) return
destinationNavigated.value = true
if (destination.isEmpty()) return
LaunchedEffect(Unit) {
highlightId = highlightEntryId
navController.navigate(destination) {
popUpTo(navController.graph.findStartDestination().id) {
inclusive = true
}
}
}
}

View File

@@ -156,6 +156,15 @@ data class SettingsPage(
}
}
fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
return SettingsPage.create(
name = name,
displayName = displayName,
parameter = parameter,
arguments = arguments
)
}
fun String.toHashId(): String {
return this.hashCode().toUInt().toString(36)
}

View File

@@ -51,12 +51,3 @@ interface SettingsPageProvider {
}
}
}
fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
return SettingsPage.create(
name = name,
displayName = displayName,
parameter = parameter,
arguments = arguments
)
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.spa.framework
import android.content.Context
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.framework.common.LogCategory
import com.android.settingslib.spa.framework.common.LogEvent
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
import com.android.settingslib.spa.tests.testutils.SpaLoggerForTest
import com.android.settingslib.spa.testutils.waitUntil
import com.google.common.truth.Truth
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
const val WAIT_UNTIL_TIMEOUT = 1000L
@RunWith(AndroidJUnit4::class)
class BrowseActivityTest {
@get:Rule
val composeTestRule = createComposeRule()
private val context: Context = ApplicationProvider.getApplicationContext()
private val spaLogger = SpaLoggerForTest()
private val spaEnvironment = SpaEnvironmentForTest(context, logger = spaLogger)
@Test
fun testBrowsePage() {
spaLogger.reset()
SpaEnvironmentFactory.reset(spaEnvironment)
val sppRepository by spaEnvironment.pageProviderRepository
val sppHome = sppRepository.getProviderOrNull("SppHome")!!
val pageHome = sppHome.createSettingsPage()
val sppLayer1 = sppRepository.getProviderOrNull("SppLayer1")!!
val pageLayer1 = sppLayer1.createSettingsPage()
composeTestRule.setContent {
BrowseContent(
allProviders = listOf(sppHome, sppLayer1),
initialDestination = pageHome.buildRoute(),
initialEntryId = null
)
}
composeTestRule.onNodeWithText(sppHome.getTitle(null)).assertIsDisplayed()
spaLogger.verifyPageEvent(pageHome.id, 1, 0)
spaLogger.verifyPageEvent(pageLayer1.id, 0, 0)
// click to layer1 page
composeTestRule.onNodeWithText("SppHome to Layer1").assertIsDisplayed().performClick()
waitUntil(WAIT_UNTIL_TIMEOUT) {
composeTestRule.onAllNodesWithText(sppLayer1.getTitle(null))
.fetchSemanticsNodes().size == 1
}
spaLogger.verifyPageEvent(pageHome.id, 1, 1)
spaLogger.verifyPageEvent(pageLayer1.id, 1, 0)
}
}
private fun SpaLoggerForTest.verifyPageEvent(id: String, entryCount: Int, leaveCount: Int) {
Truth.assertThat(getEventCount(id, LogEvent.PAGE_ENTER, LogCategory.FRAMEWORK))
.isEqualTo(entryCount)
Truth.assertThat(getEventCount(id, LogEvent.PAGE_LEAVE, LogCategory.FRAMEWORK))
.isEqualTo(leaveCount)
}

View File

@@ -35,6 +35,7 @@ import com.android.settingslib.spa.framework.common.SettingsPageProviderReposito
import com.android.settingslib.spa.framework.common.SpaEnvironment
import com.android.settingslib.spa.framework.common.SpaLogger
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.widget.preference.SimplePreferenceMacro
class SpaLoggerForTest : SpaLogger {
data class MsgCountKey(val msg: String, val category: LogCategory)
@@ -98,6 +99,12 @@ object SppLayer1 : SettingsPageProvider {
fun buildInject(): SettingsEntryBuilder {
return SettingsEntryBuilder.createInject(this.createSettingsPage())
.setMacro {
SimplePreferenceMacro(
title = "SppHome to Layer1",
clickRoute = name
)
}
}
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {