From 62ffc00df863f4c8e5ce5b6f4e98c94e3ddcd97c Mon Sep 17 00:00:00 2001 From: Jonatan Rhodin Date: Sun, 24 Nov 2024 23:23:40 +0100 Subject: [PATCH] Update and add screen tests --- .../compose/screen/ConnectScreenTest.kt | 1 + .../screen/CustomListLocationsScreenTest.kt | 7 +- .../compose/screen/SettingsScreenTest.kt | 2 + .../location/SearchLocationScreenTest.kt | 105 ++++++++++ .../SelectLocationScreenTest.kt | 183 +++++++----------- 5 files changed, 182 insertions(+), 116 deletions(-) create mode 100644 android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt rename android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/{ => location}/SelectLocationScreenTest.kt (50%) diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt index 1c3342086354..487e73902532 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt @@ -474,6 +474,7 @@ class ConnectScreenTest { val inPort = 99 val inProtocol = TransportProtocol.Udp every { mockLocation.hostname } returns mockHostName + every { mockLocation.entryHostname } returns null // In every { mockTunnelEndpoint.obfuscation } returns null diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt index 444bbd2c5b07..484bb132d68f 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt @@ -163,9 +163,7 @@ class CustomListLocationsScreenTest { } // Assert - onNodeWithText(EMPTY_SEARCH_FIRST_ROW.format(mockSearchString), substring = true) - .assertExists() - onNodeWithText(EMPTY_SEARCH_SECOND_ROW, substring = true).assertExists() + onNodeWithText(EMPTY_SEARCH.format(mockSearchString)).assertExists() } @Test @@ -239,8 +237,7 @@ class CustomListLocationsScreenTest { const val ADD_LOCATIONS_TEXT = "Add locations" const val EDIT_LOCATIONS_TEXT = "Edit locations" const val SEARCH_PLACEHOLDER = "Search for..." - const val EMPTY_SEARCH_FIRST_ROW = "No result for %s." - const val EMPTY_SEARCH_SECOND_ROW = "Try a different search" + const val EMPTY_SEARCH = "No result for \"%s\", please try a different search" const val NO_LOCATIONS_FOUND_TEXT = "No locations found" } } diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt index 2509c7be8d50..e691909a40a0 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt @@ -33,6 +33,7 @@ class SettingsScreenTest { isLoggedIn = true, isSupportedVersion = true, isPlayBuild = false, + multihopEnabled = false, ) ) } @@ -56,6 +57,7 @@ class SettingsScreenTest { isLoggedIn = false, isSupportedVersion = true, isPlayBuild = false, + multihopEnabled = false, ) ) } diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt new file mode 100644 index 000000000000..5901599df9c4 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt @@ -0,0 +1,105 @@ +package net.mullvad.mullvadvpn.compose.screen.location + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import io.mockk.MockKAnnotations +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_ITEM_CUSTOM_LISTS +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.state.RelayListItem +import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState +import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@OptIn(ExperimentalTestApi::class) +class SearchLocationScreenTest { + @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @AfterEach + fun teardown() { + unmockkAll() + } + + @Test + fun testSearchInput() = + composeExtension.use { + // Arrange + val mockedSearchTermInput: (String) -> Unit = mockk(relaxed = true) + setContentWithTheme { + SearchLocationScreen( + state = + SearchLocationUiState.NoQuery(searchTerm = "", filterChips = emptyList()), + onSearchInputChanged = mockedSearchTermInput, + ) + } + val mockSearchString = "SEARCH" + + // Act + onNodeWithText("Search for...").performTextInput(mockSearchString) + + // Assert + verify { mockedSearchTermInput.invoke(mockSearchString) } + } + + @Test + fun testSearchTermNotFound() = + composeExtension.use { + // Arrange + val mockSearchString = "SEARCH" + setContentWithTheme { + SearchLocationScreen( + state = + SearchLocationUiState.Content( + searchTerm = mockSearchString, + filterChips = emptyList(), + relayListItems = + listOf(RelayListItem.LocationsEmptyText(mockSearchString)), + customLists = emptyList(), + ) + ) + } + + // Assert + onNodeWithText("No result for \"$mockSearchString\", please try a different search") + .assertExists() + } + + @Test + fun givenNoCustomListsAndSearchIsActiveShouldNotShowCustomListHeader() = + composeExtension.use { + // Arrange + val mockSearchString = "SEARCH" + setContentWithTheme { + SearchLocationScreen( + state = + SearchLocationUiState.Content( + searchTerm = mockSearchString, + filterChips = emptyList(), + relayListItems = emptyList(), + customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, + ) + ) + } + + // Assert + onNodeWithText(CUSTOM_LISTS_EMPTY_TEXT).assertDoesNotExist() + onNodeWithTag(SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG).assertDoesNotExist() + } + + companion object { + private const val CUSTOM_LISTS_EMPTY_TEXT = "To create a custom list press the \"︙\"" + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt similarity index 50% rename from android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreenTest.kt rename to android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt index 31097725db1c..a154344f2673 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt @@ -1,63 +1,74 @@ -package net.mullvad.mullvadvpn.compose.screen +package net.mullvad.mullvadvpn.compose.screen.location import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput import io.mockk.MockKAnnotations +import io.mockk.every import io.mockk.mockk +import io.mockk.unmockkAll import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_ITEM_CUSTOM_LISTS import net.mullvad.mullvadvpn.compose.setContentWithTheme import net.mullvad.mullvadvpn.compose.state.RelayListItem +import net.mullvad.mullvadvpn.compose.state.RelayListType +import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState -import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG -import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.performLongClick +import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationListViewModel +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension +import org.koin.core.context.loadKoinModules +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module @OptIn(ExperimentalTestApi::class) class SelectLocationScreenTest { @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() + private val listViewModel: SelectLocationListViewModel = mockk(relaxed = true) + @BeforeEach fun setup() { MockKAnnotations.init(this) + loadKoinModules(module { viewModel { listViewModel } }) + every { listViewModel.uiState } returns MutableStateFlow(SelectLocationListUiState.Loading) } - @Test - fun testDefaultState() = - composeExtension.use { - // Arrange - setContentWithTheme { SelectLocationScreen(state = SelectLocationUiState.Loading) } - - // Assert - onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertExists() - } + @AfterEach + fun teardown() { + unmockkAll() + } @Test fun testShowRelayListState() = composeExtension.use { // Arrange + every { listViewModel.uiState } returns + MutableStateFlow( + SelectLocationListUiState.Content( + relayListItems = + DUMMY_RELAY_COUNTRIES.map { RelayListItem.GeoLocationItem(item = it) }, + customLists = emptyList(), + ) + ) setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Content( - searchTerm = "", + SelectLocationUiState( + // searchTerm = "", filterChips = emptyList(), - relayListItems = - DUMMY_RELAY_COUNTRIES.map { - RelayListItem.GeoLocationItem(item = it) - }, - customLists = emptyList(), + multihopEnabled = false, + relayListType = RelayListType.EXIT, ) ) } @@ -71,98 +82,30 @@ class SelectLocationScreenTest { onNodeWithText("Relay host 2").assertDoesNotExist() } - @Test - fun testSearchInput() = - composeExtension.use { - // Arrange - val mockedSearchTermInput: (String) -> Unit = mockk(relaxed = true) - setContentWithTheme { - SelectLocationScreen( - state = - SelectLocationUiState.Content( - searchTerm = "", - filterChips = emptyList(), - relayListItems = emptyList(), - customLists = emptyList(), - ), - onSearchTermInput = mockedSearchTermInput, - ) - } - val mockSearchString = "SEARCH" - - // Act - onNodeWithText("Search for...").performTextInput(mockSearchString) - - // Assert - verify { mockedSearchTermInput.invoke(mockSearchString) } - } - - @Test - fun testSearchTermNotFound() = - composeExtension.use { - // Arrange - val mockedSearchTermInput: (String) -> Unit = mockk(relaxed = true) - val mockSearchString = "SEARCH" - setContentWithTheme { - SelectLocationScreen( - state = - SelectLocationUiState.Content( - searchTerm = mockSearchString, - filterChips = emptyList(), - relayListItems = - listOf(RelayListItem.LocationsEmptyText(mockSearchString)), - customLists = emptyList(), - ), - onSearchTermInput = mockedSearchTermInput, - ) - } - - // Assert - onNodeWithText("No result for $mockSearchString.", substring = true).assertExists() - onNodeWithText("Try a different search", substring = true).assertExists() - } - @Test fun customListFooterShouldShowEmptyTextWhenNoCustomList() = composeExtension.use { // Arrange - val mockSearchString = "" - setContentWithTheme { - SelectLocationScreen( - state = - SelectLocationUiState.Content( - searchTerm = mockSearchString, - filterChips = emptyList(), - relayListItems = listOf(RelayListItem.CustomListFooter(false)), - customLists = emptyList(), - ) + every { listViewModel.uiState } returns + MutableStateFlow( + SelectLocationListUiState.Content( + relayListItems = listOf(RelayListItem.CustomListFooter(false)), + customLists = emptyList(), + ) ) - } - - // Assert - onNodeWithText(CUSTOM_LISTS_EMPTY_TEXT).assertExists() - } - - @Test - fun givenNoCustomListsAndSearchIsActiveShouldNotShowCustomListHeader() = - composeExtension.use { - // Arrange - val mockSearchString = "SEARCH" setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Content( - searchTerm = mockSearchString, + SelectLocationUiState( filterChips = emptyList(), - relayListItems = emptyList(), - customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, + multihopEnabled = false, + relayListType = RelayListType.EXIT, ) ) } // Assert - onNodeWithText(CUSTOM_LISTS_EMPTY_TEXT).assertDoesNotExist() - onNodeWithTag(SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG).assertDoesNotExist() + onNodeWithText(CUSTOM_LISTS_EMPTY_TEXT).assertExists() } @Test @@ -170,15 +113,21 @@ class SelectLocationScreenTest { composeExtension.use { // Arrange val customList = DUMMY_RELAY_ITEM_CUSTOM_LISTS[0] + every { listViewModel.uiState } returns + MutableStateFlow( + SelectLocationListUiState.Content( + relayListItems = listOf(RelayListItem.CustomListItem(customList)), + customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, + ) + ) val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true) setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Content( - searchTerm = "", + SelectLocationUiState( filterChips = emptyList(), - relayListItems = listOf(RelayListItem.CustomListItem(customList)), - customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, + multihopEnabled = false, + relayListType = RelayListType.EXIT, ), onSelectRelay = mockedOnSelectRelay, ) @@ -196,16 +145,22 @@ class SelectLocationScreenTest { composeExtension.use { // Arrange val customList = DUMMY_RELAY_ITEM_CUSTOM_LISTS[0] + every { listViewModel.uiState } returns + MutableStateFlow( + SelectLocationListUiState.Content( + relayListItems = listOf(RelayListItem.CustomListItem(item = customList)), + customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, + ) + ) val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true) setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Content( - searchTerm = "", + SelectLocationUiState( + // searchTerm = "", filterChips = emptyList(), - relayListItems = - listOf(RelayListItem.CustomListItem(item = customList)), - customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, + multihopEnabled = false, + relayListType = RelayListType.EXIT, ), onSelectRelay = mockedOnSelectRelay, ) @@ -223,15 +178,21 @@ class SelectLocationScreenTest { composeExtension.use { // Arrange val relayItem = DUMMY_RELAY_COUNTRIES[0] + every { listViewModel.uiState } returns + MutableStateFlow( + SelectLocationListUiState.Content( + relayListItems = listOf(RelayListItem.GeoLocationItem(relayItem)), + customLists = emptyList(), + ) + ) val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true) setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Content( - searchTerm = "", + SelectLocationUiState( filterChips = emptyList(), - relayListItems = listOf(RelayListItem.GeoLocationItem(relayItem)), - customLists = emptyList(), + multihopEnabled = false, + relayListType = RelayListType.EXIT, ), onSelectRelay = mockedOnSelectRelay, )