diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt index 2ab114777e..331b46f665 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt @@ -156,22 +156,10 @@ fun LazyListScope.vaultAddEditLoginItems( items(loginState.uriList) { uriItem -> Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextFieldWithActions( - label = stringResource(id = R.string.uri), - value = uriItem.uri.orEmpty(), - onValueChange = { - loginItemTypeHandlers.onUriTextChange(uriItem.copy(uri = it)) - }, - actions = { - BitwardenIconButtonWithResource( - iconRes = IconResource( - iconPainter = painterResource(id = R.drawable.ic_settings), - contentDescription = stringResource(id = R.string.options), - ), - onClick = loginItemTypeHandlers.onUriSettingsClick, - ) - }, - modifier = Modifier.padding(horizontal = 16.dp), + VaultAddEditUriItem( + uriItem = uriItem, + onUriValueChange = loginItemTypeHandlers.onUriValueChange, + onUriItemRemoved = loginItemTypeHandlers.onRemoveUriClick, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditUriItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditUriItem.kt new file mode 100644 index 0000000000..b011906be1 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditUriItem.kt @@ -0,0 +1,99 @@ +package com.x8bit.bitwarden.ui.vault.feature.addedit + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialogRow +import com.x8bit.bitwarden.ui.platform.components.BitwardenIconButtonWithResource +import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionDialog +import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions +import com.x8bit.bitwarden.ui.platform.components.model.IconResource +import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem +import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType +import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDisplayMatchType +import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toUriMatchType + +/** + * The URI item displayed to the user. + */ +@Suppress("LongMethod") +@Composable +fun VaultAddEditUriItem( + uriItem: UriItem, + onUriItemRemoved: (UriItem) -> Unit, + onUriValueChange: (UriItem) -> Unit, +) { + var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) } + var shouldShowMatchDialog by rememberSaveable { mutableStateOf(false) } + + BitwardenTextFieldWithActions( + label = stringResource(id = R.string.uri), + value = uriItem.uri.orEmpty(), + onValueChange = { onUriValueChange(uriItem.copy(uri = it)) }, + actions = { + BitwardenIconButtonWithResource( + iconRes = IconResource( + iconPainter = painterResource(id = R.drawable.ic_settings), + contentDescription = stringResource(id = R.string.options), + ), + onClick = { shouldShowOptionsDialog = true }, + ) + }, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + if (shouldShowOptionsDialog) { + BitwardenSelectionDialog( + title = stringResource(id = R.string.options), + onDismissRequest = { shouldShowOptionsDialog = false }, + ) { + BitwardenBasicDialogRow( + text = stringResource(id = R.string.match_detection), + onClick = { + shouldShowOptionsDialog = false + shouldShowMatchDialog = true + }, + ) + BitwardenBasicDialogRow( + text = stringResource(id = R.string.remove), + onClick = { + shouldShowOptionsDialog = false + onUriItemRemoved(uriItem) + }, + ) + } + } + + if (shouldShowMatchDialog) { + val selectedString = uriItem.match.toDisplayMatchType().text.invoke() + + BitwardenSelectionDialog( + title = stringResource(id = R.string.uri_match_detection), + onDismissRequest = { shouldShowMatchDialog = false }, + ) { + UriMatchDisplayType + .entries + .forEach { matchType -> + BitwardenSelectionRow( + text = matchType.text, + isSelected = matchType.text.invoke() == selectedString, + onClick = { + shouldShowMatchDialog = false + onUriValueChange( + uriItem.copy(match = matchType.toUriMatchType()), + ) + }, + ) + } + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index ff6189fb66..1c042f5eac 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -190,6 +190,7 @@ class VaultAddEditViewModel @Inject constructor( is VaultAddEditAction.Common.CustomFieldActionSelect -> handleCustomFieldActionSelected( action, ) + is VaultAddEditAction.Common.CollectionSelect -> handleCollectionSelect(action) } } @@ -542,8 +543,8 @@ class VaultAddEditViewModel @Inject constructor( handleLoginPasswordTextInputChange(action) } - is VaultAddEditAction.ItemType.LoginType.UriTextChange -> { - handleLoginUriTextInputChange(action) + is VaultAddEditAction.ItemType.LoginType.UriValueChange -> { + handleLoginUriValueInputChange(action) } is VaultAddEditAction.ItemType.LoginType.OpenUsernameGeneratorClick -> { @@ -562,8 +563,8 @@ class VaultAddEditViewModel @Inject constructor( handleLoginSetupTotpClick(action) } - is VaultAddEditAction.ItemType.LoginType.UriSettingsClick -> { - handleLoginUriSettingsClick() + is VaultAddEditAction.ItemType.LoginType.RemoveUriClick -> { + handleLoginRemoveUriClick(action) } is VaultAddEditAction.ItemType.LoginType.AddNewUriClick -> { @@ -596,16 +597,16 @@ class VaultAddEditViewModel @Inject constructor( } } - private fun handleLoginUriTextInputChange( - action: VaultAddEditAction.ItemType.LoginType.UriTextChange, + private fun handleLoginUriValueInputChange( + action: VaultAddEditAction.ItemType.LoginType.UriValueChange, ) { updateLoginContent { loginType -> loginType.copy( uriList = loginType .uriList .map { uriItem -> - if (uriItem.id == action.uri.id) { - action.uri + if (uriItem.id == action.uriItem.id) { + action.uriItem } else { uriItem } @@ -614,6 +615,18 @@ class VaultAddEditViewModel @Inject constructor( } } + private fun handleLoginRemoveUriClick( + action: VaultAddEditAction.ItemType.LoginType.RemoveUriClick, + ) { + updateLoginContent { loginType -> + loginType.copy( + uriList = loginType.uriList.filter { + it != action.uriItem + }, + ) + } + } + private fun handleLoginOpenUsernameGeneratorClick() { sendEvent(event = VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Username)) } @@ -1899,11 +1912,11 @@ sealed class VaultAddEditAction { data class PasswordTextChange(val password: String) : LoginType() /** - * Fired when the URI text input is changed. + * Fired when the URI is changed. * - * @property uri The new URI text. + * @property uriItem The new URI. */ - data class UriTextChange(val uri: UriItem) : LoginType() + data class UriValueChange(val uriItem: UriItem) : LoginType() /** * Represents the action to set up TOTP. @@ -1940,9 +1953,9 @@ sealed class VaultAddEditAction { data object OpenPasswordGeneratorClick : LoginType() /** - * Represents the action of clicking TOTP settings + * Represents the action of removing a URI item. */ - data object UriSettingsClick : LoginType() + data class RemoveUriClick(val uriItem: UriItem) : LoginType() /** * Represents the action to add a new URI field. @@ -2164,8 +2177,8 @@ sealed class VaultAddEditAction { * Indicates that the vault item data has been received. */ data class VaultDataReceive( - val vaultData: DataState, - val userData: UserState?, + val vaultData: DataState, + val userData: UserState?, ) : Internal() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt index ce53fe6aa8..4155745f28 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt @@ -10,8 +10,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem * * @property onUsernameTextChange Handles the action when the username text is changed. * @property onPasswordTextChange Handles the action when the password text is changed. - * @property onUriTextChange Handles the action when the URI text is changed. - * reprompt toggle is changed. + * @property onRemoveUriClick Handles the action when the URI is removed. + * @property onUriValueChange Handles the action when the URI value is changed. * @property onOpenUsernameGeneratorClick Handles the action when the username generator * button is clicked. * @property onPasswordCheckerClick Handles the action when the password checker @@ -28,14 +28,14 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem data class VaultAddEditLoginTypeHandlers( val onUsernameTextChange: (String) -> Unit, val onPasswordTextChange: (String) -> Unit, - val onUriTextChange: (UriItem) -> Unit, + val onRemoveUriClick: (UriItem) -> Unit, + val onUriValueChange: (UriItem) -> Unit, val onOpenUsernameGeneratorClick: () -> Unit, val onPasswordCheckerClick: () -> Unit, val onOpenPasswordGeneratorClick: () -> Unit, val onSetupTotpClick: (Boolean) -> Unit, val onCopyTotpKeyClick: (String) -> Unit, val onClearTotpKeyClick: () -> Unit, - val onUriSettingsClick: () -> Unit, val onAddNewUriClick: () -> Unit, ) { companion object { @@ -60,9 +60,9 @@ data class VaultAddEditLoginTypeHandlers( VaultAddEditAction.ItemType.LoginType.PasswordTextChange(newPassword), ) }, - onUriTextChange = { newUri -> + onUriValueChange = { newUri -> viewModel.trySendAction( - VaultAddEditAction.ItemType.LoginType.UriTextChange(newUri), + VaultAddEditAction.ItemType.LoginType.UriValueChange(newUri), ) }, onOpenUsernameGeneratorClick = { @@ -85,8 +85,12 @@ data class VaultAddEditLoginTypeHandlers( VaultAddEditAction.ItemType.LoginType.SetupTotpClick(isGranted), ) }, - onUriSettingsClick = { - viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.UriSettingsClick) + onRemoveUriClick = { uriItem -> + viewModel.trySendAction( + VaultAddEditAction.ItemType.LoginType.RemoveUriClick( + uriItem, + ), + ) }, onAddNewUriClick = { viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.AddNewUriClick) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/UriMatchDisplayType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/UriMatchDisplayType.kt new file mode 100644 index 0000000000..5c7642643b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/model/UriMatchDisplayType.kt @@ -0,0 +1,50 @@ +package com.x8bit.bitwarden.ui.vault.feature.addedit.model + +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.Text +import com.x8bit.bitwarden.ui.platform.base.util.asText + +/** + * The options displayed to the user when choosing a match type + * for their URI. + */ +@Suppress("MagicNumber") +enum class UriMatchDisplayType( + val text: Text, +) { + /** + * the default option for when the user has not chosen one. + */ + DEFAULT(R.string.default_text.asText()), + + /** + * The URIs match if their top-level and second-level domains match. + */ + BASE_DOMAIN(R.string.base_domain.asText()), + + /** + * The URIs match if their hostnames (and ports if specified) match. + */ + HOST(R.string.host.asText()), + + /** + * The URIs match if the "test" URI starts with the known URI. + */ + STARTS_WITH(R.string.starts_with.asText()), + + /** + * The URIs match if the "test" URI matches the known URI according to a specified regular + * expression for the item. + */ + REGULAR_EXPRESSION(R.string.reg_ex.asText()), + + /** + * The URIs match if they are exactly the same. + */ + EXACT(R.string.exact.asText()), + + /** + * The URIs should never match. + */ + NEVER(R.string.never.asText()), +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/UriMatchDisplayTypeUtil.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/UriMatchDisplayTypeUtil.kt new file mode 100644 index 0000000000..7720357f4e --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/UriMatchDisplayTypeUtil.kt @@ -0,0 +1,32 @@ +package com.x8bit.bitwarden.ui.vault.feature.addedit.util + +import com.bitwarden.core.UriMatchType +import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType + +/** + * Method to convert the SDK match type for display to the user. + */ +fun UriMatchType?.toDisplayMatchType(): UriMatchDisplayType = + when (this) { + UriMatchType.DOMAIN -> UriMatchDisplayType.BASE_DOMAIN + UriMatchType.EXACT -> UriMatchDisplayType.EXACT + UriMatchType.HOST -> UriMatchDisplayType.HOST + UriMatchType.NEVER -> UriMatchDisplayType.NEVER + UriMatchType.REGULAR_EXPRESSION -> UriMatchDisplayType.REGULAR_EXPRESSION + UriMatchType.STARTS_WITH -> UriMatchDisplayType.STARTS_WITH + null -> UriMatchDisplayType.DEFAULT + } + +/** + * Method to convert the match display type over to the SDK match type. + */ +fun UriMatchDisplayType.toUriMatchType(): UriMatchType? = + when (this) { + UriMatchDisplayType.DEFAULT -> null + UriMatchDisplayType.BASE_DOMAIN -> UriMatchType.DOMAIN + UriMatchDisplayType.HOST -> UriMatchType.HOST + UriMatchDisplayType.STARTS_WITH -> UriMatchType.STARTS_WITH + UriMatchDisplayType.REGULAR_EXPRESSION -> UriMatchType.REGULAR_EXPRESSION + UriMatchDisplayType.EXACT -> UriMatchType.EXACT + UriMatchDisplayType.NEVER -> UriMatchType.NEVER + } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index dca1b5ea6b..ea0fa61e80 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTouchInput +import com.bitwarden.core.UriMatchType import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest @@ -720,7 +721,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { } @Test - fun `in ItemType_Login state changing URI text field should trigger UriTextChange`() { + fun `in ItemType_Login state changing URI text field should trigger UriValueChange`() { mutableStateFlow.update { currentState -> updateLoginType(currentState) { copy(uriList = listOf(UriItem("TestId", "URI", null))) @@ -733,7 +734,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { verify { viewModel.trySendAction( - VaultAddEditAction.ItemType.LoginType.UriTextChange( + VaultAddEditAction.ItemType.LoginType.UriValueChange( UriItem("TestId", "TestURI", null), ), ) @@ -759,20 +760,184 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test - fun `in ItemType_Login state clicking the URI settings action should trigger UriSettingsClick`() { + fun `in ItemType_Login Uri settings dialog should be dismissed on cancel click`() { composeTestRule .onNodeWithTextAfterScroll(text = "URI") .onSiblings() .filterToOne(hasContentDescription(value = "Options")) .performClick() + composeTestRule + .onNodeWithText("Cancel") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule.assertNoDialogExists() + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_Login Uri settings dialog should send RemoveUriClick action if remove is clicked`() { + mutableStateFlow.update { currentState -> + updateLoginType(currentState) { + copy(uriList = listOf(UriItem("TestId", null, null))) + } + } + + composeTestRule + .onNodeWithTextAfterScroll(text = "URI") + .onSiblings() + .filterToOne(hasContentDescription(value = "Options")) + .performClick() + + composeTestRule + .onNodeWithText("Remove") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule.assertNoDialogExists() + verify { viewModel.trySendAction( - VaultAddEditAction.ItemType.LoginType.UriSettingsClick, + VaultAddEditAction.ItemType.LoginType.RemoveUriClick( + UriItem( + "TestId", + null, + null, + ), + ), ) } } + @Suppress("MaxLineLength") + @Test + fun `in ItemType_Login Uri settings dialog with open match detection click should open list of options`() { + composeTestRule + .onNodeWithTextAfterScroll(text = "URI") + .onSiblings() + .filterToOne(hasContentDescription(value = "Options")) + .performClick() + + composeTestRule + .onNodeWithText("Match detection") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule + .onAllNodesWithText("URI match detection") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Default") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Base domain") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Host") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Starts with") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Regular expression") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Exact") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_Login on URI settings click and on match detection click and option click should emit UriValueChange action`() { + mutableStateFlow.update { currentState -> + updateLoginType(currentState) { + copy(uriList = listOf(UriItem("TestId", null, null))) + } + } + + composeTestRule + .onNodeWithTextAfterScroll(text = "URI") + .onSiblings() + .filterToOne(hasContentDescription(value = "Options")) + .performClick() + + composeTestRule + .onNodeWithText("Match detection") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule + .onNodeWithText("URI match detection") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Exact") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule.assertNoDialogExists() + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LoginType.UriValueChange( + UriItem( + "TestId", + null, + UriMatchType.EXACT, + ), + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in ItemType_Login on URI settings click and on match detection click and cancel click should dismiss the dialog`() { + mutableStateFlow.update { currentState -> + updateLoginType(currentState) { + copy(uriList = listOf(UriItem("TestId", null, null))) + } + } + + composeTestRule + .onNodeWithTextAfterScroll(text = "URI") + .onSiblings() + .filterToOne(hasContentDescription(value = "Options")) + .performClick() + + composeTestRule + .onNodeWithText("Match detection") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule + .onAllNodesWithText("URI match detection") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText("Cancel") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule.assertNoDialogExists() + } + @Test fun `in ItemType_Login state clicking the New URI button should trigger AddNewUriClick`() { composeTestRule @@ -1884,7 +2049,7 @@ class VaultAddEditScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test fun `Ownership option should send OwnershipChange action`() { - mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES + mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES updateStateWithOwners() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 40b4d8f119..873d8f7028 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -882,23 +882,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { assertEquals(expectedState, viewModel.stateFlow.value) } - @Test - fun `UriTextChange should update uri in LoginItem`() = runTest { - val action = VaultAddEditAction.ItemType.LoginType.UriTextChange( - UriItem("testId", "TestUri", null), - ) - - viewModel.actionChannel.trySend(action) - - val expectedState = createVaultAddItemState( - typeContentViewState = createLoginTypeContentViewState( - uri = listOf(UriItem("testId", "TestUri", null)), - ), - ) - - assertEquals(expectedState, viewModel.stateFlow.value) - } - @Suppress("MaxLineLength") @Test fun `OpenUsernameGeneratorClick should emit NavigateToGeneratorModal with username GeneratorMode`() = @@ -1110,13 +1093,65 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `UriSettingsClick should emit ShowToast with 'URI Settings' message`() = runTest { - val viewModel = createAddVaultItemViewModel() + fun `UriValueChange should update URI value in state`() = runTest { + val viewModel = createAddVaultItemViewModel( + savedStateHandle = createSavedStateHandleWithState( + state = createVaultAddItemState( + typeContentViewState = createLoginTypeContentViewState( + uri = listOf(UriItem("testID", null, null)), + ), + ), + vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID), + ), + ) + val expectedState = loginInitialState.copy( + viewState = VaultAddEditState.ViewState.Content( + common = createCommonContentViewState(), + type = createLoginTypeContentViewState( + uri = listOf(UriItem("testID", "Test", null)), + ), + ), + ) - viewModel.eventFlow.test { - viewModel.actionChannel.trySend(VaultAddEditAction.ItemType.LoginType.UriSettingsClick) - assertEquals(VaultAddEditEvent.ShowToast("URI Settings".asText()), awaitItem()) - } + viewModel.trySendAction( + VaultAddEditAction.ItemType.LoginType.UriValueChange( + uriItem = UriItem("testID", "Test", null), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + } + + @Suppress("MaxLineLength") + @Test + fun `RemoveUriClick should remove URI value in state`() = runTest { + val viewModel = createAddVaultItemViewModel( + savedStateHandle = createSavedStateHandleWithState( + state = createVaultAddItemState( + typeContentViewState = createLoginTypeContentViewState( + uri = listOf(UriItem("testID", null, null)), + ), + ), + vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID), + ), + ) + + val expectedState = loginInitialState.copy( + viewState = VaultAddEditState.ViewState.Content( + common = createCommonContentViewState(), + type = createLoginTypeContentViewState( + uri = listOf(), + ), + ), + ) + + viewModel.trySendAction( + VaultAddEditAction.ItemType.LoginType.RemoveUriClick( + uriItem = UriItem("testID", null, null), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) } @Test diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/UriMatchDisplayUtilTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/UriMatchDisplayUtilTest.kt new file mode 100644 index 0000000000..62e0c9b2fa --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/UriMatchDisplayUtilTest.kt @@ -0,0 +1,40 @@ +package com.x8bit.bitwarden.ui.vault.feature.addedit.util + +import com.bitwarden.core.UriMatchType +import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class UriMatchDisplayUtilTest { + @Test + fun `toDisplayMatchType should correctly convert to UriMatchDisplayType`() { + URI_MATCH_TYPE_MAP.forEach { + assertEquals( + it.key.toDisplayMatchType(), + it.value, + ) + } + } + + @Test + fun `toUriMatchType should correctly convert to UriMatchType`() { + URI_MATCH_TYPE_MAP.forEach { + assertEquals( + it.key, + it.value.toUriMatchType(), + ) + } + } +} + +private val URI_MATCH_TYPE_MAP: Map = + mapOf( + Pair(null, UriMatchDisplayType.DEFAULT), + Pair(UriMatchType.DOMAIN, UriMatchDisplayType.BASE_DOMAIN), + Pair(UriMatchType.HOST, UriMatchDisplayType.HOST), + Pair(UriMatchType.EXACT, UriMatchDisplayType.EXACT), + Pair(UriMatchType.STARTS_WITH, UriMatchDisplayType.STARTS_WITH), + Pair(UriMatchType.REGULAR_EXPRESSION, UriMatchDisplayType.REGULAR_EXPRESSION), + Pair(UriMatchType.EXACT, UriMatchDisplayType.EXACT), + Pair(UriMatchType.NEVER, UriMatchDisplayType.NEVER), + )