From 0a3a549efe051f2a845b4408281e5cf3438f16f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20G=C3=B6ransson?= Date: Thu, 25 Jul 2024 14:35:08 +0200 Subject: [PATCH] Adapt tests to flat list uiState --- .../screen/CustomListLocationsScreenTest.kt | 25 +-- .../screen/SelectLocationScreenTest.kt | 125 ++++---------- .../usecase/CustomListActionUseCaseTest.kt | 3 +- .../CustomListLocationsViewModelTest.kt | 86 +++++++--- .../viewmodel/SelectLocationViewModelTest.kt | 157 ++++++++++++------ 5 files changed, 215 insertions(+), 181 deletions(-) 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 4f4db0a529b6..1a8d35a5a907 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 @@ -12,6 +12,7 @@ import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES import net.mullvad.mullvadvpn.compose.setContentWithTheme import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState +import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR import net.mullvad.mullvadvpn.compose.test.SAVE_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.lib.model.RelayItem @@ -80,8 +81,14 @@ class CustomListLocationsScreenTest { CustomListLocationsScreen( state = CustomListLocationsUiState.Content.Data( - availableLocations = DUMMY_RELAY_COUNTRIES, - selectedLocations = emptySet(), + locations = + listOf( + RelayLocationListItem(DUMMY_RELAY_COUNTRIES[0], checked = true), + RelayLocationListItem( + DUMMY_RELAY_COUNTRIES[1], + checked = false + ), + ), searchTerm = "" ), ) @@ -89,11 +96,7 @@ class CustomListLocationsScreenTest { // Assert onNodeWithText("Relay Country 1").assertExists() - onNodeWithText("Relay City 1").assertDoesNotExist() - onNodeWithText("Relay host 1").assertDoesNotExist() onNodeWithText("Relay Country 2").assertExists() - onNodeWithText("Relay City 2").assertDoesNotExist() - onNodeWithText("Relay host 2").assertDoesNotExist() } @Test @@ -107,8 +110,8 @@ class CustomListLocationsScreenTest { state = CustomListLocationsUiState.Content.Data( newList = false, - availableLocations = DUMMY_RELAY_COUNTRIES, - selectedLocations = setOf(selectedCountry) + locations = + listOf(RelayLocationListItem(selectedCountry, checked = true)) ), onRelaySelectionClick = mockedOnRelaySelectionClicked ) @@ -131,7 +134,7 @@ class CustomListLocationsScreenTest { state = CustomListLocationsUiState.Content.Data( newList = false, - availableLocations = DUMMY_RELAY_COUNTRIES, + locations = emptyList(), ), onSearchTermInput = mockedSearchTermInput ) @@ -197,7 +200,7 @@ class CustomListLocationsScreenTest { state = CustomListLocationsUiState.Content.Data( newList = false, - availableLocations = DUMMY_RELAY_COUNTRIES, + locations = emptyList(), saveEnabled = true, ), onSaveClick = mockOnSaveClick @@ -221,7 +224,7 @@ class CustomListLocationsScreenTest { state = CustomListLocationsUiState.Content.Data( newList = false, - availableLocations = DUMMY_RELAY_COUNTRIES, + locations = emptyList(), saveEnabled = false, ), onSaveClick = mockOnSaveClick 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/SelectLocationScreenTest.kt index 4fcee479d612..4f3bac57e266 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/SelectLocationScreenTest.kt @@ -12,6 +12,7 @@ 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.SelectLocationUiState import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG @@ -54,13 +55,13 @@ class SelectLocationScreenTest { SelectLocationScreen( state = SelectLocationUiState.Content( + searchTerm = "", + filterChips = emptyList(), + relayListItems = + DUMMY_RELAY_COUNTRIES.map { + RelayListItem.GeoLocationItem(item = it) + }, customLists = emptyList(), - filteredCustomLists = emptyList(), - countries = DUMMY_RELAY_COUNTRIES, - selectedItem = null, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = "" ), ) } @@ -74,45 +75,6 @@ class SelectLocationScreenTest { onNodeWithText("Relay host 2").assertDoesNotExist() } - @Test - fun testShowRelayListStateSelected() = - composeExtension.use { - val updatedDummyList = - DUMMY_RELAY_COUNTRIES.let { - val cities = it[0].cities.toMutableList() - val city = cities.removeAt(0) - cities.add(0, city.copy(expanded = true)) - - val mutableRelayList = it.toMutableList() - mutableRelayList[0] = it[0].copy(expanded = true, cities = cities.toList()) - mutableRelayList - } - - // Arrange - setContentWithTheme { - SelectLocationScreen( - state = - SelectLocationUiState.Content( - customLists = emptyList(), - filteredCustomLists = emptyList(), - countries = updatedDummyList, - selectedItem = updatedDummyList[0].cities[0].relays[0].id, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = "" - ), - ) - } - - // Assert - onNodeWithText("Relay Country 1").assertExists() - onNodeWithText("Relay City 1").assertExists() - onNodeWithText("Relay host 1").assertExists() - onNodeWithText("Relay Country 2").assertExists() - onNodeWithText("Relay City 2").assertDoesNotExist() - onNodeWithText("Relay host 2").assertDoesNotExist() - } - @Test fun testSearchInput() = composeExtension.use { @@ -122,13 +84,10 @@ class SelectLocationScreenTest { SelectLocationScreen( state = SelectLocationUiState.Content( - customLists = emptyList(), - filteredCustomLists = emptyList(), - countries = emptyList(), - selectedItem = null, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = "" + searchTerm = "", + filterChips = emptyList(), + relayListItems = emptyList(), + customLists = emptyList() ), onSearchTermInput = mockedSearchTermInput ) @@ -152,13 +111,11 @@ class SelectLocationScreenTest { SelectLocationScreen( state = SelectLocationUiState.Content( + searchTerm = mockSearchString, + filterChips = emptyList(), + relayListItems = + listOf(RelayListItem.LocationsEmptyText(mockSearchString)), customLists = emptyList(), - filteredCustomLists = emptyList(), - countries = emptyList(), - selectedItem = null, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = mockSearchString ), onSearchTermInput = mockedSearchTermInput ) @@ -170,7 +127,7 @@ class SelectLocationScreenTest { } @Test - fun givenNoCustomListsAndSearchIsTermIsEmptyShouldShowCustomListsEmptyText() = + fun customListFooterShouldShowEmptyTextWhenNoCustomList() = composeExtension.use { // Arrange val mockSearchString = "" @@ -178,13 +135,10 @@ class SelectLocationScreenTest { SelectLocationScreen( state = SelectLocationUiState.Content( + searchTerm = mockSearchString, + filterChips = emptyList(), + relayListItems = listOf(RelayListItem.CustomListFooter(false)), customLists = emptyList(), - filteredCustomLists = emptyList(), - countries = emptyList(), - selectedItem = null, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = mockSearchString ), ) } @@ -202,13 +156,10 @@ class SelectLocationScreenTest { SelectLocationScreen( state = SelectLocationUiState.Content( + searchTerm = mockSearchString, + filterChips = emptyList(), + relayListItems = emptyList(), customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, - filteredCustomLists = emptyList(), - countries = emptyList(), - selectedItem = null, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = mockSearchString ), ) } @@ -228,13 +179,10 @@ class SelectLocationScreenTest { SelectLocationScreen( state = SelectLocationUiState.Content( - customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, - filteredCustomLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, - countries = emptyList(), - selectedItem = null, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = "" + searchTerm = "", + filterChips = emptyList(), + relayListItems = listOf(RelayListItem.CustomListItem(customList)), + customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS ), onSelectRelay = mockedOnSelectRelay ) @@ -257,13 +205,11 @@ class SelectLocationScreenTest { SelectLocationScreen( state = SelectLocationUiState.Content( - customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, - filteredCustomLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS, - countries = emptyList(), - selectedItem = null, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = "" + searchTerm = "", + filterChips = emptyList(), + relayListItems = + listOf(RelayListItem.CustomListItem(item = customList)), + customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS ), onSelectRelay = mockedOnSelectRelay ) @@ -286,13 +232,10 @@ class SelectLocationScreenTest { SelectLocationScreen( state = SelectLocationUiState.Content( + searchTerm = "", + filterChips = emptyList(), + relayListItems = listOf(RelayListItem.GeoLocationItem(relayItem)), customLists = emptyList(), - filteredCustomLists = emptyList(), - countries = DUMMY_RELAY_COUNTRIES, - selectedItem = null, - selectedOwnership = null, - selectedProvidersCount = 0, - searchTerm = "" ), onSelectRelay = mockedOnSelectRelay ) diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt index d98292ccd9e2..d72d183202a3 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt @@ -71,7 +71,6 @@ class CustomListActionUseCaseTest { RelayItem.Location.Country( id = locationId, name = locationName, - expanded = false, cities = emptyList() ) ) @@ -151,7 +150,7 @@ class CustomListActionUseCaseTest { val action = CustomListAction.Delete(id = customListId) val expectedResult = Deleted(undo = action.not(name = name, locations = listOf(location))).right() - every { mockLocation.countryCode } returns location.countryCode + every { mockLocation.code } returns location.code coEvery { mockCustomListsRepository.deleteCustomList(id = customListId) } returns Unit.right() coEvery { mockCustomListsRepository.getCustomListById(customListId) } returns diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt index 4879031ce72e..d2eaedd8c279 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt @@ -13,7 +13,9 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.LocationsChanged import net.mullvad.mullvadvpn.compose.screen.CustomListLocationsNavArgs import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState +import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.lib.common.test.assertLists import net.mullvad.mullvadvpn.lib.model.CustomList import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName @@ -23,6 +25,7 @@ import net.mullvad.mullvadvpn.lib.model.Provider import net.mullvad.mullvadvpn.lib.model.ProviderId import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.relaylist.descendants +import net.mullvad.mullvadvpn.relaylist.withDescendants import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import net.mullvad.mullvadvpn.usecase.customlists.CustomListRelayItemsUseCase @@ -66,15 +69,20 @@ class CustomListLocationsViewModelTest { fun `when selected locations is not null and relay countries is not empty should return ui state content`() = runTest { // Arrange - val expectedList = DUMMY_COUNTRIES + val expectedList = + DUMMY_COUNTRIES.map { + RelayLocationListItem( + item = it, + depth = it.toDepth(), + checked = false, + expanded = false + ) + } val customListId = CustomListId("id") val expectedState = - CustomListLocationsUiState.Content.Data( - newList = true, - availableLocations = expectedList - ) + CustomListLocationsUiState.Content.Data(newList = true, locations = expectedList) val viewModel = createViewModel(customListId, true) - relayListFlow.value = expectedList + relayListFlow.value = DUMMY_COUNTRIES // Act, Assert viewModel.uiState.test { assertEquals(expectedState, awaitItem()) } @@ -85,8 +93,8 @@ class CustomListLocationsViewModelTest { // Arrange val expectedList = DUMMY_COUNTRIES val customListId = CustomListId("id") - val expectedSelection = - (DUMMY_COUNTRIES + DUMMY_COUNTRIES.flatMap { it.descendants() }).toSet() + val expectedSelection = (DUMMY_COUNTRIES.take(1).withDescendants()).map { it.id } + val viewModel = createViewModel(customListId, true) relayListFlow.value = expectedList @@ -95,12 +103,19 @@ class CustomListLocationsViewModelTest { // Check no selected val firstState = awaitItem() assertIs(firstState) - assertEquals(emptySet(), firstState.selectedLocations) + assertEquals(emptyList(), firstState.selectedLocations()) + // Expand country + viewModel.onExpand(DUMMY_COUNTRIES[0], true) + awaitItem() + // Expand city + viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], expand = true) + awaitItem() + // Select country viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0], true) // Check all items selected val secondState = awaitItem() assertIs(secondState) - assertEquals(expectedSelection, secondState.selectedLocations) + assertLists(expectedSelection, secondState.selectedLocations()) } } @@ -108,25 +123,29 @@ class CustomListLocationsViewModelTest { fun `when deselecting child should deselect parent`() = runTest { // Arrange val expectedList = DUMMY_COUNTRIES - val initialSelection = - (DUMMY_COUNTRIES + DUMMY_COUNTRIES.flatMap { it.descendants() }).toSet() + val initialSelection = DUMMY_COUNTRIES.withDescendants() + val initialSelectionIds = initialSelection.map { it.id } val customListId = CustomListId("id") - val expectedSelection = emptySet() + val expectedSelection = emptyList() relayListFlow.value = expectedList - selectedLocationsFlow.value = initialSelection.toList() + selectedLocationsFlow.value = initialSelection val viewModel = createViewModel(customListId, true) // Act, Assert viewModel.uiState.test { + // Expand country + viewModel.onExpand(DUMMY_COUNTRIES[0], true) + // Expand city + viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], true) // Check initial selected val firstState = awaitItem() assertIs(firstState) - assertEquals(initialSelection, firstState.selectedLocations) + assertEquals(initialSelectionIds, firstState.selectedLocations()) viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0].cities[0].relays[0], false) // Check all items selected val secondState = awaitItem() assertIs(secondState) - assertEquals(expectedSelection, secondState.selectedLocations) + assertEquals(expectedSelection, secondState.selectedLocations()) } } @@ -136,23 +155,28 @@ class CustomListLocationsViewModelTest { val expectedList = DUMMY_COUNTRIES val initialSelection = (DUMMY_COUNTRIES + DUMMY_COUNTRIES.flatMap { it.descendants() }).toSet() + val initialSelectionIds = initialSelection.map { it.id } val customListId = CustomListId("id") - val expectedSelection = emptySet() + val expectedSelection = emptyList() relayListFlow.value = expectedList selectedLocationsFlow.value = initialSelection.toList() val viewModel = createViewModel(customListId, true) // Act, Assert viewModel.uiState.test { + // Expand country + viewModel.onExpand(DUMMY_COUNTRIES[0], true) + // Expand city + viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], true) // Check initial selected val firstState = awaitItem() assertIs(firstState) - assertEquals(initialSelection, firstState.selectedLocations) + assertEquals(initialSelectionIds, firstState.selectedLocations()) viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0], false) // Check all items selected val secondState = awaitItem() assertIs(secondState) - assertEquals(expectedSelection, secondState.selectedLocations) + assertEquals(expectedSelection, secondState.selectedLocations()) } } @@ -161,21 +185,27 @@ class CustomListLocationsViewModelTest { // Arrange val expectedList = DUMMY_COUNTRIES val customListId = CustomListId("id") - val expectedSelection = DUMMY_COUNTRIES[0].cities[0].relays.toSet() + val expectedSelection = DUMMY_COUNTRIES[0].cities[0].relays.map { it.id } val viewModel = createViewModel(customListId, true) relayListFlow.value = expectedList // Act, Assert viewModel.uiState.test { + awaitItem() // Initial item + // Expand country + viewModel.onExpand(DUMMY_COUNTRIES[0], true) + awaitItem() + // Expand city + viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], true) // Check no selected val firstState = awaitItem() assertIs(firstState) - assertEquals(emptySet(), firstState.selectedLocations) + assertEquals(emptyList(), firstState.selectedLocations()) viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0].cities[0].relays[0], true) // Check all items selected val secondState = awaitItem() assertIs(secondState) - assertEquals(expectedSelection, secondState.selectedLocations) + assertEquals(expectedSelection, secondState.selectedLocations()) } } @@ -235,18 +265,26 @@ class CustomListLocationsViewModelTest { ) } + private fun CustomListLocationsUiState.Content.Data.selectedLocations() = + this.locations.filter { it.checked }.map { it.item.id } + + private fun RelayItem.Location.toDepth() = + when (this) { + is RelayItem.Location.Country -> 0 + is RelayItem.Location.City -> 1 + is RelayItem.Location.Relay -> 2 + } + companion object { private val DUMMY_COUNTRIES = listOf( RelayItem.Location.Country( name = "Sweden", id = GeoLocationId.Country("SE"), - expanded = false, cities = listOf( RelayItem.Location.City( name = "Gothenburg", - expanded = false, id = GeoLocationId.City(GeoLocationId.Country("SE"), "GBG"), relays = listOf( diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt index 28f52ba26168..d15e460e5d78 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt @@ -11,15 +11,18 @@ import io.mockk.mockkStatic import io.mockk.unmockkAll import kotlin.test.assertEquals import kotlin.test.assertIs +import kotlin.test.assertTrue import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.LocationsChanged +import net.mullvad.mullvadvpn.compose.state.RelayListItem import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.common.test.assertLists import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.CustomList import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName import net.mullvad.mullvadvpn.lib.model.GeoLocationId @@ -29,13 +32,14 @@ import net.mullvad.mullvadvpn.lib.model.Providers import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.lib.model.RelayItemId import net.mullvad.mullvadvpn.relaylist.descendants -import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm +import net.mullvad.mullvadvpn.repository.CustomListsRepository import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase +import net.mullvad.mullvadvpn.usecase.customlists.FilterCustomListsRelayItemUseCase import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -47,9 +51,11 @@ class SelectLocationViewModelTest { private val mockRelayListFilterRepository: RelayListFilterRepository = mockk() private val mockAvailableProvidersUseCase: AvailableProvidersUseCase = mockk(relaxed = true) private val mockCustomListActionUseCase: CustomListActionUseCase = mockk(relaxed = true) - private val mockCustomListsRelayItemUseCase: CustomListsRelayItemUseCase = mockk() + private val mockFilteredCustomListRelayItemsUseCase: FilterCustomListsRelayItemUseCase = mockk() private val mockFilteredRelayListUseCase: FilteredRelayListUseCase = mockk() private val mockRelayListRepository: RelayListRepository = mockk() + private val mockCustomListsRepository: CustomListsRepository = mockk() + private val mockCustomListsRelayItemUseCase: CustomListsRelayItemUseCase = mockk() private lateinit var viewModel: SelectLocationViewModel @@ -58,7 +64,9 @@ class SelectLocationViewModelTest { private val selectedProviders = MutableStateFlow>(Constraint.Any) private val selectedRelayItemFlow = MutableStateFlow>(Constraint.Any) private val filteredRelayList = MutableStateFlow>(emptyList()) - private val customRelayListItems = MutableStateFlow>(emptyList()) + private val filteredCustomRelayListItems = + MutableStateFlow>(emptyList()) + private val customListsRelayItem = MutableStateFlow>(emptyList()) @BeforeEach fun setup() { @@ -68,7 +76,8 @@ class SelectLocationViewModelTest { every { mockAvailableProvidersUseCase() } returns allProviders every { mockRelayListRepository.selectedLocation } returns selectedRelayItemFlow every { mockFilteredRelayListUseCase() } returns filteredRelayList - every { mockCustomListsRelayItemUseCase() } returns customRelayListItems + every { mockFilteredCustomListRelayItemsUseCase() } returns filteredCustomRelayListItems + every { mockCustomListsRelayItemUseCase() } returns customListsRelayItem mockkStatic(RELAY_LIST_EXTENSIONS) mockkStatic(RELAY_ITEM_EXTENSIONS) @@ -77,10 +86,12 @@ class SelectLocationViewModelTest { SelectLocationViewModel( relayListFilterRepository = mockRelayListFilterRepository, availableProvidersUseCase = mockAvailableProvidersUseCase, - customListsRelayItemUseCase = mockCustomListsRelayItemUseCase, + filteredCustomListRelayItemsUseCase = mockFilteredCustomListRelayItemsUseCase, customListActionUseCase = mockCustomListActionUseCase, filteredRelayListUseCase = mockFilteredRelayListUseCase, - relayListRepository = mockRelayListRepository + relayListRepository = mockRelayListRepository, + customListsRepository = mockCustomListsRepository, + customListsRelayItemUseCase = mockCustomListsRelayItemUseCase, ) } @@ -96,41 +107,50 @@ class SelectLocationViewModelTest { } @Test - fun `given relayListWithSelection emits update uiState should contain new update`() = runTest { + fun `given filteredRelayList emits update uiState should contain new update`() = runTest { // Arrange - val mockCountries = listOf(mockk(), mockk()) - val selectedItem: RelayItemId = mockk() - every { mockCountries.filterOnSearchTerm(any(), selectedItem) } returns mockCountries - filteredRelayList.value = mockCountries - selectedRelayItemFlow.value = Constraint.Only(selectedItem) + filteredRelayList.value = testCountries + val selectedId = testCountries.first().id + selectedRelayItemFlow.value = Constraint.Only(selectedId) // Act, Assert viewModel.uiState.test { val actualState = awaitItem() assertIs(actualState) - assertLists(mockCountries, actualState.countries) - assertEquals(selectedItem, actualState.selectedItem) + assertLists( + testCountries.map { it.id }, + actualState.relayListItems.mapNotNull { it.relayItemId() } + ) + assertTrue( + actualState.relayListItems + .filterIsInstance() + .first { it.relayItemId() == selectedId } + .isSelected + ) } } @Test - fun `given relayListWithSelection emits update with no selections selectedItem should be null`() = - runTest { - // Arrange - val mockCountries = listOf(mockk(), mockk()) - val selectedItem: RelayItemId? = null - every { mockCountries.filterOnSearchTerm(any(), selectedItem) } returns mockCountries - filteredRelayList.value = mockCountries - selectedRelayItemFlow.value = Constraint.Any - - // Act, Assert - viewModel.uiState.test { - val actualState = awaitItem() - assertIs(actualState) - assertLists(mockCountries, actualState.countries) - assertEquals(selectedItem, actualState.selectedItem) - } + fun `given relay is selected all relay items should not be selected`() = runTest { + // Arrange + filteredRelayList.value = testCountries + selectedRelayItemFlow.value = Constraint.Any + + // Act, Assert + viewModel.uiState.test { + val actualState = awaitItem() + assertIs(actualState) + assertLists( + testCountries.map { it.id }, + actualState.relayListItems.mapNotNull { it.relayItemId() } + ) + assertTrue( + actualState.relayListItems.filterIsInstance().all { + !it.isSelected + } + ) } + } @Test fun `on selectRelay call uiSideEffect should emit CloseScreen and connect`() = runTest { @@ -153,15 +173,8 @@ class SelectLocationViewModelTest { @Test fun `on onSearchTermInput call uiState should emit with filtered countries`() = runTest { // Arrange - val mockCustomList = listOf(mockk(relaxed = true)) - val mockCountries = listOf(mockk(), mockk()) - val selectedItem: RelayItemId? = null - val mockRelayList: List = mockk(relaxed = true) - val mockSearchString = "SEARCH" - every { mockRelayList.filterOnSearchTerm(mockSearchString, selectedItem) } returns - mockCountries - every { mockCustomList.filterOnSearchTerm(mockSearchString) } returns mockCustomList - filteredRelayList.value = mockRelayList + val mockSearchString = "got" + filteredRelayList.value = testCountries selectedRelayItemFlow.value = Constraint.Any // Act, Assert @@ -172,25 +185,26 @@ class SelectLocationViewModelTest { // Update search string viewModel.onSearchTermInput(mockSearchString) - // Assert + // We get some unnecessary emissions for now + awaitItem() + awaitItem() + awaitItem() + val actualState = awaitItem() assertIs(actualState) - assertLists(mockCountries, actualState.countries) - assertEquals(selectedItem, actualState.selectedItem) + assertTrue( + actualState.relayListItems.filterIsInstance().any { + it.item is RelayItem.Location.City && it.item.name == "Gothenburg" + } + ) } } @Test fun `when onSearchTermInput returns empty result uiState should return empty list`() = runTest { // Arrange - val mockCustomList = listOf(mockk(relaxed = true)) - val mockCountries = emptyList() - val selectedItem: RelayItemId? = null - val mockRelayList: List = mockk(relaxed = true) + filteredRelayList.value = testCountries val mockSearchString = "SEARCH" - every { mockRelayList.filterOnSearchTerm(mockSearchString, selectedItem) } returns - mockCountries - every { mockCustomList.filterOnSearchTerm(mockSearchString) } returns mockCustomList // Act, Assert viewModel.uiState.test { @@ -200,10 +214,17 @@ class SelectLocationViewModelTest { // Update search string viewModel.onSearchTermInput(mockSearchString) + // We get some unnecessary emissions for now + awaitItem() + awaitItem() + // Assert val actualState = awaitItem() assertIs(actualState) - assertEquals(mockSearchString, actualState.searchTerm) + assertEquals( + listOf(RelayListItem.LocationsEmptyText(mockSearchString)), + actualState.relayListItems + ) } } @@ -259,10 +280,13 @@ class SelectLocationViewModelTest { } val customList = RelayItem.CustomList( - id = CustomListId("1"), - customListName = CustomListName.fromString("custom"), + customList = + CustomList( + id = CustomListId("1"), + name = CustomListName.fromString("custom"), + locations = emptyList() + ), locations = emptyList(), - expanded = false ) coEvery { mockCustomListActionUseCase(any()) } returns expectedResult.right() @@ -276,6 +300,17 @@ class SelectLocationViewModelTest { } } + fun RelayListItem.relayItemId() = + when (this) { + is RelayListItem.CustomListFooter -> null + RelayListItem.CustomListHeader -> null + RelayListItem.LocationHeader -> null + is RelayListItem.LocationsEmptyText -> null + is RelayListItem.CustomListEntryItem -> item.id + is RelayListItem.CustomListItem -> item.id + is RelayListItem.GeoLocationItem -> item.id + } + companion object { private const val RELAY_LIST_EXTENSIONS = "net.mullvad.mullvadvpn.relaylist.RelayListExtensionsKt" @@ -283,5 +318,21 @@ class SelectLocationViewModelTest { "net.mullvad.mullvadvpn.relaylist.RelayItemExtensionsKt" private const val CUSTOM_LIST_EXTENSIONS = "net.mullvad.mullvadvpn.relaylist.CustomListExtensionsKt" + + private val testCountries = + listOf( + RelayItem.Location.Country( + id = GeoLocationId.Country("se"), + "Sweden", + listOf( + RelayItem.Location.City( + id = GeoLocationId.City(GeoLocationId.Country("se"), "got"), + "Gothenburg", + emptyList() + ) + ) + ), + RelayItem.Location.Country(id = GeoLocationId.Country("no"), "Norway", emptyList()) + ) } }