mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-1683: Show master password reprompts on Search Screen (#925)
This commit is contained in:
parent
81c78fc115
commit
3fe0950983
6 changed files with 488 additions and 76 deletions
|
@ -133,6 +133,34 @@ fun SearchContent(
|
|||
showConfirmationDialog = option
|
||||
}
|
||||
|
||||
is ListingItemOverflowAction.VaultAction.EditClick -> {
|
||||
if (it.shouldDisplayMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData =
|
||||
MasterPasswordRepromptData(
|
||||
cipherId = it.id,
|
||||
type = MasterPasswordRepromptData.Type.Edit,
|
||||
)
|
||||
} else {
|
||||
searchHandlers.onOverflowItemClick(option)
|
||||
}
|
||||
}
|
||||
|
||||
is ListingItemOverflowAction.VaultAction.CopyPasswordClick -> {
|
||||
if (it.shouldDisplayMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData =
|
||||
MasterPasswordRepromptData(
|
||||
cipherId = it.id,
|
||||
type = MasterPasswordRepromptData
|
||||
.Type
|
||||
.CopyPassword(
|
||||
password = option.password,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
searchHandlers.onOverflowItemClick(option)
|
||||
}
|
||||
}
|
||||
|
||||
else -> searchHandlers.onOverflowItemClick(option)
|
||||
}
|
||||
},
|
||||
|
@ -179,13 +207,15 @@ private fun AutofillSelectionDialog(
|
|||
)
|
||||
} else {
|
||||
when (type) {
|
||||
MasterPasswordRepromptData.Type.AUTOFILL -> {
|
||||
MasterPasswordRepromptData.Type.Autofill -> {
|
||||
onAutofillItemClick(item.id)
|
||||
}
|
||||
|
||||
MasterPasswordRepromptData.Type.AUTOFILL_AND_SAVE -> {
|
||||
MasterPasswordRepromptData.Type.AutofillAndSave -> {
|
||||
onAutofillAndSaveItemClick(item.id)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,7 +229,7 @@ private fun AutofillSelectionDialog(
|
|||
onClick = {
|
||||
selectionCallback(
|
||||
displayItem,
|
||||
MasterPasswordRepromptData.Type.AUTOFILL,
|
||||
MasterPasswordRepromptData.Type.Autofill,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -210,7 +240,7 @@ private fun AutofillSelectionDialog(
|
|||
onClick = {
|
||||
selectionCallback(
|
||||
displayItem,
|
||||
MasterPasswordRepromptData.Type.AUTOFILL_AND_SAVE,
|
||||
MasterPasswordRepromptData.Type.AutofillAndSave,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -517,25 +517,53 @@ class SearchViewModel @Inject constructor(
|
|||
}
|
||||
return
|
||||
}
|
||||
handleMasterPasswordRepromptData(data = action.masterPasswordRepromptData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the deferred actions
|
||||
when (action.masterPasswordRepromptData.type) {
|
||||
MasterPasswordRepromptData.Type.AUTOFILL -> {
|
||||
trySendAction(
|
||||
SearchAction.AutofillItemClick(
|
||||
itemId = action.masterPasswordRepromptData.cipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
private fun handleMasterPasswordRepromptData(
|
||||
data: MasterPasswordRepromptData,
|
||||
) {
|
||||
// Complete the deferred actions
|
||||
val cipherId = data.cipherId
|
||||
when (val type = data.type) {
|
||||
MasterPasswordRepromptData.Type.Autofill -> {
|
||||
trySendAction(
|
||||
SearchAction.AutofillItemClick(
|
||||
itemId = cipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
MasterPasswordRepromptData.Type.AUTOFILL_AND_SAVE -> {
|
||||
trySendAction(
|
||||
SearchAction.AutofillAndSaveItemClick(
|
||||
itemId = action.masterPasswordRepromptData.cipherId,
|
||||
MasterPasswordRepromptData.Type.AutofillAndSave -> {
|
||||
trySendAction(
|
||||
SearchAction.AutofillAndSaveItemClick(
|
||||
itemId = cipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
MasterPasswordRepromptData.Type.Edit -> {
|
||||
trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = cipherId,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is MasterPasswordRepromptData.Type.CopyPassword -> {
|
||||
trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
overflowAction = ListingItemOverflowAction
|
||||
.VaultAction
|
||||
.CopyPasswordClick(
|
||||
password = type.password,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1100,8 +1128,32 @@ data class MasterPasswordRepromptData(
|
|||
/**
|
||||
* The type of action that requires the prompt.
|
||||
*/
|
||||
enum class Type {
|
||||
AUTOFILL,
|
||||
AUTOFILL_AND_SAVE,
|
||||
sealed class Type : Parcelable {
|
||||
|
||||
/**
|
||||
* Autofill was selected.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Autofill : Type()
|
||||
|
||||
/**
|
||||
* Autofill-and-save was selected.
|
||||
*/
|
||||
@Parcelize
|
||||
data object AutofillAndSave : Type()
|
||||
|
||||
/**
|
||||
* Edit was selected.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Edit : Type()
|
||||
|
||||
/**
|
||||
* Copy password was selected.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CopyPassword(
|
||||
val password: String,
|
||||
) : Type()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -190,7 +190,7 @@ private fun CipherView.toDisplayItem(
|
|||
.filter {
|
||||
this.login != null || (it != AutofillSelectionOption.AUTOFILL_AND_SAVE)
|
||||
},
|
||||
shouldDisplayMasterPasswordReprompt = isAutofill && reprompt == CipherRepromptType.PASSWORD,
|
||||
shouldDisplayMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||
)
|
||||
|
||||
private fun CipherView.toIconData(
|
||||
|
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.test.onChildren
|
|||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
|
@ -23,6 +24,7 @@ import com.x8bit.bitwarden.ui.platform.feature.search.model.AutofillSelectionOpt
|
|||
import com.x8bit.bitwarden.ui.platform.feature.search.util.createMockDisplayItemForCipher
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.util.createMockDisplayItemForSend
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.util.assertMasterPasswordDialogDisplayed
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
|
@ -285,29 +287,7 @@ class SearchScreenTest : BaseComposeTest() {
|
|||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule.assertMasterPasswordDialogDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -342,29 +322,7 @@ class SearchScreenTest : BaseComposeTest() {
|
|||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule.assertMasterPasswordDialogDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -433,7 +391,7 @@ class SearchScreenTest : BaseComposeTest() {
|
|||
password = "password",
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = "mockId-1",
|
||||
type = MasterPasswordRepromptData.Type.AUTOFILL,
|
||||
type = MasterPasswordRepromptData.Type.Autofill,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -469,7 +427,7 @@ class SearchScreenTest : BaseComposeTest() {
|
|||
password = "password",
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = "mockId-1",
|
||||
type = MasterPasswordRepromptData.Type.AUTOFILL_AND_SAVE,
|
||||
type = MasterPasswordRepromptData.Type.AutofillAndSave,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -518,6 +476,293 @@ class SearchScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithText(text = "Search mockName").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on cipher item overflow click should display options dialog`() {
|
||||
val number = 1
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = SearchState.ViewState.Content(
|
||||
displayItems = listOf(createMockDisplayItemForCipher(number = number)),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.onChildren()
|
||||
.filterToOne(hasText("mockName-$number"))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on cipher item overflow option click should emit the appropriate action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = SearchState.ViewState.Content(
|
||||
displayItems = listOf(createMockDisplayItemForCipher(number = 1)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("View")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
cipherId = "mockId-1",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Edit")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.EditClick(
|
||||
cipherId = "mockId-1",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Copy username")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.CopyUsernameClick(
|
||||
username = "mockUsername-1",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Copy password")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.CopyPasswordClick(
|
||||
password = "mockPassword-1",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Launch")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
overflowAction = ListingItemOverflowAction.VaultAction.LaunchClick(
|
||||
url = "www.mockuri1.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on cipher item overflow edit click when reprompt required should show the master password dialog`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = SearchState.ViewState.Content(
|
||||
displayItems = listOf(
|
||||
createMockDisplayItemForCipher(number = 1)
|
||||
.copy(shouldDisplayMasterPasswordReprompt = true),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Edit")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||
|
||||
composeTestRule.assertMasterPasswordDialogDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking submit on the master password dialog for edit should close the dialog and send MasterPasswordRepromptSubmit`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = SearchState.ViewState.Content(
|
||||
displayItems = listOf(
|
||||
createMockDisplayItemForCipher(number = 1)
|
||||
.copy(shouldDisplayMasterPasswordReprompt = true),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Edit")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performTextInput("password")
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.MasterPasswordRepromptSubmit(
|
||||
password = "password",
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = "mockId-1",
|
||||
type = MasterPasswordRepromptData.Type.Edit,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on cipher item overflow copy password click when reprompt required should show the master password dialog`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = SearchState.ViewState.Content(
|
||||
displayItems = listOf(
|
||||
createMockDisplayItemForCipher(number = 1)
|
||||
.copy(shouldDisplayMasterPasswordReprompt = true),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Copy password")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||
|
||||
composeTestRule.assertMasterPasswordDialogDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking submit on the master password dialog for copy password should close the dialog and send MasterPasswordRepromptSubmit`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = SearchState.ViewState.Content(
|
||||
displayItems = listOf(
|
||||
createMockDisplayItemForCipher(number = 1)
|
||||
.copy(shouldDisplayMasterPasswordReprompt = true),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Options")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Copy password")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performTextInput("password")
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.MasterPasswordRepromptSubmit(
|
||||
password = "password",
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = "mockId-1",
|
||||
type = MasterPasswordRepromptData.Type.CopyPassword(
|
||||
password = "mockPassword-1",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on send item overflow click should display dialog`() {
|
||||
val number = 1
|
||||
|
|
|
@ -330,7 +330,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
|||
password = password,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = cipherId,
|
||||
type = MasterPasswordRepromptData.Type.AUTOFILL,
|
||||
type = MasterPasswordRepromptData.Type.Autofill,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -368,7 +368,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
|||
password = password,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = cipherId,
|
||||
type = MasterPasswordRepromptData.Type.AUTOFILL,
|
||||
type = MasterPasswordRepromptData.Type.Autofill,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -403,7 +403,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
|||
password = password,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = cipherId,
|
||||
type = MasterPasswordRepromptData.Type.AUTOFILL,
|
||||
type = MasterPasswordRepromptData.Type.Autofill,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -447,7 +447,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
|||
password = password,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = cipherId,
|
||||
type = MasterPasswordRepromptData.Type.AUTOFILL_AND_SAVE,
|
||||
type = MasterPasswordRepromptData.Type.AutofillAndSave,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -460,6 +460,59 @@ class SearchViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for edit should emit NavigateToEditCipher`() =
|
||||
runTest {
|
||||
val cipherId = "cipherId-1234"
|
||||
val password = "password"
|
||||
val viewModel = createViewModel()
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Success(isValid = true)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.MasterPasswordRepromptSubmit(
|
||||
password = password,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = cipherId,
|
||||
type = MasterPasswordRepromptData.Type.Edit,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(SearchEvent.NavigateToEditCipher(cipherId), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `MasterPasswordRepromptSubmit for a request Success with a valid password for copy password should call setText on the ClipboardManager`() =
|
||||
runTest {
|
||||
val cipherId = "cipherId-1234"
|
||||
val password = "password"
|
||||
val viewModel = createViewModel()
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
} returns ValidatePasswordResult.Success(isValid = true)
|
||||
|
||||
viewModel.trySendAction(
|
||||
SearchAction.MasterPasswordRepromptSubmit(
|
||||
password = password,
|
||||
masterPasswordRepromptData = MasterPasswordRepromptData(
|
||||
cipherId = cipherId,
|
||||
type = MasterPasswordRepromptData.Type.CopyPassword(
|
||||
password = password,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(password)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OverflowOptionClick Send EditClick should emit NavigateToEditSend`() = runTest {
|
||||
val sendId = "sendId"
|
||||
|
|
|
@ -5,6 +5,9 @@ import androidx.compose.ui.semantics.getOrNull
|
|||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteraction
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
|
@ -44,6 +47,35 @@ fun ComposeContentTestRule.assertNoDialogExists() {
|
|||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the master password reprompt dialog is displayed.
|
||||
*/
|
||||
fun ComposeContentTestRule.assertMasterPasswordDialogDisplayed() {
|
||||
this
|
||||
.onAllNodesWithText(text = "Master password confirmation")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
this
|
||||
.onAllNodesWithText(
|
||||
text = "This action is protected, to continue please re-enter your master " +
|
||||
"password to verify your identity.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
this
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
this
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
this
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper that asserts that the node does not exist in the scrollable list.
|
||||
*/
|
||||
|
|
Loading…
Add table
Reference in a new issue