diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/OnboardingNavigation.kt b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/OnboardingNavigation.kt index ce45ca75550..208bb840d40 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/OnboardingNavigation.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/OnboardingNavigation.kt @@ -422,7 +422,6 @@ internal fun NavGraphBuilder.wearOnboarding( } navigation(startDestination = startRoute) { - // TODO discovery should be able to add existing system commonScreens(navController = navController, wearNameToOnboard = wearNameToOnboard) nameYourWearDeviceScreen( onBackClick = navController::popBackStack, @@ -451,6 +450,5 @@ internal fun NavGraphBuilder.wearOnboarding( }, onNext = onOnboardingDone, ) - // TODO: Consider making auth_code a value class to prevent string parameter mismatches } } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModel.kt index 372cb0eaaee..6747633731b 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/LocationSharingViewModel.kt @@ -8,6 +8,7 @@ import androidx.navigation.toRoute import dagger.hilt.android.lifecycle.HiltViewModel import io.homeassistant.companion.android.database.sensor.SensorDao import io.homeassistant.companion.android.onboarding.locationsharing.navigation.LocationSharingRoute +import io.homeassistant.companion.android.sensors.LocationSensorManager import javax.inject.Inject import kotlinx.coroutines.launch import timber.log.Timber @@ -29,10 +30,9 @@ internal class LocationSharingViewModel @VisibleForTesting constructor( try { sensorDao.setSensorsEnabled( sensorIds = listOf( - // TODO add sensor ID from `LocationSensorManager` instead of string - "location_background", - "zone_background", - "accurate_location", + LocationSensorManager.backgroundLocation.id, + LocationSensorManager.zoneLocation.id, + LocationSensorManager.singleAccurateLocation.id, ), serverId = serverId, enabled = enabled, diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/manualserver/ManualServerScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/manualserver/ManualServerScreen.kt index 2bb2f5f1fe5..68e1084ec7e 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/manualserver/ManualServerScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/manualserver/ManualServerScreen.kt @@ -194,7 +194,6 @@ private fun ServerUrlTextField( if (isError) { Text( text = stringResource(commonR.string.manual_server_wrong_url), - // TODO probably wrong style and color/token style = HATextStyle.BodyMedium.copy(color = LocalHAColorScheme.current.colorBorderDangerNormal), ) } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreen.kt index b95f3d48993..8d8d7f5b70b 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/ServerDiscoveryScreen.kt @@ -191,8 +191,7 @@ private fun OneServerFound( imageVector = Icons.Default.Storage, contentDescription = null, modifier = Modifier - .size(ICON_SIZE), // TODO double check the size of the icon within the modal - // TODO change the color with proper token + .size(ICON_SIZE), tint = LocalHAColorScheme.current.colorFillPrimaryLoudResting, ) Text( @@ -384,7 +383,7 @@ private fun AnimatedIcon() { .size(80.dp) .scale(pulse) .align(Alignment.Center) - .background(HABrandColors.Blue, CircleShape), // TODO we might want to use a semantic token? + .background(HABrandColors.Blue, CircleShape), ) { Icon( imageVector = ImageVector.vectorResource(commonR.drawable.ic_stat_ic_notification_blue), diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/BaseOnboardingNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/BaseOnboardingNavigationTest.kt new file mode 100644 index 00000000000..09897f57c85 --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/BaseOnboardingNavigationTest.kt @@ -0,0 +1,116 @@ +package io.homeassistant.companion.android.onboarding + +import android.content.pm.PackageManager +import androidx.activity.compose.LocalActivityResultRegistryOwner +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.ActivityResultRegistryOwner +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.core.content.ContextCompat +import androidx.navigation.NavController +import androidx.navigation.compose.ComposeNavigator +import androidx.navigation.compose.NavHost +import androidx.navigation.testing.TestNavHostController +import dagger.hilt.android.testing.HiltAndroidRule +import io.homeassistant.companion.android.HiltComponentActivity +import io.homeassistant.companion.android.testing.unit.ConsoleLogRule +import io.homeassistant.companion.android.util.LocationPermissionActivityResultRegistry +import io.homeassistant.companion.android.util.compose.navigateToUri +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockkStatic +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule + +/** + * Base class for onboarding navigation tests providing shared setup, mocks, and utilities. + * + * Subclasses should extend this class and use [testNavigation] to set up the navigation + * test environment with the onboarding flow. + */ +internal abstract class BaseOnboardingNavigationTest { + + @get:Rule(order = 0) + var consoleLog = ConsoleLogRule() + + @get:Rule(order = 1) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 2) + val composeTestRule = createAndroidComposeRule() + + protected lateinit var navController: TestNavHostController + + protected var onboardingDone = false + + @Before + fun baseSetup() { + mockkStatic(NavController::navigateToUri) + every { any().navigateToUri(any()) } just Runs + } + + protected fun setContent( + urlToOnboard: String? = null, + hideExistingServers: Boolean = false, + skipWelcome: Boolean = false, + hasLocationTracking: Boolean = true, + ) { + composeTestRule.setContent { + navController = TestNavHostController(LocalContext.current) + navController.navigatorProvider.addNavigator(ComposeNavigator()) + + CompositionLocalProvider( + LocalActivityResultRegistryOwner provides object : ActivityResultRegistryOwner { + override val activityResultRegistry: ActivityResultRegistry = + LocationPermissionActivityResultRegistry(true) + }, + ) { + NavHost( + navController = navController, + startDestination = OnboardingRoute(hasLocationTracking = true), + ) { + onboarding( + navController, + onShowSnackbar = { _, _ -> true }, + onOnboardingDone = { + onboardingDone = true + }, + urlToOnboard = urlToOnboard, + hideExistingServers = hideExistingServers, + skipWelcome = skipWelcome, + hasLocationTracking = hasLocationTracking, + ) + } + } + } + } + + protected fun testNavigation( + urlToOnboard: String? = null, + hideExistingServers: Boolean = false, + skipWelcome: Boolean = false, + hasLocationTracking: Boolean = true, + testContent: suspend AndroidComposeTestRule<*, *>.() -> Unit, + ) { + setContent( + urlToOnboard = urlToOnboard, + hideExistingServers = hideExistingServers, + skipWelcome = skipWelcome, + hasLocationTracking = hasLocationTracking, + ) + runTest { + composeTestRule.testContent() + } + } + + protected fun mockCheckPermission(grant: Boolean) { + mockkStatic(ContextCompat::class) + every { + ContextCompat.checkSelfPermission(any(), any()) + } returns if (grant) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/OnboardingNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/OnboardingNavigationTest.kt deleted file mode 100644 index a20a0ff8e60..00000000000 --- a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/OnboardingNavigationTest.kt +++ /dev/null @@ -1,665 +0,0 @@ -package io.homeassistant.companion.android.onboarding - -import android.content.pm.PackageManager -import androidx.activity.compose.LocalActivityResultRegistryOwner -import androidx.activity.result.ActivityResultRegistry -import androidx.activity.result.ActivityResultRegistryOwner -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertIsNotDisplayed -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollTo -import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeUp -import androidx.core.content.ContextCompat -import androidx.navigation.NavController -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.compose.ComposeNavigator -import androidx.navigation.compose.NavHost -import androidx.navigation.testing.TestNavHostController -import androidx.navigation.toRoute -import dagger.hilt.android.testing.BindValue -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import dagger.hilt.android.testing.HiltTestApplication -import dagger.hilt.android.testing.UninstallModules -import io.homeassistant.companion.android.HiltComponentActivity -import io.homeassistant.companion.android.common.R as commonR -import io.homeassistant.companion.android.common.data.HomeAssistantVersion -import io.homeassistant.companion.android.onboarding.connection.CONNECTION_SCREEN_TAG -import io.homeassistant.companion.android.onboarding.connection.ConnectionNavigationEvent -import io.homeassistant.companion.android.onboarding.connection.ConnectionViewModel -import io.homeassistant.companion.android.onboarding.connection.navigation.ConnectionRoute -import io.homeassistant.companion.android.onboarding.localfirst.navigation.LocalFirstRoute -import io.homeassistant.companion.android.onboarding.localfirst.navigation.navigateToLocalFirst -import io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation.LocationForSecureConnectionRoute -import io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation.navigateToLocationForSecureConnection -import io.homeassistant.companion.android.onboarding.locationsharing.navigation.LocationSharingRoute -import io.homeassistant.companion.android.onboarding.locationsharing.navigation.navigateToLocationSharing -import io.homeassistant.companion.android.onboarding.manualserver.navigation.ManualServerRoute -import io.homeassistant.companion.android.onboarding.nameyourdevice.NameYourDeviceNavigationEvent -import io.homeassistant.companion.android.onboarding.nameyourdevice.NameYourDeviceViewModel -import io.homeassistant.companion.android.onboarding.nameyourdevice.navigation.NameYourDeviceRoute -import io.homeassistant.companion.android.onboarding.nameyourdevice.navigation.navigateToNameYourDevice -import io.homeassistant.companion.android.onboarding.serverdiscovery.DELAY_BEFORE_DISPLAY_DISCOVERY -import io.homeassistant.companion.android.onboarding.serverdiscovery.HomeAssistantInstance -import io.homeassistant.companion.android.onboarding.serverdiscovery.HomeAssistantSearcher -import io.homeassistant.companion.android.onboarding.serverdiscovery.ONE_SERVER_FOUND_MODAL_TAG -import io.homeassistant.companion.android.onboarding.serverdiscovery.ServerDiscoveryModule -import io.homeassistant.companion.android.onboarding.serverdiscovery.navigation.ServerDiscoveryMode -import io.homeassistant.companion.android.onboarding.serverdiscovery.navigation.ServerDiscoveryRoute -import io.homeassistant.companion.android.onboarding.serverdiscovery.navigation.navigateToServerDiscovery -import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.SetHomeNetworkRoute -import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.navigateToSetHomeNetworkRoute -import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute -import io.homeassistant.companion.android.testing.unit.ConsoleLogRule -import io.homeassistant.companion.android.testing.unit.stringResource -import io.homeassistant.companion.android.util.LocationPermissionActivityResultRegistry -import io.homeassistant.companion.android.util.compose.navigateToUri -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.verify -import java.net.URL -import junit.framework.TestCase.assertTrue -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -@RunWith(RobolectricTestRunner::class) -@Config(application = HiltTestApplication::class) -@UninstallModules(ServerDiscoveryModule::class) -@HiltAndroidTest -internal class OnboardingNavigationTest { - @get:Rule(order = 0) - var consoleLog = ConsoleLogRule() - - @get:Rule(order = 1) - val hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 2) - val composeTestRule = createAndroidComposeRule() - - @BindValue - @JvmField - val searcher: HomeAssistantSearcher = object : HomeAssistantSearcher { - override fun discoveredInstanceFlow(): Flow { - return instanceChannel.consumeAsFlow() - } - } - - private val connectionNavigationEventFlow = MutableSharedFlow() - - @BindValue - @JvmField - val connectionViewModel: ConnectionViewModel = mockk(relaxed = true) { - every { urlFlow } returns MutableStateFlow("http://homeassistant.local:8123") - every { isLoadingFlow } returns MutableStateFlow(false) - every { navigationEventsFlow } returns connectionNavigationEventFlow - every { errorFlow } returns MutableStateFlow(null) - } - - private val nameYourDeviceNavigationFlow = MutableSharedFlow() - - @BindValue - @JvmField - val nameYourDeviceViewModel: NameYourDeviceViewModel = mockk(relaxed = true) { - every { navigationEventsFlow } returns nameYourDeviceNavigationFlow - every { onSaveClick() } coAnswers { - nameYourDeviceNavigationFlow.emit(NameYourDeviceNavigationEvent.DeviceNameSaved(42, hasPlainTextAccess = false, isPubliclyAccessible = false)) - } - every { deviceNameFlow } returns MutableStateFlow("Test") - every { isValidNameFlow } returns MutableStateFlow(true) - every { isSaveClickableFlow } returns MutableStateFlow(true) - every { isSavingFlow } returns MutableStateFlow(false) - } - - private val instanceChannel = Channel() - - private lateinit var navController: TestNavHostController - - private var onboardingDone = false - - @Before - fun setup() { - mockkStatic(NavController::navigateToUri) - every { any().navigateToUri(any()) } just Runs - } - - private fun setContent( - urlToOnboard: String? = null, - hideExistingServers: Boolean = false, - skipWelcome: Boolean = false, - hasLocationTracking: Boolean = true, - ) { - composeTestRule.setContent { - navController = TestNavHostController(LocalContext.current) - navController.navigatorProvider.addNavigator(ComposeNavigator()) - - CompositionLocalProvider( - LocalActivityResultRegistryOwner provides object : ActivityResultRegistryOwner { - override val activityResultRegistry: ActivityResultRegistry = LocationPermissionActivityResultRegistry(true) - }, - ) { - NavHost( - navController = navController, - startDestination = OnboardingRoute(hasLocationTracking = true), - ) { - onboarding( - navController, - onShowSnackbar = { message, action -> true }, - onOnboardingDone = { - onboardingDone = true - }, - urlToOnboard = urlToOnboard, - hideExistingServers = hideExistingServers, - skipWelcome = skipWelcome, - hasLocationTracking = hasLocationTracking, - ) - } - } - } - } - - private fun testNavigation( - urlToOnboard: String? = null, - hideExistingServers: Boolean = false, - skipWelcome: Boolean = false, - hasLocationTracking: Boolean = true, - testContent: suspend AndroidComposeTestRule<*, *>.() -> Unit, - ) { - setContent( - urlToOnboard = urlToOnboard, - hideExistingServers = hideExistingServers, - skipWelcome = skipWelcome, - hasLocationTracking = hasLocationTracking, - ) - runTest { - composeTestRule.testContent() - } - } - - @Test - fun `Given no action when starting the app then show Welcome`() { - testNavigation { - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - onNodeWithText(stringResource(commonR.string.welcome_learn_more)).performScrollTo().assertIsDisplayed().performClick() - verify { any().navigateToUri("https://www.home-assistant.io") } - } - } - - @Test - fun `Given skipWelcome without urlToOnboard when starting then show ServerDiscovery`() { - testNavigation(skipWelcome = true) { - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given skipWelcome and urlToOnboard when starting then show ServerDiscovery and no back arrow`() { - val url = "http://ha.org" - testNavigation(skipWelcome = true, urlToOnboard = url) { - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - assertEquals(url, navController.currentBackStackEntry?.toRoute()?.url) - - onNodeWithContentDescription(stringResource(commonR.string.navigate_up)).assertIsNotDisplayed() - } - } - - @Test - fun `Given clicking on connect button when starting the onboarding then show ServerDiscovery then back goes to Welcome`() { - testNavigation { - onNodeWithText(stringResource(commonR.string.welcome_connect_to_ha)).assertIsDisplayed().performClick() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithContentDescription(stringResource(commonR.string.get_help)).performClick() - verify { any().navigateToUri("https://www.home-assistant.io/installation/") } - - onNodeWithContentDescription(stringResource(commonR.string.navigate_up)).assertIsDisplayed().performClick() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given clicking on connect button with server to onboard when starting the onboarding then show Connection screen then back goes to Welcome`() { - testNavigation("http://homeassistant.local") { - onNodeWithText(stringResource(commonR.string.welcome_connect_to_ha)).assertIsDisplayed().performClick() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithTag(CONNECTION_SCREEN_TAG).assertIsDisplayed() - - composeTestRule.activity.onBackPressedDispatcher.onBackPressed() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given clicking on connect button with hide existing server and no server to onboard when starting the onboarding then show Discovery screen with existing server hidden then back goes to Welcome`() { - testNavigation(hideExistingServers = true) { - onNodeWithText(stringResource(commonR.string.welcome_connect_to_ha)).assertIsDisplayed().performClick() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - assertTrue(navController.currentBackStackEntry?.toRoute()?.discoveryMode == ServerDiscoveryMode.HIDE_EXISTING) - - onNodeWithContentDescription(stringResource(commonR.string.navigate_up)).assertIsDisplayed().performClick() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given clicking enter manual address button when discovering server then show ManualServer then back goes to ServerDiscovery`() { - testNavigation { - navController.navigateToServerDiscovery() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - onNodeWithText(stringResource(commonR.string.manual_setup)).performScrollTo().assertIsDisplayed().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithContentDescription(stringResource(commonR.string.get_help)).performClick() - verify { any().navigateToUri("https://www.home-assistant.io/installation/") } - - onNodeWithContentDescription(stringResource(commonR.string.navigate_up)).assertIsDisplayed().performClick() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given enter manual address when setting url and clicking connect then show ConnectScreen then back goes to ManualServer`() { - testNavigation { - navController.navigateToServerDiscovery() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - onNodeWithText(stringResource(commonR.string.manual_setup)).performScrollTo().assertIsDisplayed().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithContentDescription(stringResource(commonR.string.get_help)).performClick() - verify { any().navigateToUri("https://www.home-assistant.io/installation/") } - - onNodeWithText("http://homeassistant.local:8123").performTextInput("http://ha.local") - - onNodeWithText(stringResource(commonR.string.connect)).performScrollTo().assertIsDisplayed().assertIsEnabled().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - onNodeWithTag(CONNECTION_SCREEN_TAG).assertIsDisplayed() - - composeTestRule.activity.onBackPressedDispatcher.onBackPressed() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @OptIn(ExperimentalTestApi::class) - @Test - fun `Given a server discovered when clicking on it then show ConnectScreen then back goes to ServerDiscovery`() { - val instanceUrl = "http://ha.local" - testNavigation { - navController.navigateToServerDiscovery() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - onNodeWithText(stringResource(commonR.string.manual_setup)).performScrollTo().assertIsDisplayed() - - instanceChannel.trySend(HomeAssistantInstance("Test", URL(instanceUrl), HomeAssistantVersion(2025, 9, 1))) - - onNodeWithContentDescription(stringResource(commonR.string.get_help)).performClick() - verify { any().navigateToUri("https://www.home-assistant.io/installation/") } - - // Wait for the screen to update based on the instance given in instanceChannel - waitUntilAtLeastOneExists(hasText(instanceUrl), timeoutMillis = DELAY_BEFORE_DISPLAY_DISCOVERY.inWholeMilliseconds) - - onNodeWithTag(ONE_SERVER_FOUND_MODAL_TAG).performTouchInput { - swipeUp(startY = bottom * 0.9f, endY = centerY, durationMillis = 200) - } - - waitForIdle() - - onNodeWithText(instanceUrl).assertIsDisplayed() - - onNodeWithText(stringResource(commonR.string.server_discovery_connect)).assertIsDisplayed().performClick() - - waitForIdle() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithTag(CONNECTION_SCREEN_TAG).assertIsDisplayed() - - composeTestRule.activity.onBackPressedDispatcher.onBackPressed() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @OptIn(ExperimentalTestApi::class) - @Test - fun `Given a server discovered and connecting when authenticated then show NameYourDevice then back goes to ServerDiscovery not ConnectionScreen`() { - val instanceUrl = "http://ha.local" - testNavigation { - navController.navigateToServerDiscovery() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - onNodeWithText(stringResource(commonR.string.manual_setup)).performScrollTo().assertIsDisplayed() - - instanceChannel.trySend(HomeAssistantInstance("Test", URL(instanceUrl), HomeAssistantVersion(2025, 9, 1))) - - // Wait for the screen to update based on the instance given in instanceChannel - waitUntilAtLeastOneExists(hasText(instanceUrl), timeoutMillis = DELAY_BEFORE_DISPLAY_DISCOVERY.inWholeMilliseconds) - - onNodeWithText(instanceUrl).assertIsDisplayed() - - onNodeWithText(stringResource(commonR.string.server_discovery_connect)).performClick() - - onNodeWithTag(CONNECTION_SCREEN_TAG).assertIsDisplayed() - - assertTrue(connectionNavigationEventFlow.subscriptionCount.value == 1) - connectionNavigationEventFlow.emit(ConnectionNavigationEvent.Authenticated(instanceUrl, "super_code", false)) - - waitUntilAtLeastOneExists(hasText(stringResource(commonR.string.name_your_device_title))) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - val route = navController.currentBackStackEntry?.toRoute() - assertEquals(instanceUrl, route?.url) - assertEquals("super_code", route?.authCode) - - onNodeWithContentDescription(stringResource(commonR.string.navigate_up)).performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given device named and skip welcome with url when pressing next then show LocalFirst then goes back stop the app`() { - localFirstTest(true, "http://ha.local") - } - - @Test - fun `Given device named and skip welcome without url when pressing next then show LocalFirst then goes back stop the app`() { - localFirstTest(true, null) - } - - @Test - fun `Given device named when pressing next then show LocalFirst then goes back stop the app`() { - localFirstTest(false, null) - } - - private fun localFirstTest(skipWelcome: Boolean, urlToOnboard: String?) { - testNavigation(skipWelcome = skipWelcome, urlToOnboard = urlToOnboard) { - navController.navigateToNameYourDevice("http://dummy.local", "code") - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.name_your_device_save)).performScrollTo().assertIsDisplayed().assertIsEnabled().performClick() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - composeTestRule.activity.onBackPressedDispatcher.onBackPressed() - - // The back stack is unchanged in this situation, but in reality the app is in background - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given LocalFirst when pressing next then show LocationSharing then goes back stop the app`() { - testNavigation { - navController.navigateToLocalFirst(42, true) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - onNodeWithText(stringResource(commonR.string.local_first_next)).performScrollTo().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - composeTestRule.activity.onBackPressedDispatcher.onBackPressed() - - // In the test scenario since we never opened NameYourDevice the stack still contains Welcome - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given device named with public HTTPS url when pressing next then show LocationSharing`() { - testDeviceNamedWithPublicUrl(hasPlainTextAccess = true) - } - - @Test - fun `Given device named with public HTTP url when pressing next then show LocationSharing`() { - testDeviceNamedWithPublicUrl(hasPlainTextAccess = false) - } - - private fun testDeviceNamedWithPublicUrl(hasPlainTextAccess: Boolean) { - every { nameYourDeviceViewModel.onSaveClick() } coAnswers { - nameYourDeviceNavigationFlow.emit( - NameYourDeviceNavigationEvent.DeviceNameSaved( - serverId = 42, - hasPlainTextAccess = hasPlainTextAccess, - isPubliclyAccessible = true, - ), - ) - } - testNavigation { - navController.navigateToNameYourDevice("http://homeassistant.local", "code") - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.name_your_device_save)).performScrollTo().assertIsDisplayed().assertIsEnabled().performClick() - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given LocationSharing when agreeing with plain text access to share then show SetHomeNetwork`() { - testNavigation { - navController.navigateToLocationSharing(42, true) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - mockCheckPermission(true) - - onNodeWithText(stringResource(commonR.string.location_sharing_share)).performScrollTo().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given LocationSharing when agreeing without plain text access to share then onboarding is done`() { - testNavigation { - navController.navigateToLocationSharing(42, false) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - mockCheckPermission(true) - onNodeWithText(stringResource(commonR.string.location_sharing_share)).performScrollTo().performClick() - - assertTrue(onboardingDone) - } - } - - @Test - fun `Given LocationSharing when denying to share with plain text access then goes to LocationForSecureConnection then goes back stop the app`() { - testNavigation { - navController.navigateToLocationSharing(42, true) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - mockCheckPermission(false) - - onNodeWithText(stringResource(commonR.string.location_sharing_no_share)).performScrollTo().performClick() - assertFalse(onboardingDone) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - composeTestRule.activity.onBackPressedDispatcher.onBackPressed() - - // In the test scenario since we never opened NameYourDevice the stack still contains Welcome - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given LocationSharing when denying to share without plain text access then onboarding is done`() { - testNavigation { - navController.navigateToLocationSharing(42, false) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - mockCheckPermission(false) - onNodeWithText(stringResource(commonR.string.location_sharing_no_share)).performScrollTo().performClick() - assertTrue(onboardingDone) - } - } - - @Test - fun `Given LocationForSecureConnection when agreeing to share then show SetHomeNetwork`() { - testNavigation { - navController.navigateToLocationForSecureConnection(42) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.connection_security_most_secure)).performScrollTo().performClick() - onNodeWithText(stringResource(commonR.string.location_secure_connection_next)).performScrollTo().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given SetHomeNetwork when going back then stop the app`() { - testNavigation { - navController.navigateToSetHomeNetworkRoute(42) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.set_home_network_next)).performScrollTo().performClick() - assertTrue(onboardingDone) - } - } - - @Test - fun `Given LocationForSecureConnection when choosing less secure option then onboarding completes`() { - testNavigation { - navController.navigateToLocationForSecureConnection(42) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.connection_security_less_secure)).performScrollTo().performClick() - onNodeWithText(stringResource(commonR.string.location_secure_connection_next)).performScrollTo().performClick() - - assertTrue(onboardingDone) - } - } - - @Test - fun `Given no location tracking with HTTPS public server when device named then onboarding completes`() { - every { nameYourDeviceViewModel.onSaveClick() } coAnswers { - nameYourDeviceNavigationFlow.emit( - NameYourDeviceNavigationEvent.DeviceNameSaved( - serverId = 42, - hasPlainTextAccess = false, - isPubliclyAccessible = true, - ), - ) - } - testNavigation(hasLocationTracking = false) { - navController.navigateToNameYourDevice("https://www.home-assistant.io", "code") - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.name_your_device_save)).performScrollTo().assertIsDisplayed().assertIsEnabled().performClick() - - assertTrue(onboardingDone) - } - } - - @Test - fun `Given no location tracking with HTTP public server and no location permission when device named then show LocationForSecureConnection`() { - every { nameYourDeviceViewModel.onSaveClick() } coAnswers { - nameYourDeviceNavigationFlow.emit( - NameYourDeviceNavigationEvent.DeviceNameSaved( - serverId = 42, - hasPlainTextAccess = true, - isPubliclyAccessible = true, - ), - ) - } - testNavigation(hasLocationTracking = false) { - mockCheckPermission(false) - navController.navigateToNameYourDevice("http://homeassistant.local", "code") - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.name_your_device_save)).performScrollTo().assertIsDisplayed().assertIsEnabled().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given no location tracking with HTTP public server and has location permission when device named then show SetHomeNetwork`() { - every { nameYourDeviceViewModel.onSaveClick() } coAnswers { - nameYourDeviceNavigationFlow.emit( - NameYourDeviceNavigationEvent.DeviceNameSaved( - serverId = 42, - hasPlainTextAccess = true, - isPubliclyAccessible = true, - ), - ) - } - testNavigation(hasLocationTracking = false) { - mockCheckPermission(true) - navController.navigateToNameYourDevice("http://homeassistant.local", "code") - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.name_your_device_save)).performScrollTo().assertIsDisplayed().assertIsEnabled().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given no location tracking from LocalFirst with HTTP and no permission when next clicked then show LocationForSecureConnection`() { - testNavigation(hasLocationTracking = false) { - mockCheckPermission(false) - navController.navigateToLocalFirst(serverId = 42, hasPlainTextAccess = true) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.local_first_next)).performScrollTo().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given no location tracking from LocalFirst with HTTP and has permission when next clicked then show SetHomeNetwork`() { - testNavigation(hasLocationTracking = false) { - mockCheckPermission(true) - navController.navigateToLocalFirst(serverId = 42, hasPlainTextAccess = true) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.local_first_next)).performScrollTo().performClick() - - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - } - } - - @Test - fun `Given no location tracking from LocalFirst with HTTPS when next clicked then onboarding completes`() { - testNavigation(hasLocationTracking = false) { - navController.navigateToLocalFirst(serverId = 42, hasPlainTextAccess = false) - assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - - onNodeWithText(stringResource(commonR.string.local_first_next)).performScrollTo().performClick() - - assertTrue(onboardingDone) - } - } - // TODO maybe split this file into multiples one dedicated to each screen -} - -private fun mockCheckPermission(grant: Boolean) { - mockkStatic(ContextCompat::class) - every { ContextCompat.checkSelfPermission(any(), any()) } returns if (grant) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED -} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/WearOnboardingNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/WearOnboardingNavigationTest.kt index 7b86e26113d..8d03fb27ff1 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/WearOnboardingNavigationTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/WearOnboardingNavigationTest.kt @@ -73,7 +73,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test -import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.runner.RunWith @@ -201,7 +200,7 @@ internal class WearOnboardingNavigationTest { @Test fun `Given server onboard when starting the navigation then opens ConnectionScreen`() { testNavigation("http://ha") { - Assertions.assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) onNodeWithTag(HA_WEBVIEW_TAG).assertIsDisplayed() } } diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/connection/navigation/ConnectionNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/connection/navigation/ConnectionNavigationTest.kt new file mode 100644 index 00000000000..0faee21ed3b --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/connection/navigation/ConnectionNavigationTest.kt @@ -0,0 +1,59 @@ +package io.homeassistant.companion.android.onboarding.connection.navigation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.toRoute +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.onboarding.connection.CONNECTION_SCREEN_TAG +import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute +import io.homeassistant.companion.android.testing.unit.stringResource +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Connection screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@HiltAndroidTest +internal class ConnectionNavigationTest : BaseOnboardingNavigationTest() { + + @Test + fun `Given skipWelcome and urlToOnboard when starting then show Connection screen and no back arrow`() { + val url = "http://ha.org" + testNavigation(skipWelcome = true, urlToOnboard = url) { + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertEquals(url, navController.currentBackStackEntry?.toRoute()?.url) + + onNodeWithContentDescription(stringResource(commonR.string.navigate_up)).assertIsNotDisplayed() + } + } + + @Test + fun `Given clicking on connect button with server to onboard when starting the onboarding then show Connection screen then back goes to Welcome`() { + testNavigation("http://homeassistant.local") { + onNodeWithText(stringResource(commonR.string.welcome_connect_to_ha)) + .assertIsDisplayed() + .performClick() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithTag(CONNECTION_SCREEN_TAG).assertIsDisplayed() + + composeTestRule.activity.onBackPressedDispatcher.onBackPressed() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/localfirst/navigation/LocalFirstNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/localfirst/navigation/LocalFirstNavigationTest.kt new file mode 100644 index 00000000000..78f757e5c10 --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/localfirst/navigation/LocalFirstNavigationTest.kt @@ -0,0 +1,93 @@ +package io.homeassistant.companion.android.onboarding.localfirst.navigation + +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.navigation.NavDestination.Companion.hasRoute +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation.LocationForSecureConnectionRoute +import io.homeassistant.companion.android.onboarding.locationsharing.navigation.LocationSharingRoute +import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.SetHomeNetworkRoute +import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute +import io.homeassistant.companion.android.testing.unit.stringResource +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Local First screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@HiltAndroidTest +internal class LocalFirstNavigationTest : BaseOnboardingNavigationTest() { + + @Test + fun `Given LocalFirst when pressing next then show LocationSharing then goes back stop the app`() { + testNavigation { + navController.navigateToLocalFirst(42, true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + onNodeWithText(stringResource(commonR.string.local_first_next)) + .performScrollTo() + .performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + composeTestRule.activity.onBackPressedDispatcher.onBackPressed() + + // In the test scenario since we never opened NameYourDevice the stack still contains Welcome + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given no location tracking from LocalFirst with HTTP and no permission when next clicked then show LocationForSecureConnection`() { + testNavigation(hasLocationTracking = false) { + mockCheckPermission(false) + navController.navigateToLocalFirst(serverId = 42, hasPlainTextAccess = true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.local_first_next)) + .performScrollTo() + .performClick() + + assertTrue( + navController.currentBackStackEntry?.destination?.hasRoute() == true, + ) + } + } + + @Test + fun `Given no location tracking from LocalFirst with HTTP and has permission when next clicked then show SetHomeNetwork`() { + testNavigation(hasLocationTracking = false) { + mockCheckPermission(true) + navController.navigateToLocalFirst(serverId = 42, hasPlainTextAccess = true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.local_first_next)) + .performScrollTo() + .performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given no location tracking from LocalFirst with HTTPS when next clicked then onboarding completes`() { + testNavigation(hasLocationTracking = false) { + navController.navigateToLocalFirst(serverId = 42, hasPlainTextAccess = false) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.local_first_next)) + .performScrollTo() + .performClick() + + assertTrue(onboardingDone) + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationforsecureconnection/navigation/LocationForSecureConnectionNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationforsecureconnection/navigation/LocationForSecureConnectionNavigationTest.kt new file mode 100644 index 00000000000..07ba6f45c47 --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationforsecureconnection/navigation/LocationForSecureConnectionNavigationTest.kt @@ -0,0 +1,64 @@ +package io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation + +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.navigation.NavDestination.Companion.hasRoute +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.SetHomeNetworkRoute +import io.homeassistant.companion.android.testing.unit.stringResource +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Location For Secure Connection screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@HiltAndroidTest +internal class LocationForSecureConnectionNavigationTest : BaseOnboardingNavigationTest() { + + @Test + fun `Given LocationForSecureConnection when agreeing to share then show SetHomeNetwork`() { + testNavigation { + navController.navigateToLocationForSecureConnection(42) + assertTrue( + navController.currentBackStackEntry?.destination?.hasRoute() == true, + ) + + onNodeWithText(stringResource(commonR.string.connection_security_most_secure)) + .performScrollTo() + .performClick() + onNodeWithText(stringResource(commonR.string.location_secure_connection_next)) + .performScrollTo() + .performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given LocationForSecureConnection when choosing less secure option then onboarding completes`() { + testNavigation { + navController.navigateToLocationForSecureConnection(42) + assertTrue( + navController.currentBackStackEntry?.destination?.hasRoute() == true, + ) + + onNodeWithText(stringResource(commonR.string.connection_security_less_secure)) + .performScrollTo() + .performClick() + onNodeWithText(stringResource(commonR.string.location_secure_connection_next)) + .performScrollTo() + .performClick() + + assertTrue(onboardingDone) + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/navigation/LocationSharingNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/navigation/LocationSharingNavigationTest.kt new file mode 100644 index 00000000000..ea0ed5e8ec8 --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/locationsharing/navigation/LocationSharingNavigationTest.kt @@ -0,0 +1,96 @@ +package io.homeassistant.companion.android.onboarding.locationsharing.navigation + +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.navigation.NavDestination.Companion.hasRoute +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation.LocationForSecureConnectionRoute +import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.SetHomeNetworkRoute +import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute +import io.homeassistant.companion.android.testing.unit.stringResource +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Location Sharing screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@HiltAndroidTest +internal class LocationSharingNavigationTest : BaseOnboardingNavigationTest() { + + @Test + fun `Given LocationSharing when agreeing with plain text access to share then show SetHomeNetwork`() { + testNavigation { + navController.navigateToLocationSharing(42, true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + mockCheckPermission(true) + + onNodeWithText(stringResource(commonR.string.location_sharing_share)) + .performScrollTo() + .performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given LocationSharing when agreeing without plain text access to share then onboarding is done`() { + testNavigation { + navController.navigateToLocationSharing(42, false) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + mockCheckPermission(true) + onNodeWithText(stringResource(commonR.string.location_sharing_share)) + .performScrollTo() + .performClick() + + assertTrue(onboardingDone) + } + } + + @Test + fun `Given LocationSharing when denying to share with plain text access then goes to LocationForSecureConnection then goes back stop the app`() { + testNavigation { + navController.navigateToLocationSharing(42, true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + mockCheckPermission(false) + + onNodeWithText(stringResource(commonR.string.location_sharing_no_share)) + .performScrollTo() + .performClick() + assertFalse(onboardingDone) + assertTrue( + navController.currentBackStackEntry?.destination?.hasRoute() == true, + ) + + composeTestRule.activity.onBackPressedDispatcher.onBackPressed() + + // In the test scenario since we never opened NameYourDevice the stack still contains Welcome + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given LocationSharing when denying to share without plain text access then onboarding is done`() { + testNavigation { + navController.navigateToLocationSharing(42, false) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + mockCheckPermission(false) + onNodeWithText(stringResource(commonR.string.location_sharing_no_share)) + .performScrollTo() + .performClick() + assertTrue(onboardingDone) + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/manualserver/navigation/ManualServerNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/manualserver/navigation/ManualServerNavigationTest.kt new file mode 100644 index 00000000000..2d229a95caf --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/manualserver/navigation/ManualServerNavigationTest.kt @@ -0,0 +1,69 @@ +package io.homeassistant.companion.android.onboarding.manualserver.navigation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import androidx.navigation.NavController +import androidx.navigation.NavDestination.Companion.hasRoute +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.onboarding.connection.CONNECTION_SCREEN_TAG +import io.homeassistant.companion.android.onboarding.connection.navigation.ConnectionRoute +import io.homeassistant.companion.android.onboarding.serverdiscovery.navigation.ServerDiscoveryRoute +import io.homeassistant.companion.android.onboarding.serverdiscovery.navigation.navigateToServerDiscovery +import io.homeassistant.companion.android.testing.unit.stringResource +import io.homeassistant.companion.android.util.compose.navigateToUri +import io.mockk.verify +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Manual Server screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@HiltAndroidTest +internal class ManualServerNavigationTest : BaseOnboardingNavigationTest() { + + @Test + fun `Given enter manual address when setting url and clicking connect then show ConnectScreen then back goes to ManualServer`() { + testNavigation { + navController.navigateToServerDiscovery() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + onNodeWithText(stringResource(commonR.string.manual_setup)) + .performScrollTo() + .assertIsDisplayed() + .performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithContentDescription(stringResource(commonR.string.get_help)).performClick() + verify { any().navigateToUri("https://www.home-assistant.io/installation/") } + + onNodeWithText("http://homeassistant.local:8123").performTextInput("http://ha.local") + + onNodeWithText(stringResource(commonR.string.connect)) + .performScrollTo() + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + onNodeWithTag(CONNECTION_SCREEN_TAG).assertIsDisplayed() + + composeTestRule.activity.onBackPressedDispatcher.onBackPressed() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/nameyourdevice/navigation/NameYourDeviceNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/nameyourdevice/navigation/NameYourDeviceNavigationTest.kt new file mode 100644 index 00000000000..9b4760b06cb --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/nameyourdevice/navigation/NameYourDeviceNavigationTest.kt @@ -0,0 +1,205 @@ +package io.homeassistant.companion.android.onboarding.nameyourdevice.navigation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.navigation.NavDestination.Companion.hasRoute +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.onboarding.localfirst.navigation.LocalFirstRoute +import io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation.LocationForSecureConnectionRoute +import io.homeassistant.companion.android.onboarding.locationsharing.navigation.LocationSharingRoute +import io.homeassistant.companion.android.onboarding.nameyourdevice.NameYourDeviceNavigationEvent +import io.homeassistant.companion.android.onboarding.nameyourdevice.NameYourDeviceViewModel +import io.homeassistant.companion.android.onboarding.sethomenetwork.navigation.SetHomeNetworkRoute +import io.homeassistant.companion.android.testing.unit.stringResource +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Name Your Device screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@HiltAndroidTest +internal class NameYourDeviceNavigationTest : BaseOnboardingNavigationTest() { + val nameYourDeviceNavigationFlow = MutableSharedFlow() + + @BindValue + @JvmField + val nameYourDeviceViewModel: NameYourDeviceViewModel = mockk(relaxed = true) { + every { navigationEventsFlow } returns nameYourDeviceNavigationFlow + every { onSaveClick() } coAnswers { + nameYourDeviceNavigationFlow.emit( + NameYourDeviceNavigationEvent.DeviceNameSaved( + serverId = 42, + hasPlainTextAccess = false, + isPubliclyAccessible = false, + ), + ) + } + every { deviceNameFlow } returns MutableStateFlow("Test") + every { isValidNameFlow } returns MutableStateFlow(true) + every { isSaveClickableFlow } returns MutableStateFlow(true) + every { isSavingFlow } returns MutableStateFlow(false) + } + + @Test + fun `Given device named and skip welcome with url when pressing next then show LocalFirst then goes back stop the app`() { + localFirstTest(skipWelcome = true, urlToOnboard = "http://ha.local") + } + + @Test + fun `Given device named and skip welcome without url when pressing next then show LocalFirst then goes back stop the app`() { + localFirstTest(skipWelcome = true, urlToOnboard = null) + } + + @Test + fun `Given device named when pressing next then show LocalFirst then goes back stop the app`() { + localFirstTest(skipWelcome = false, urlToOnboard = null) + } + + private fun localFirstTest(skipWelcome: Boolean, urlToOnboard: String?) { + testNavigation(skipWelcome = skipWelcome, urlToOnboard = urlToOnboard) { + navController.navigateToNameYourDevice("http://dummy.local", "code") + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.name_your_device_save)) + .performScrollTo() + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + composeTestRule.activity.onBackPressedDispatcher.onBackPressed() + + // The back stack is unchanged in this situation, but in reality the app is in background + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given device named with public HTTPS url when pressing next then show LocationSharing`() { + testDeviceNamedWithPublicUrl(hasPlainTextAccess = true) + } + + @Test + fun `Given device named with public HTTP url when pressing next then show LocationSharing`() { + testDeviceNamedWithPublicUrl(hasPlainTextAccess = false) + } + + private fun testDeviceNamedWithPublicUrl(hasPlainTextAccess: Boolean) { + every { nameYourDeviceViewModel.onSaveClick() } coAnswers { + nameYourDeviceNavigationFlow.emit( + NameYourDeviceNavigationEvent.DeviceNameSaved( + serverId = 42, + hasPlainTextAccess = hasPlainTextAccess, + isPubliclyAccessible = true, + ), + ) + } + testNavigation { + navController.navigateToNameYourDevice("http://homeassistant.local", "code") + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.name_your_device_save)) + .performScrollTo() + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given no location tracking with HTTPS public server when device named then onboarding completes`() { + every { nameYourDeviceViewModel.onSaveClick() } coAnswers { + nameYourDeviceNavigationFlow.emit( + NameYourDeviceNavigationEvent.DeviceNameSaved( + serverId = 42, + hasPlainTextAccess = false, + isPubliclyAccessible = true, + ), + ) + } + testNavigation(hasLocationTracking = false) { + navController.navigateToNameYourDevice("https://www.home-assistant.io", "code") + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.name_your_device_save)) + .performScrollTo() + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + + assertTrue(onboardingDone) + } + } + + @Test + fun `Given no location tracking with HTTP public server and no location permission when device named then show LocationForSecureConnection`() { + every { nameYourDeviceViewModel.onSaveClick() } coAnswers { + nameYourDeviceNavigationFlow.emit( + NameYourDeviceNavigationEvent.DeviceNameSaved( + serverId = 42, + hasPlainTextAccess = true, + isPubliclyAccessible = true, + ), + ) + } + testNavigation(hasLocationTracking = false) { + mockCheckPermission(false) + navController.navigateToNameYourDevice("http://homeassistant.local", "code") + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.name_your_device_save)) + .performScrollTo() + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + + assertTrue( + navController.currentBackStackEntry?.destination?.hasRoute() == true, + ) + } + } + + @Test + fun `Given no location tracking with HTTP public server and has location permission when device named then show SetHomeNetwork`() { + coEvery { nameYourDeviceViewModel.onSaveClick() } coAnswers { + nameYourDeviceNavigationFlow.emit( + NameYourDeviceNavigationEvent.DeviceNameSaved( + serverId = 42, + hasPlainTextAccess = true, + isPubliclyAccessible = true, + ), + ) + } + testNavigation(hasLocationTracking = false) { + mockCheckPermission(true) + navController.navigateToNameYourDevice("http://homeassistant.local", "code") + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.name_your_device_save)) + .performScrollTo() + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/navigation/ServerDiscoveryNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/navigation/ServerDiscoveryNavigationTest.kt new file mode 100644 index 00000000000..66038a6fe09 --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/serverdiscovery/navigation/ServerDiscoveryNavigationTest.kt @@ -0,0 +1,238 @@ +package io.homeassistant.companion.android.onboarding.serverdiscovery.navigation + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeUp +import androidx.navigation.NavController +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.toRoute +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import dagger.hilt.android.testing.UninstallModules +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.common.data.HomeAssistantVersion +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.onboarding.connection.CONNECTION_SCREEN_TAG +import io.homeassistant.companion.android.onboarding.connection.ConnectionNavigationEvent +import io.homeassistant.companion.android.onboarding.connection.ConnectionViewModel +import io.homeassistant.companion.android.onboarding.connection.navigation.ConnectionRoute +import io.homeassistant.companion.android.onboarding.manualserver.navigation.ManualServerRoute +import io.homeassistant.companion.android.onboarding.nameyourdevice.navigation.NameYourDeviceRoute +import io.homeassistant.companion.android.onboarding.serverdiscovery.DELAY_BEFORE_DISPLAY_DISCOVERY +import io.homeassistant.companion.android.onboarding.serverdiscovery.HomeAssistantInstance +import io.homeassistant.companion.android.onboarding.serverdiscovery.HomeAssistantSearcher +import io.homeassistant.companion.android.onboarding.serverdiscovery.ONE_SERVER_FOUND_MODAL_TAG +import io.homeassistant.companion.android.onboarding.serverdiscovery.ServerDiscoveryModule +import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute +import io.homeassistant.companion.android.testing.unit.stringResource +import io.homeassistant.companion.android.util.compose.navigateToUri +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import java.net.URL +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.consumeAsFlow +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Server Discovery screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@UninstallModules(ServerDiscoveryModule::class) +@HiltAndroidTest +internal class ServerDiscoveryNavigationTest : BaseOnboardingNavigationTest() { + + @BindValue + @JvmField + val searcher: HomeAssistantSearcher = object : HomeAssistantSearcher { + override fun discoveredInstanceFlow(): Flow { + return instanceChannel.consumeAsFlow() + } + } + + val instanceChannel = Channel() + + val connectionNavigationEventFlow = MutableSharedFlow() + + @BindValue + @JvmField + val connectionViewModel: ConnectionViewModel = mockk(relaxed = true) { + every { urlFlow } returns MutableStateFlow("http://homeassistant.local:8123") + every { isLoadingFlow } returns MutableStateFlow(false) + every { navigationEventsFlow } returns connectionNavigationEventFlow + every { errorFlow } returns MutableStateFlow(null) + } + + @Test + fun `Given skipWelcome without urlToOnboard when starting then show ServerDiscovery`() { + testNavigation(skipWelcome = true) { + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given clicking on connect button when starting the onboarding then show ServerDiscovery then back goes to Welcome`() { + testNavigation { + onNodeWithText(stringResource(commonR.string.welcome_connect_to_ha)) + .assertIsDisplayed() + .performClick() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithContentDescription(stringResource(commonR.string.get_help)).performClick() + verify { any().navigateToUri("https://www.home-assistant.io/installation/") } + + onNodeWithContentDescription(stringResource(commonR.string.navigate_up)) + .assertIsDisplayed() + .performClick() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given clicking on connect button with hide existing server and no server to onboard when starting the onboarding then show Discovery screen with existing server hidden then back goes to Welcome`() { + testNavigation(hideExistingServers = true) { + onNodeWithText(stringResource(commonR.string.welcome_connect_to_ha)) + .assertIsDisplayed() + .performClick() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertTrue( + navController.currentBackStackEntry?.toRoute()?.discoveryMode == + ServerDiscoveryMode.HIDE_EXISTING, + ) + + onNodeWithContentDescription(stringResource(commonR.string.navigate_up)) + .assertIsDisplayed() + .performClick() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @Test + fun `Given clicking enter manual address button when discovering server then show ManualServer then back goes to ServerDiscovery`() { + testNavigation { + navController.navigateToServerDiscovery() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + onNodeWithText(stringResource(commonR.string.manual_setup)) + .performScrollTo() + .assertIsDisplayed() + .performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithContentDescription(stringResource(commonR.string.get_help)).performClick() + verify { any().navigateToUri("https://www.home-assistant.io/installation/") } + + onNodeWithContentDescription(stringResource(commonR.string.navigate_up)) + .assertIsDisplayed() + .performClick() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun `Given a server discovered when clicking on it then show ConnectScreen then back goes to ServerDiscovery`() { + val instanceUrl = "http://ha.local" + testNavigation { + navController.navigateToServerDiscovery() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + onNodeWithText(stringResource(commonR.string.manual_setup)) + .performScrollTo() + .assertIsDisplayed() + + instanceChannel.trySend( + HomeAssistantInstance("Test", URL(instanceUrl), HomeAssistantVersion(2025, 9, 1)), + ) + + onNodeWithContentDescription(stringResource(commonR.string.get_help)).performClick() + verify { any().navigateToUri("https://www.home-assistant.io/installation/") } + + waitUntilAtLeastOneExists( + hasText(instanceUrl), + timeoutMillis = DELAY_BEFORE_DISPLAY_DISCOVERY.inWholeMilliseconds, + ) + + onNodeWithTag(ONE_SERVER_FOUND_MODAL_TAG).performTouchInput { + swipeUp(startY = bottom * 0.9f, endY = centerY, durationMillis = 200) + } + + waitForIdle() + + onNodeWithText(instanceUrl).assertIsDisplayed() + + onNodeWithText(stringResource(commonR.string.server_discovery_connect)) + .assertIsDisplayed() + .performClick() + + waitForIdle() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithTag(CONNECTION_SCREEN_TAG).assertIsDisplayed() + + composeTestRule.activity.onBackPressedDispatcher.onBackPressed() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun `Given a server discovered and connecting when authenticated then show NameYourDevice then back goes to ServerDiscovery not ConnectionScreen`() { + val instanceUrl = "http://ha.local" + testNavigation { + navController.navigateToServerDiscovery() + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + onNodeWithText(stringResource(commonR.string.manual_setup)) + .performScrollTo() + .assertIsDisplayed() + + instanceChannel.trySend( + HomeAssistantInstance("Test", URL(instanceUrl), HomeAssistantVersion(2025, 9, 1)), + ) + + waitUntilAtLeastOneExists( + hasText(instanceUrl), + timeoutMillis = DELAY_BEFORE_DISPLAY_DISCOVERY.inWholeMilliseconds, + ) + + onNodeWithText(instanceUrl).assertIsDisplayed() + + onNodeWithText(stringResource(commonR.string.server_discovery_connect)).performClick() + + onNodeWithTag(CONNECTION_SCREEN_TAG).assertIsDisplayed() + + assertTrue(connectionNavigationEventFlow.subscriptionCount.value == 1) + connectionNavigationEventFlow.emit( + ConnectionNavigationEvent.Authenticated(instanceUrl, "super_code", false), + ) + + waitUntilAtLeastOneExists(hasText(stringResource(commonR.string.name_your_device_title))) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + val route = navController.currentBackStackEntry?.toRoute() + assertEquals(instanceUrl, route?.url) + assertEquals("super_code", route?.authCode) + + onNodeWithContentDescription(stringResource(commonR.string.navigate_up)).performClick() + + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/sethomenetwork/navigation/SetHomeNetworkNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/sethomenetwork/navigation/SetHomeNetworkNavigationTest.kt new file mode 100644 index 00000000000..627730f9bdc --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/sethomenetwork/navigation/SetHomeNetworkNavigationTest.kt @@ -0,0 +1,38 @@ +package io.homeassistant.companion.android.onboarding.sethomenetwork.navigation + +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.navigation.NavDestination.Companion.hasRoute +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.testing.unit.stringResource +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Set Home Network screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@HiltAndroidTest +internal class SetHomeNetworkNavigationTest : BaseOnboardingNavigationTest() { + + @Test + fun `Given SetHomeNetwork when clicking next then onboarding completes`() { + testNavigation { + navController.navigateToSetHomeNetworkRoute(42) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + + onNodeWithText(stringResource(commonR.string.set_home_network_next)) + .performScrollTo() + .performClick() + assertTrue(onboardingDone) + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/welcome/navigation/WelcomeNavigationTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/welcome/navigation/WelcomeNavigationTest.kt new file mode 100644 index 00000000000..43f5f0cfc42 --- /dev/null +++ b/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/welcome/navigation/WelcomeNavigationTest.kt @@ -0,0 +1,41 @@ +package io.homeassistant.companion.android.onboarding.welcome.navigation + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.navigation.NavController +import androidx.navigation.NavDestination.Companion.hasRoute +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.homeassistant.companion.android.common.R as commonR +import io.homeassistant.companion.android.onboarding.BaseOnboardingNavigationTest +import io.homeassistant.companion.android.testing.unit.stringResource +import io.homeassistant.companion.android.util.compose.navigateToUri +import io.mockk.verify +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Navigation tests for the Welcome screen in the onboarding flow. + */ +@RunWith(RobolectricTestRunner::class) +@Config(application = HiltTestApplication::class) +@HiltAndroidTest +internal class WelcomeNavigationTest : BaseOnboardingNavigationTest() { + + @Test + fun `Given no action when starting the app then show Welcome`() { + testNavigation { + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + onNodeWithText(stringResource(commonR.string.welcome_learn_more)) + .performScrollTo() + .assertIsDisplayed() + .performClick() + verify { any().navigateToUri("https://www.home-assistant.io") } + } + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/util/compose/HAAppTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/util/compose/HAAppTest.kt index 9fa1ff32ff5..6ca4789bcc4 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/util/compose/HAAppTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/util/compose/HAAppTest.kt @@ -1,5 +1,7 @@ package io.homeassistant.companion.android.util.compose +import android.app.Activity +import android.content.Intent import android.content.pm.PackageManager import androidx.activity.compose.LocalActivity import androidx.compose.runtime.CompositionLocalProvider @@ -29,21 +31,25 @@ import io.homeassistant.companion.android.frontend.navigation.FrontendActivityRo import io.homeassistant.companion.android.frontend.navigation.FrontendRoute import io.homeassistant.companion.android.launcher.HAStartDestinationRoute import io.homeassistant.companion.android.onboarding.OnboardingRoute +import io.homeassistant.companion.android.onboarding.WearOnboardApp import io.homeassistant.companion.android.onboarding.WearOnboardingRoute import io.homeassistant.companion.android.onboarding.connection.navigation.ConnectionRoute import io.homeassistant.companion.android.onboarding.locationforsecureconnection.navigation.navigateToLocationForSecureConnection +import io.homeassistant.companion.android.onboarding.nameyourweardevice.navigation.navigateToNameYourWearDevice import io.homeassistant.companion.android.onboarding.serverdiscovery.navigation.ServerDiscoveryRoute import io.homeassistant.companion.android.onboarding.welcome.navigation.WelcomeRoute import io.homeassistant.companion.android.testing.unit.ConsoleLogRule import io.homeassistant.companion.android.testing.unit.stringResource import io.homeassistant.companion.android.util.compose.webview.HA_WEBVIEW_TAG import io.mockk.every +import io.mockk.slot import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.assertNull import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -65,6 +71,8 @@ class HAAppTest { private lateinit var navController: TestNavHostController + private lateinit var spyActivity: HiltComponentActivity + lateinit var activityNavigator: ActivityNavigator private fun testApp(startDestination: HAStartDestinationRoute?, isAutomotive: Boolean = false, testContent: suspend AndroidComposeTestRule<*, *>.() -> Unit) { @@ -74,7 +82,7 @@ class HAAppTest { navController.navigatorProvider.addNavigator(ComposeNavigator()) navController.navigatorProvider.addNavigator(activityNavigator) - val spyActivity = spyk(composeTestRule.activity) + spyActivity = spyk(composeTestRule.activity) val spyPackageManager = spyk(composeTestRule.activity.packageManager) every { spyActivity.packageManager } returns spyPackageManager every { spyPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) } returns isAutomotive @@ -100,7 +108,7 @@ class HAAppTest { @Test fun `Given default OnboardingRoute as start when starts then show Welcome`() { testApp(OnboardingRoute(hasLocationTracking = true)) { - Assertions.assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) onNodeWithText(stringResource(R.string.welcome_home_assistant_title)).assertIsDisplayed() onNodeWithText(stringResource(R.string.welcome_details)).assertIsDisplayed() onNodeWithContentDescription(stringResource(R.string.home_assistant_branding_icon_content_description)).assertIsDisplayed() @@ -112,7 +120,7 @@ class HAAppTest { @Test fun `Given OnboardingRoute with skipWelcome without urlToOnboard as start when starts then show ServerDiscovery`() { testApp(OnboardingRoute(hasLocationTracking = true, skipWelcome = true)) { - Assertions.assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) } } @@ -120,8 +128,8 @@ class HAAppTest { fun `Given OnboardingRoute with skipWelcome with urlToOnboard as start when starts then show ServerDiscovery`() { val url = "http://ha.org" testApp(OnboardingRoute(hasLocationTracking = true, skipWelcome = true, urlToOnboard = url)) { - Assertions.assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) - Assertions.assertEquals( + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertEquals( url, navController.currentBackStackEntry?.toRoute()?.url, ) @@ -131,7 +139,7 @@ class HAAppTest { @Test fun `Given FrontendRoute as start when starts then navigate to Frontend and finish current activity`() { testApp(FrontendRoute()) { - Assertions.assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) verify(exactly = 1) { activityNavigator.navigate( match { @@ -143,14 +151,14 @@ class HAAppTest { ) } // TODO remove this once we are using WebViewActivity anymore - Assertions.assertTrue(activity.isFinishing) + assertTrue(activity.isFinishing) } } @Test fun `Given WearOnboardingRoute with url to onboard as start when starts then navigate to ConnectionScreen`() { testApp(WearOnboardingRoute("wear", "http://ha")) { - Assertions.assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) onNodeWithTag(HA_WEBVIEW_TAG).assertIsDisplayed() } } @@ -158,7 +166,7 @@ class HAAppTest { @Test fun `Given WearOnboardingRoute without as start when starts then navigate to ServerDiscoveryScreen`() { testApp(WearOnboardingRoute("wear", null)) { - Assertions.assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) onNodeWithText(stringResource(R.string.searching_home_network)).assertIsDisplayed() } } @@ -171,7 +179,7 @@ class HAAppTest { onNodeWithText(stringResource(R.string.connection_security_less_secure)).performScrollTo().performClick() onNodeWithText(stringResource(R.string.location_secure_connection_next)).performScrollTo().assertIsEnabled().assertIsDisplayed().performClick() - Assertions.assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) + assertTrue(navController.currentBackStackEntry?.destination?.hasRoute() == true) } } @@ -196,5 +204,33 @@ class HAAppTest { } } - // TODO find a way to test the activity result since setResult is not exposed. + @Test + fun `Given WearOnboarding done then setResult is called with correct output`() { + val wearName = "My Wear Device" + val serverUrl = "http://ha.local" + val authCode = "test-auth-code" + + testApp(WearOnboardingRoute(wearName, serverUrl)) { + navController.navigateToNameYourWearDevice( + defaultDeviceName = wearName, + url = serverUrl, + authCode = authCode, + requiredMTLS = false, + ) + + onNodeWithText(stringResource(R.string.name_your_device_save)).performScrollTo().performClick() + + val intentSlot = slot() + verify { spyActivity.setResult(Activity.RESULT_OK, capture(intentSlot)) } + + val output = WearOnboardApp.Output.fromIntent(intentSlot.captured) + assertEquals(serverUrl, output.url) + assertEquals(wearName, output.deviceName) + assertEquals(authCode, output.authCode) + assertNull(output.tlsClientCertificateUri) + assertNull(output.tlsClientCertificatePassword) + + verify { spyActivity.finish() } + } + } } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/compose/composable/HATopBar.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/compose/composable/HATopBar.kt index 043818a6c83..71950c88391 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/compose/composable/HATopBar.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/compose/composable/HATopBar.kt @@ -56,7 +56,6 @@ fun HATopBar( }, colors = TopAppBarColors( containerColor = LocalHAColorScheme.current.colorSurfaceDefault, - // TODO validate that we use colorOnNeutralQuiet navigationIconContentColor = LocalHAColorScheme.current.colorOnNeutralQuiet, actionIconContentColor = LocalHAColorScheme.current.colorOnNeutralQuiet, // For now this color are not used we would need to decide with Design team which token to use here