Add tests of BrowseActivity

Bug: 256582545
Test: unit-test & local build gallery
Change-Id: If2c258b307250ac0e77fb7e0978eeef6fecac3bc
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index c3c90ab..6ed7481 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -19,6 +19,7 @@
 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 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 @@
 
         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 @@
         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
+            }
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index a372bbd..82e05a5 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -156,6 +156,15 @@
     }
 }
 
+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)
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index 60599d4..940005d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -51,12 +51,3 @@
         }
     }
 }
-
-fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
-    return SettingsPage.create(
-        name = name,
-        displayName = displayName,
-        parameter = parameter,
-        arguments = arguments
-    )
-}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/BrowseActivityTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/BrowseActivityTest.kt
new file mode 100644
index 0000000..bde3bba
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/BrowseActivityTest.kt
@@ -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)
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
index a404dd3..ab269f2 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
@@ -35,6 +35,7 @@
 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 @@
 
     fun buildInject(): SettingsEntryBuilder {
         return SettingsEntryBuilder.createInject(this.createSettingsPage())
+            .setMacro {
+                SimplePreferenceMacro(
+                    title = "SppHome to Layer1",
+                    clickRoute = name
+                )
+            }
     }
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {