diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-03-13 13:54:12 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-03-14 14:54:25 +0100 |
| commit | 9cb1e26a0ad83c363592674eefbaa462de24cd21 (patch) | |
| tree | 868117761094f4bcdd06bccf0feed7c042bf825e /android | |
| parent | 9e138799b96fea7cb38f045d2667053f5e11b1d9 (diff) | |
| download | mullvadvpn-9cb1e26a0ad83c363592674eefbaa462de24cd21.tar.xz mullvadvpn-9cb1e26a0ad83c363592674eefbaa462de24cd21.zip | |
Add screen tests for custom lists
Diffstat (limited to 'android')
10 files changed, 1137 insertions, 64 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/Actions.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/Actions.kt new file mode 100644 index 0000000000..ebd570252a --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/Actions.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.invokeGlobalAssertions +import androidx.compose.ui.test.longClick +import androidx.compose.ui.test.performTouchInput + +fun SemanticsNodeInteraction.performLongClick(): SemanticsNodeInteraction { + @OptIn(ExperimentalTestApi::class) return this.invokeGlobalAssertions().performLongClickImpl() +} + +private fun SemanticsNodeInteraction.performLongClickImpl(): SemanticsNodeInteraction { + return performTouchInput { longClick() } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt new file mode 100644 index 0000000000..5a20438c23 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt @@ -0,0 +1,51 @@ +package net.mullvad.mullvadvpn.compose.data + +import net.mullvad.mullvadvpn.model.Constraint +import net.mullvad.mullvadvpn.model.PortRange +import net.mullvad.mullvadvpn.model.RelayEndpointData +import net.mullvad.mullvadvpn.model.RelayList +import net.mullvad.mullvadvpn.model.RelayListCity +import net.mullvad.mullvadvpn.model.RelayListCountry +import net.mullvad.mullvadvpn.model.WireguardEndpointData +import net.mullvad.mullvadvpn.model.WireguardRelayEndpointData +import net.mullvad.mullvadvpn.relaylist.RelayItem +import net.mullvad.mullvadvpn.relaylist.toRelayCountries + +private val DUMMY_RELAY_1 = + net.mullvad.mullvadvpn.model.Relay( + hostname = "Relay host 1", + active = true, + endpointData = RelayEndpointData.Wireguard(WireguardRelayEndpointData), + owned = true, + provider = "PROVIDER" + ) +private val DUMMY_RELAY_2 = + net.mullvad.mullvadvpn.model.Relay( + hostname = "Relay host 2", + active = true, + endpointData = RelayEndpointData.Wireguard(WireguardRelayEndpointData), + owned = true, + provider = "PROVIDER" + ) +private val DUMMY_RELAY_CITY_1 = RelayListCity("Relay City 1", "RCi1", arrayListOf(DUMMY_RELAY_1)) +private val DUMMY_RELAY_CITY_2 = RelayListCity("Relay City 2", "RCi2", arrayListOf(DUMMY_RELAY_2)) +private val DUMMY_RELAY_COUNTRY_1 = + RelayListCountry("Relay Country 1", "RCo1", arrayListOf(DUMMY_RELAY_CITY_1)) +private val DUMMY_RELAY_COUNTRY_2 = + RelayListCountry("Relay Country 2", "RCo2", arrayListOf(DUMMY_RELAY_CITY_2)) + +private val DUMMY_WIREGUARD_PORT_RANGES = ArrayList<PortRange>() +private val DUMMY_WIREGUARD_ENDPOINT_DATA = WireguardEndpointData(DUMMY_WIREGUARD_PORT_RANGES) + +val DUMMY_RELAY_COUNTRIES = + RelayList( + arrayListOf(DUMMY_RELAY_COUNTRY_1, DUMMY_RELAY_COUNTRY_2), + DUMMY_WIREGUARD_ENDPOINT_DATA, + ) + .toRelayCountries(ownership = Constraint.Any(), providers = Constraint.Any()) + +val DUMMY_CUSTOM_LISTS = + listOf( + RelayItem.CustomList("First list", false, "1", locations = DUMMY_RELAY_COUNTRIES), + RelayItem.CustomList("Empty list", expanded = false, "2", locations = emptyList()) + ) diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialogTest.kt new file mode 100644 index 0000000000..baeb5902d7 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialogTest.kt @@ -0,0 +1,144 @@ +package net.mullvad.mullvadvpn.compose.dialog + +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.mockk +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState +import net.mullvad.mullvadvpn.compose.test.CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG +import net.mullvad.mullvadvpn.model.CustomListsError +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class CreateCustomListDialogTest { + @OptIn(ExperimentalTestApi::class) + @JvmField + @RegisterExtension + val composeExtension = createEdgeToEdgeComposeExtension() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun givenNoErrorShouldShowNoErrorMessage() = + composeExtension.use { + // Arrange + val state = CreateCustomListUiState(error = null) + setContentWithTheme { CreateCustomListDialog(state = state) } + + // Assert + onNodeWithText(NAME_EXIST_ERROR_TEXT).assertDoesNotExist() + onNodeWithText(OTHER_ERROR_TEXT).assertDoesNotExist() + } + + @Test + fun givenCustomListExistsShouldShowCustomListExitsErrorText() = + composeExtension.use { + // Arrange + val state = CreateCustomListUiState(error = CustomListsError.CustomListExists) + setContentWithTheme { CreateCustomListDialog(state = state) } + + // Assert + onNodeWithText(NAME_EXIST_ERROR_TEXT).assertExists() + onNodeWithText(OTHER_ERROR_TEXT).assertDoesNotExist() + } + + @Test + fun givenOtherCustomListErrorShouldShowAnErrorOccurredErrorText() = + composeExtension.use { + // Arrange + val state = CreateCustomListUiState(error = CustomListsError.OtherError) + setContentWithTheme { CreateCustomListDialog(state = state) } + + // Assert + onNodeWithText(NAME_EXIST_ERROR_TEXT).assertDoesNotExist() + onNodeWithText(OTHER_ERROR_TEXT).assertExists() + } + + @Test + fun whenCancelIsClickedShouldDismissDialog() = + composeExtension.use { + // Arrange + val mockedOnDismiss: () -> Unit = mockk(relaxed = true) + val state = CreateCustomListUiState() + setContentWithTheme { + CreateCustomListDialog(state = state, onDismiss = mockedOnDismiss) + } + + // Act + onNodeWithText(CANCEL_BUTTON_TEXT).performClick() + + // Assert + verify { mockedOnDismiss.invoke() } + } + + @Test + fun givenEmptyTextInputWhenSubmitIsClickedThenShouldNotCallOnCreate() = + composeExtension.use { + // Arrange + val mockedCreateCustomList: (String) -> Unit = mockk(relaxed = true) + val state = CreateCustomListUiState() + setContentWithTheme { + CreateCustomListDialog(state = state, createCustomList = mockedCreateCustomList) + } + + // Act + onNodeWithText(CREATE_BUTTON_TEXT).performClick() + + // Assert + verify(exactly = 0) { mockedCreateCustomList.invoke(any()) } + } + + @Test + fun givenValidTextInputWhenSubmitIsClickedThenShouldCallOnCreate() = + composeExtension.use { + // Arrange + val mockedCreateCustomList: (String) -> Unit = mockk(relaxed = true) + val inputText = "NEW LIST" + val state = CreateCustomListUiState() + setContentWithTheme { + CreateCustomListDialog(state = state, createCustomList = mockedCreateCustomList) + } + + // Act + onNodeWithTag(CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG).performTextInput(inputText) + onNodeWithText(CREATE_BUTTON_TEXT).performClick() + + // Assert + verify { mockedCreateCustomList.invoke(inputText) } + } + + @Test + fun whenInputIsChangedShouldCallOnInputChanged() = + composeExtension.use { + // Arrange + val mockedOnInputChanged: () -> Unit = mockk(relaxed = true) + val inputText = "NEW LIST" + val state = CreateCustomListUiState() + setContentWithTheme { + CreateCustomListDialog(state = state, onInputChanged = mockedOnInputChanged) + } + + // Act + onNodeWithTag(CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG).performTextInput(inputText) + + // Assert + verify { mockedOnInputChanged.invoke() } + } + + companion object { + private const val NAME_EXIST_ERROR_TEXT = "Name is already taken." + private const val OTHER_ERROR_TEXT = "An error occurred." + private const val CANCEL_BUTTON_TEXT = "Cancel" + private const val CREATE_BUTTON_TEXT = "Create" + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt new file mode 100644 index 0000000000..e79c5a2fe7 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt @@ -0,0 +1,76 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import io.mockk.MockKAnnotations +import io.mockk.mockk +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class DeleteCustomListConfirmationDialogTest { + @OptIn(ExperimentalTestApi::class) + @JvmField + @RegisterExtension + val composeExtension = createEdgeToEdgeComposeExtension() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun givenNameShouldShowDeleteNameTitle() = + composeExtension.use { + // Arrange + val name = "List should be deleted" + setContentWithTheme { DeleteCustomListConfirmationDialog(name = name) } + + // Assert + onNodeWithText(DELETE_TITLE.format(name)).assertExists() + } + + @Test + fun whenDeleteIsClickedShouldCallOnDelete() = + composeExtension.use { + // Arrange + val name = "List should be deleted" + val mockedOnDelete: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + DeleteCustomListConfirmationDialog(name = name, onDelete = mockedOnDelete) + } + + // Act + onNodeWithText(DELETE_BUTTON_TEXT).performClick() + + // Assert + verify { mockedOnDelete.invoke() } + } + + @Test + fun whenCancelIsClickedShouldCallOnBack() = + composeExtension.use { + // Arrange + val name = "List should be deleted" + val mockedOnBack: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + DeleteCustomListConfirmationDialog(name = name, onBack = mockedOnBack) + } + + // Act + onNodeWithText(CANCEL_BUTTON_TEXT).performClick() + + // Assert + verify { mockedOnBack.invoke() } + } + + companion object { + private const val DELETE_TITLE = "Delete \"%s\"?" + private const val CANCEL_BUTTON_TEXT = "Cancel" + private const val DELETE_BUTTON_TEXT = "Delete" + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt new file mode 100644 index 0000000000..cbd6ae09d7 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt @@ -0,0 +1,144 @@ +package net.mullvad.mullvadvpn.compose.dialog + +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.mockk +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.state.UpdateCustomListUiState +import net.mullvad.mullvadvpn.compose.test.EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG +import net.mullvad.mullvadvpn.model.CustomListsError +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class EditCustomListNameDialogTest { + @OptIn(ExperimentalTestApi::class) + @JvmField + @RegisterExtension + val composeExtension = createEdgeToEdgeComposeExtension() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun givenNoErrorShouldShowNoErrorMessage() = + composeExtension.use { + // Arrange + val state = UpdateCustomListUiState(error = null) + setContentWithTheme { EditCustomListNameDialog(state = state) } + + // Assert + onNodeWithText(NAME_EXIST_ERROR_TEXT).assertDoesNotExist() + onNodeWithText(OTHER_ERROR_TEXT).assertDoesNotExist() + } + + @Test + fun givenCustomListExistsShouldShowCustomListExitsErrorText() = + composeExtension.use { + // Arrange + val state = UpdateCustomListUiState(error = CustomListsError.CustomListExists) + setContentWithTheme { EditCustomListNameDialog(state = state) } + + // Assert + onNodeWithText(NAME_EXIST_ERROR_TEXT).assertExists() + onNodeWithText(OTHER_ERROR_TEXT).assertDoesNotExist() + } + + @Test + fun givenOtherCustomListErrorShouldShowAnErrorOccurredErrorText() = + composeExtension.use { + // Arrange + val state = UpdateCustomListUiState(error = CustomListsError.OtherError) + setContentWithTheme { EditCustomListNameDialog(state = state) } + + // Assert + onNodeWithText(NAME_EXIST_ERROR_TEXT).assertDoesNotExist() + onNodeWithText(OTHER_ERROR_TEXT).assertExists() + } + + @Test + fun whenCancelIsClickedShouldDismissDialog() = + composeExtension.use { + // Arrange + val mockedOnDismiss: () -> Unit = mockk(relaxed = true) + val state = UpdateCustomListUiState() + setContentWithTheme { + EditCustomListNameDialog(state = state, onDismiss = mockedOnDismiss) + } + + // Act + onNodeWithText(CANCEL_BUTTON_TEXT).performClick() + + // Assert + verify { mockedOnDismiss.invoke() } + } + + @Test + fun givenEmptyTextInputWhenSaveIsClickedThenShouldNotCallUpdateName() = + composeExtension.use { + // Arrange + val mockedUpdateName: (String) -> Unit = mockk(relaxed = true) + val state = UpdateCustomListUiState() + setContentWithTheme { + EditCustomListNameDialog(state = state, updateName = mockedUpdateName) + } + + // Act + onNodeWithText(SAVE_BUTTON_TEXT).performClick() + + // Assert + verify(exactly = 0) { mockedUpdateName.invoke(any()) } + } + + @Test + fun givenValidTextInputWhenSaveIsClickedThenShouldCallUpdateName() = + composeExtension.use { + // Arrange + val mockedUpdateName: (String) -> Unit = mockk(relaxed = true) + val inputText = "NEW NAME" + val state = UpdateCustomListUiState() + setContentWithTheme { + EditCustomListNameDialog(state = state, updateName = mockedUpdateName) + } + + // Act + onNodeWithTag(EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG).performTextInput(inputText) + onNodeWithText(SAVE_BUTTON_TEXT).performClick() + + // Assert + verify { mockedUpdateName.invoke(inputText) } + } + + @Test + fun whenInputIsChangedShouldCallOnInputChanged() = + composeExtension.use { + // Arrange + val mockedOnInputChanged: () -> Unit = mockk(relaxed = true) + val inputText = "NEW NAME" + val state = UpdateCustomListUiState() + setContentWithTheme { + EditCustomListNameDialog(state = state, onInputChanged = mockedOnInputChanged) + } + + // Act + onNodeWithTag(EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG).performTextInput(inputText) + + // Assert + verify { mockedOnInputChanged.invoke() } + } + + companion object { + private const val NAME_EXIST_ERROR_TEXT = "Name is already taken." + private const val OTHER_ERROR_TEXT = "An error occurred." + private const val CANCEL_BUTTON_TEXT = "Cancel" + private const val SAVE_BUTTON_TEXT = "Save" + } +} 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 new file mode 100644 index 0000000000..5951550550 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt @@ -0,0 +1,246 @@ +package net.mullvad.mullvadvpn.compose.screen + +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.mockk +import io.mockk.verify +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.test.CIRCULAR_PROGRESS_INDICATOR +import net.mullvad.mullvadvpn.compose.test.SAVE_BUTTON_TEST_TAG +import net.mullvad.mullvadvpn.relaylist.RelayItem +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@OptIn(ExperimentalTestApi::class) +class CustomListLocationsScreenTest { + @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun givenLoadingStateShouldShowLoadingSpinner() = + composeExtension.use { + // Arrange + setContentWithTheme { + CustomListLocationsScreen( + state = CustomListLocationsUiState.Loading(newList = false) + ) + } + + // Assert + onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertExists() + } + + @Test + fun givenNewListTrueShouldShowAddLocations() = + composeExtension.use { + // Arrange + val newList = true + setContentWithTheme { + CustomListLocationsScreen( + state = CustomListLocationsUiState.Loading(newList = newList) + ) + } + + // Assert + onNodeWithText(ADD_LOCATIONS_TEXT).assertExists() + } + + @Test + fun givenNewListFalseShouldShowEditLocations() = + composeExtension.use { + // Arrange + val newList = false + setContentWithTheme { + CustomListLocationsScreen( + state = CustomListLocationsUiState.Loading(newList = newList) + ) + } + + // Assert + onNodeWithText(EDIT_LOCATIONS_TEXT).assertExists() + } + + @Test + fun givenListOfAvailableLocationsShouldShowThem() = + composeExtension.use { + // Arrange + setContentWithTheme { + CustomListLocationsScreen( + state = + CustomListLocationsUiState.Content.Data( + availableLocations = DUMMY_RELAY_COUNTRIES, + selectedLocations = emptySet(), + searchTerm = "" + ), + ) + } + + // 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 + fun whenClickingOnRelayShouldCallOnSelectForThatRelay() = + composeExtension.use { + // Arrange + val selectedCountry = DUMMY_RELAY_COUNTRIES[0] + val mockedOnRelaySelectionClicked: (RelayItem, Boolean) -> Unit = mockk(relaxed = true) + setContentWithTheme { + CustomListLocationsScreen( + state = + CustomListLocationsUiState.Content.Data( + newList = false, + availableLocations = DUMMY_RELAY_COUNTRIES, + selectedLocations = setOf(selectedCountry) + ), + onRelaySelectionClick = mockedOnRelaySelectionClicked + ) + } + + // Act + onNodeWithText(selectedCountry.name).performClick() + + // Assert + verify { mockedOnRelaySelectionClicked(selectedCountry, false) } + } + + @Test + fun whenSearchInputIsUpdatedShouldCallOnSearchTermInput() = + composeExtension.use { + // Arrange + val mockedSearchTermInput: (String) -> Unit = mockk(relaxed = true) + setContentWithTheme { + CustomListLocationsScreen( + state = + CustomListLocationsUiState.Content.Data( + newList = false, + availableLocations = DUMMY_RELAY_COUNTRIES, + ), + onSearchTermInput = mockedSearchTermInput + ) + } + val mockSearchString = "SEARCH" + + // Act + onNodeWithText(SEARCH_PLACEHOLDER).performTextInput(mockSearchString) + + // Assert + verify { mockedSearchTermInput.invoke(mockSearchString) } + } + + @Test + fun whenSearchResultNotFoundShouldShowSearchNotFoundText() = + composeExtension.use { + // Arrange + val mockedSearchTermInput: (String) -> Unit = mockk(relaxed = true) + val mockSearchString = "SEARCH" + setContentWithTheme { + CustomListLocationsScreen( + state = + CustomListLocationsUiState.Content.Empty( + newList = false, + searchTerm = mockSearchString + ), + onSearchTermInput = mockedSearchTermInput + ) + } + + // Assert + onNodeWithText(EMPTY_SEARCH_FIRST_ROW.format(mockSearchString), substring = true) + .assertExists() + onNodeWithText(EMPTY_SEARCH_SECOND_ROW, substring = true).assertExists() + } + + @Test + fun whenRelayListIsEmptyShouldShowNoRelaysText() = + composeExtension.use { + // Arrange + val emptySearchString = "" + setContentWithTheme { + CustomListLocationsScreen( + state = + CustomListLocationsUiState.Content.Empty( + newList = false, + searchTerm = emptySearchString + ) + ) + } + + // Assert + onNodeWithText(NO_LOCATIONS_FOUND_TEXT).assertExists() + } + + @Test + fun givenSaveIsEnabledWhenSaveClickedShouldCallOnSaveClick() = + composeExtension.use { + // Arrange + val mockOnSaveClick: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + CustomListLocationsScreen( + state = + CustomListLocationsUiState.Content.Data( + newList = false, + availableLocations = DUMMY_RELAY_COUNTRIES, + saveEnabled = true, + ), + onSaveClick = mockOnSaveClick + ) + } + + // Act + onNodeWithTag(SAVE_BUTTON_TEST_TAG).performClick() + + // Assert + verify { mockOnSaveClick() } + } + + @Test + fun givenSaveIsDisabledWhenSaveClickedShouldNotCallOnSaveClick() = + composeExtension.use { + // Arrange + val mockOnSaveClick: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + CustomListLocationsScreen( + state = + CustomListLocationsUiState.Content.Data( + newList = false, + availableLocations = DUMMY_RELAY_COUNTRIES, + saveEnabled = false, + ), + onSaveClick = mockOnSaveClick + ) + } + + // Act + onNodeWithTag(SAVE_BUTTON_TEST_TAG).performClick() + + // Assert + verify(exactly = 0) { mockOnSaveClick() } + } + + companion object { + 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 NO_LOCATIONS_FOUND_TEXT = "No locations found" + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreenTest.kt new file mode 100644 index 0000000000..da9ed60997 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreenTest.kt @@ -0,0 +1,105 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.material3.SnackbarHostState +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 io.mockk.MockKAnnotations +import io.mockk.mockk +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.data.DUMMY_CUSTOM_LISTS +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.state.CustomListsUiState +import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR +import net.mullvad.mullvadvpn.compose.test.NEW_LIST_BUTTON_TEST_TAG +import net.mullvad.mullvadvpn.relaylist.RelayItem +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@OptIn(ExperimentalTestApi::class) +class CustomListsScreenTest { + @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun givenLoadingStateShouldShowLoadingSpinner() = + composeExtension.use { + // Arrange + setContentWithTheme { + CustomListsScreen( + state = CustomListsUiState.Loading, + snackbarHostState = SnackbarHostState() + ) + } + + // Assert + onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertExists() + } + + @Test + fun givenCustomListsShouldShowTheirNames() = + composeExtension.use { + // Arrange + val customLists = DUMMY_CUSTOM_LISTS + setContentWithTheme { + CustomListsScreen( + state = CustomListsUiState.Content(customLists = customLists), + snackbarHostState = SnackbarHostState() + ) + } + + // Assert + onNodeWithText(customLists[0].name).assertExists() + onNodeWithText(customLists[1].name).assertExists() + } + + @Test + fun whenNewListButtonIsClickedShouldCallAddCustomList() = + composeExtension.use { + // Arrange + val customLists = DUMMY_CUSTOM_LISTS + val mockedAddCustomList: () -> Unit = mockk(relaxed = true) + setContentWithTheme { + CustomListsScreen( + state = CustomListsUiState.Content(customLists = customLists), + snackbarHostState = SnackbarHostState(), + addCustomList = mockedAddCustomList + ) + } + + // Act + onNodeWithTag(NEW_LIST_BUTTON_TEST_TAG).performClick() + + // Assert + verify { mockedAddCustomList() } + } + + @Test + fun whenACustomListIsClickedShouldCallOpenCustomList() = + composeExtension.use { + // Arrange + val customLists = DUMMY_CUSTOM_LISTS + val clickedList = DUMMY_CUSTOM_LISTS[0] + val mockedOpenCustomList: (RelayItem.CustomList) -> Unit = mockk(relaxed = true) + setContentWithTheme { + CustomListsScreen( + state = CustomListsUiState.Content(customLists = customLists), + snackbarHostState = SnackbarHostState(), + openCustomList = mockedOpenCustomList + ) + } + + // Act + onNodeWithText(clickedList.name).performClick() + + // Assert + verify { mockedOpenCustomList(clickedList) } + } +} diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt new file mode 100644 index 0000000000..f44441b536 --- /dev/null +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt @@ -0,0 +1,170 @@ +package net.mullvad.mullvadvpn.compose.screen + +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 io.mockk.MockKAnnotations +import io.mockk.mockk +import io.mockk.verify +import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.data.DUMMY_CUSTOM_LISTS +import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.state.EditCustomListState +import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR +import net.mullvad.mullvadvpn.compose.test.DELETE_DROPDOWN_MENU_ITEM_TEST_TAG +import net.mullvad.mullvadvpn.compose.test.TOP_BAR_DROPDOWN_BUTTON_TEST_TAG +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@OptIn(ExperimentalTestApi::class) +class EditCustomListScreenTest { + @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun givenLoadingStateShouldShowLoadingSpinner() = + composeExtension.use { + // Arrange + setContentWithTheme { EditCustomListScreen(state = EditCustomListState.Loading) } + + // Assert + onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertExists() + } + + @Test + fun givenNotFoundStateShouldShowNotFound() = + composeExtension.use { + // Arrange + setContentWithTheme { EditCustomListScreen(state = EditCustomListState.NotFound) } + + // Assert + onNodeWithText(NOT_FOUND_TEXT).assertExists() + } + + @Test + fun givenContentStateShouldShowNameFromState() = + composeExtension.use { + // Arrange + val customList = DUMMY_CUSTOM_LISTS[0] + setContentWithTheme { + EditCustomListScreen( + state = + EditCustomListState.Content( + id = customList.id, + name = customList.name, + locations = customList.locations + ) + ) + } + + // Assert + onNodeWithText(customList.name) + } + + @Test + fun givenContentStateShouldShowNumberOfLocationsFromState() = + composeExtension.use { + // Arrange + val customList = DUMMY_CUSTOM_LISTS[0] + setContentWithTheme { + EditCustomListScreen( + state = + EditCustomListState.Content( + id = customList.id, + name = customList.name, + locations = customList.locations + ) + ) + } + + // Assert + onNodeWithText(LOCATIONS_TEXT.format(customList.locations.size)) + } + + @Test + fun whenClickingOnDeleteDropdownShouldCallOnDeleteList() = + composeExtension.use { + // Arrange + val mockedOnDelete: (String) -> Unit = mockk(relaxed = true) + val customList = DUMMY_CUSTOM_LISTS[0] + setContentWithTheme { + EditCustomListScreen( + state = + EditCustomListState.Content( + id = customList.id, + name = customList.name, + locations = customList.locations + ), + onDeleteList = mockedOnDelete + ) + } + + // Act + onNodeWithTag(TOP_BAR_DROPDOWN_BUTTON_TEST_TAG).performClick() + onNodeWithTag(DELETE_DROPDOWN_MENU_ITEM_TEST_TAG).performClick() + + // Assert + verify { mockedOnDelete(customList.name) } + } + + @Test + fun whenClickingOnNameCellShouldCallOnNameClicked() = + composeExtension.use { + // Arrange + val mockedOnNameClicked: (String, String) -> Unit = mockk(relaxed = true) + val customList = DUMMY_CUSTOM_LISTS[0] + setContentWithTheme { + EditCustomListScreen( + state = + EditCustomListState.Content( + id = customList.id, + name = customList.name, + locations = customList.locations + ), + onNameClicked = mockedOnNameClicked + ) + } + + // Act + onNodeWithText(customList.name).performClick() + + // Assert + verify { mockedOnNameClicked(customList.id, customList.name) } + } + + @Test + fun whenClickingOnLocationCellShouldCallOnLocationsClicked() = + composeExtension.use { + // Arrange + val mockedOnLocationsClicked: (String) -> Unit = mockk(relaxed = true) + val customList = DUMMY_CUSTOM_LISTS[0] + setContentWithTheme { + EditCustomListScreen( + state = + EditCustomListState.Content( + id = customList.id, + name = customList.name, + locations = customList.locations + ), + onLocationsClicked = mockedOnLocationsClicked + ) + } + + // Act + onNodeWithText(LOCATIONS_TEXT.format(customList.locations.size)).performClick() + + // Assert + verify { mockedOnLocationsClicked(customList.id) } + } + + companion object { + const val NOT_FOUND_TEXT = "Not found" + const val LOCATIONS_TEXT = "%d locations" + } +} 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 fe28357048..28651c3852 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 @@ -3,24 +3,22 @@ package net.mullvad.mullvadvpn.compose.screen 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.mockk import io.mockk.verify import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension +import net.mullvad.mullvadvpn.compose.data.DUMMY_CUSTOM_LISTS +import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES import net.mullvad.mullvadvpn.compose.setContentWithTheme -import net.mullvad.mullvadvpn.compose.state.RelayListState import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR -import net.mullvad.mullvadvpn.model.Constraint -import net.mullvad.mullvadvpn.model.PortRange -import net.mullvad.mullvadvpn.model.RelayEndpointData -import net.mullvad.mullvadvpn.model.RelayList -import net.mullvad.mullvadvpn.model.RelayListCity -import net.mullvad.mullvadvpn.model.RelayListCountry -import net.mullvad.mullvadvpn.model.WireguardEndpointData -import net.mullvad.mullvadvpn.model.WireguardRelayEndpointData -import net.mullvad.mullvadvpn.relaylist.toRelayCountries +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.performLongClick +import net.mullvad.mullvadvpn.relaylist.RelayItem import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -55,12 +53,11 @@ class SelectLocationScreenTest { setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Data( - relayListState = - RelayListState.RelayList( - countries = DUMMY_RELAY_COUNTRIES, - selectedItem = null - ), + SelectLocationUiState.Content( + customLists = emptyList(), + filteredCustomLists = emptyList(), + countries = DUMMY_RELAY_COUNTRIES, + selectedItem = null, selectedOwnership = null, selectedProvidersCount = 0, searchTerm = "" @@ -95,12 +92,11 @@ class SelectLocationScreenTest { setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Data( - relayListState = - RelayListState.RelayList( - countries = updatedDummyList, - selectedItem = updatedDummyList[0].cities[0].relays[0] - ), + SelectLocationUiState.Content( + customLists = emptyList(), + filteredCustomLists = emptyList(), + countries = updatedDummyList, + selectedItem = updatedDummyList[0].cities[0].relays[0], selectedOwnership = null, selectedProvidersCount = 0, searchTerm = "" @@ -125,12 +121,11 @@ class SelectLocationScreenTest { setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Data( - relayListState = - RelayListState.RelayList( - countries = emptyList(), - selectedItem = null - ), + SelectLocationUiState.Content( + customLists = emptyList(), + filteredCustomLists = emptyList(), + countries = emptyList(), + selectedItem = null, selectedOwnership = null, selectedProvidersCount = 0, searchTerm = "" @@ -156,8 +151,11 @@ class SelectLocationScreenTest { setContentWithTheme { SelectLocationScreen( state = - SelectLocationUiState.Data( - relayListState = RelayListState.Empty, + SelectLocationUiState.Content( + customLists = emptyList(), + filteredCustomLists = emptyList(), + countries = emptyList(), + selectedItem = null, selectedOwnership = null, selectedProvidersCount = 0, searchTerm = mockSearchString @@ -171,41 +169,143 @@ class SelectLocationScreenTest { onNodeWithText("Try a different search", substring = true).assertExists() } - companion object { - private val DUMMY_RELAY_1 = - net.mullvad.mullvadvpn.model.Relay( - hostname = "Relay host 1", - active = true, - endpointData = RelayEndpointData.Wireguard(WireguardRelayEndpointData), - owned = true, - provider = "PROVIDER" - ) - private val DUMMY_RELAY_2 = - net.mullvad.mullvadvpn.model.Relay( - hostname = "Relay host 2", - active = true, - endpointData = RelayEndpointData.Wireguard(WireguardRelayEndpointData), - owned = true, - provider = "PROVIDER" - ) - private val DUMMY_RELAY_CITY_1 = - RelayListCity("Relay City 1", "RCi1", arrayListOf(DUMMY_RELAY_1)) - private val DUMMY_RELAY_CITY_2 = - RelayListCity("Relay City 2", "RCi2", arrayListOf(DUMMY_RELAY_2)) - private val DUMMY_RELAY_COUNTRY_1 = - RelayListCountry("Relay Country 1", "RCo1", arrayListOf(DUMMY_RELAY_CITY_1)) - private val DUMMY_RELAY_COUNTRY_2 = - RelayListCountry("Relay Country 2", "RCo2", arrayListOf(DUMMY_RELAY_CITY_2)) + @Test + fun givenNoCustomListsAndSearchIsTermIsEmptyShouldShowCustomListsEmptyText() = + composeExtension.use { + // Arrange + val mockSearchString = "" + setContentWithTheme { + SelectLocationScreen( + state = + SelectLocationUiState.Content( + customLists = emptyList(), + filteredCustomLists = emptyList(), + countries = emptyList(), + selectedItem = null, + selectedOwnership = null, + selectedProvidersCount = 0, + searchTerm = mockSearchString + ), + ) + } + + // Assert + onNodeWithText(CUSTOM_LISTS_EMPTY_TEXT).assertExists() + } + + @Test + fun givenNoCustomListsAndSearchIsActiveShouldNotShowCustomListHeader() = + composeExtension.use { + // Arrange + val mockSearchString = "SEARCH" + setContentWithTheme { + SelectLocationScreen( + state = + SelectLocationUiState.Content( + customLists = DUMMY_CUSTOM_LISTS, + filteredCustomLists = emptyList(), + countries = emptyList(), + selectedItem = null, + selectedOwnership = null, + selectedProvidersCount = 0, + searchTerm = mockSearchString + ), + ) + } + + // Assert + onNodeWithText(CUSTOM_LISTS_EMPTY_TEXT).assertDoesNotExist() + onNodeWithTag(SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG).assertDoesNotExist() + } + + @Test + fun whenCustomListIsClickedShouldCallOnSelectRelay() = + composeExtension.use { + // Arrange + val customList = DUMMY_CUSTOM_LISTS[0] + val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true) + setContentWithTheme { + SelectLocationScreen( + state = + SelectLocationUiState.Content( + customLists = DUMMY_CUSTOM_LISTS, + filteredCustomLists = DUMMY_CUSTOM_LISTS, + countries = emptyList(), + selectedItem = null, + selectedOwnership = null, + selectedProvidersCount = 0, + searchTerm = "" + ), + onSelectRelay = mockedOnSelectRelay + ) + } - private val DUMMY_WIREGUARD_PORT_RANGES = ArrayList<PortRange>() - private val DUMMY_WIREGUARD_ENDPOINT_DATA = - WireguardEndpointData(DUMMY_WIREGUARD_PORT_RANGES) + // Act + onNodeWithText(customList.name).performClick() - private val DUMMY_RELAY_COUNTRIES = - RelayList( - arrayListOf(DUMMY_RELAY_COUNTRY_1, DUMMY_RELAY_COUNTRY_2), - DUMMY_WIREGUARD_ENDPOINT_DATA, + // Assert + verify { mockedOnSelectRelay(customList) } + } + + @Test + fun whenCustomListIsLongClickedShouldShowBottomSheet() = + composeExtension.use { + // Arrange + val customList = DUMMY_CUSTOM_LISTS[0] + val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true) + setContentWithTheme { + SelectLocationScreen( + state = + SelectLocationUiState.Content( + customLists = DUMMY_CUSTOM_LISTS, + filteredCustomLists = DUMMY_CUSTOM_LISTS, + countries = emptyList(), + selectedItem = null, + selectedOwnership = null, + selectedProvidersCount = 0, + searchTerm = "" + ), + onSelectRelay = mockedOnSelectRelay ) - .toRelayCountries(ownership = Constraint.Any(), providers = Constraint.Any()) + } + + // Act + onNodeWithText(customList.name).performLongClick() + + // Assert + onNodeWithTag(SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG) + } + + @Test + fun whenLocationIsLongClickedShouldShowBottomSheet() = + composeExtension.use { + // Arrange + val relayItem = DUMMY_RELAY_COUNTRIES[0] + val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true) + setContentWithTheme { + SelectLocationScreen( + state = + SelectLocationUiState.Content( + customLists = emptyList(), + filteredCustomLists = emptyList(), + countries = DUMMY_RELAY_COUNTRIES, + selectedItem = null, + selectedOwnership = null, + selectedProvidersCount = 0, + searchTerm = "" + ), + onSelectRelay = mockedOnSelectRelay + ) + } + + // Act + onNodeWithText(relayItem.name).performLongClick() + + // Assert + onNodeWithTag(SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG) + } + + companion object { + private const val CUSTOM_LISTS_EMPTY_TEXT = "To create a custom list press the \"︙\"" } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt index 996f610404..efd8e34250 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt @@ -16,7 +16,7 @@ const val LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBER_TEST_TAG = "lazy_list_wireguard_custom_port_number_test_tag" const val CUSTOM_PORT_DIALOG_INPUT_TEST_TAG = "custom_port_dialog_input_test_tag" -// SelectLocationScreen, ConnectScreen +// SelectLocationScreen, ConnectScreen, CustomListLocationsScreen const val CIRCULAR_PROGRESS_INDICATOR = "circular_progress_indicator" // ConnectScreen @@ -42,3 +42,25 @@ const val VOUCHER_INPUT_TEST_TAG = "voucher_input_test_tag" // OutOfTimeScreen const val OUT_OF_TIME_SCREEN_TITLE_TEST_TAG = "out_of_time_screen_title_test_tag" + +// CreateCustomListDialog +const val CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG = "create_custom_list_dialog_input_test_tag" + +// UpdateCustomListDialog +const val EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG = "edit_custom_list_dialog_input_test_tag" + +// CustomListLocationsScreen +const val SAVE_BUTTON_TEST_TAG = "save_button_test_tag" + +// CustomListsScreen +const val NEW_LIST_BUTTON_TEST_TAG = "new_list_button_test_tag" +const val TOP_BAR_DROPDOWN_BUTTON_TEST_TAG = "top_bar_dropdown_button_test_tag" +const val DELETE_DROPDOWN_MENU_ITEM_TEST_TAG = "delete_dropdown_menu_item_test_tag" + +// SelectLocationScreen +const val SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG = + "select_location_custom_list_header_test_tag" +const val SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG = + "select_location_custom_list_bottom_sheet_test_tag" +const val SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG = + "select_location_location_bottom_sheet_test_tag" |
