mirror of
https://github.com/bitwarden/android.git
synced 2026-05-27 15:15:33 -05:00
Modify Add Sends UI to allow for editing existing sends (#575)
This commit is contained in:
@@ -2,6 +2,9 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
@@ -22,6 +25,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
||||
import com.x8bit.bitwarden.ui.util.isEditableText
|
||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
@@ -35,6 +39,7 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class AddSendScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
@@ -229,6 +234,41 @@ class AddSendScreenTest : BaseComposeTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `segmented buttons should appear based on state`() {
|
||||
mutableStateFlow.update { it.copy(addSendType = AddSendType.AddItem) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Type")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("File")
|
||||
.filterToOne(!isEditableText)
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Text")
|
||||
.filterToOne(!isEditableText)
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(addSendType = AddSendType.EditItem(sendItemId = "sendId"))
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Type")
|
||||
.assertIsNotDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("File")
|
||||
.filterToOne(!isEditableText)
|
||||
.assertIsNotDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Text")
|
||||
.filterToOne(!isEditableText)
|
||||
.assertIsNotDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `File segmented button click should send FileTypeClick`() {
|
||||
composeTestRule
|
||||
@@ -594,6 +634,64 @@ class AddSendScreenTest : BaseComposeTest() {
|
||||
.assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in edit mode, clear button should be enabled based on state`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(addSendType = AddSendType.EditItem(sendItemId = "sendId"))
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Options")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Clear")
|
||||
.performScrollTo()
|
||||
.assertIsNotEnabled()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
expirationDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Clear")
|
||||
.performScrollTo()
|
||||
.assertIsEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in edit mode, clear button should send ClearExpirationDate`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
addSendType = AddSendType.EditItem(sendItemId = "sendId"),
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
expirationDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Options")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Clear")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(AddSendAction.ClearExpirationDate)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `progressbar should be displayed according to state`() {
|
||||
mutableStateFlow.update {
|
||||
@@ -683,6 +781,7 @@ class AddSendScreenTest : BaseComposeTest() {
|
||||
companion object {
|
||||
private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common(
|
||||
name = "",
|
||||
currentAccessCount = null,
|
||||
maxAccessCount = null,
|
||||
passwordInput = "",
|
||||
noteInput = "",
|
||||
@@ -690,6 +789,7 @@ class AddSendScreenTest : BaseComposeTest() {
|
||||
isDeactivateChecked = false,
|
||||
deletionDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
|
||||
expirationDate = null,
|
||||
sendUrl = null,
|
||||
)
|
||||
|
||||
private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text(
|
||||
|
||||
@@ -7,13 +7,17 @@ import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toSendView
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
@@ -45,18 +49,27 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
||||
private val environmentRepository: EnvironmentRepository = mockk {
|
||||
every { environment } returns Environment.Us
|
||||
}
|
||||
private val vaultRepository: VaultRepository = mockk()
|
||||
private val mutableSendDataStateFlow = MutableStateFlow<DataState<SendView>>(DataState.Loading)
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { getSendStateFlow(any()) } returns mutableSendDataStateFlow
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(ADD_SEND_STATE_EXTENSIONS_PATH)
|
||||
mockkStatic(SEND_VIEW_EXTENSIONS_PATH)
|
||||
mockkStatic(
|
||||
ADD_SEND_STATE_EXTENSIONS_PATH,
|
||||
ADD_SEND_VIEW_EXTENSIONS_PATH,
|
||||
SEND_VIEW_EXTENSIONS_PATH,
|
||||
)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(ADD_SEND_STATE_EXTENSIONS_PATH)
|
||||
unmockkStatic(SEND_VIEW_EXTENSIONS_PATH)
|
||||
unmockkStatic(
|
||||
ADD_SEND_STATE_EXTENSIONS_PATH,
|
||||
ADD_SEND_VIEW_EXTENSIONS_PATH,
|
||||
SEND_VIEW_EXTENSIONS_PATH,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -149,6 +162,90 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick with updateSend success should emit NavigateBack and ShowShareSheet`() =
|
||||
runTest {
|
||||
val sendId = "sendId-1"
|
||||
val viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(name = "input"),
|
||||
)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
addSendType = AddSendType.EditItem(sendId),
|
||||
viewState = viewState,
|
||||
)
|
||||
val mockSendView = createMockSendView(number = 1)
|
||||
every { mockSendView.toViewState(clock, DEFAULT_ENVIRONMENT_URL) } returns viewState
|
||||
every { viewState.toSendView(clock) } returns mockSendView
|
||||
val sendUrl = "www.test.com/send/test"
|
||||
val resultSendView = mockk<SendView> {
|
||||
every { toSendUrl(DEFAULT_ENVIRONMENT_URL) } returns sendUrl
|
||||
every { id } returns sendId
|
||||
}
|
||||
coEvery {
|
||||
vaultRepository.updateSend(sendId = sendId, sendView = mockSendView)
|
||||
} returns UpdateSendResult.Success(sendView = resultSendView)
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
val viewModel = createViewModel(initialState, AddSendType.EditItem(sendId))
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AddSendAction.SaveClick)
|
||||
assertEquals(AddSendEvent.NavigateBack, awaitItem())
|
||||
assertEquals(AddSendEvent.ShowShareSheet(sendUrl), awaitItem())
|
||||
}
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.updateSend(sendId = sendId, sendView = mockSendView)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick with updateSend failure should show error dialog`() = runTest {
|
||||
val sendId = "sendId-1"
|
||||
val viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(name = "input"),
|
||||
)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
addSendType = AddSendType.EditItem(sendId),
|
||||
viewState = viewState,
|
||||
)
|
||||
val mockSendView = mockk<SendView> {
|
||||
every { id } returns sendId
|
||||
}
|
||||
val errorMessage = "Failure"
|
||||
every { mockSendView.toViewState(clock, DEFAULT_ENVIRONMENT_URL) } returns viewState
|
||||
every { viewState.toSendView(clock) } returns mockSendView
|
||||
coEvery {
|
||||
vaultRepository.updateSend(sendId = sendId, sendView = mockSendView)
|
||||
} returns UpdateSendResult.Error(errorMessage = errorMessage)
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
val viewModel = createViewModel(initialState, AddSendType.EditItem(sendId))
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
viewModel.trySendAction(AddSendAction.SaveClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialogState = AddSendState.DialogState.Loading(
|
||||
message = R.string.saving.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialogState = AddSendState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = errorMessage.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.updateSend(sendId = sendId, sendView = mockSendView)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick with blank name should show error dialog`() {
|
||||
val viewModel = createViewModel(DEFAULT_STATE)
|
||||
@@ -276,6 +373,26 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ClearExpirationDate should clear the expiration date`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
DEFAULT_COMMON_STATE.copy(
|
||||
expirationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(initialState)
|
||||
|
||||
viewModel.trySendAction(AddSendAction.ClearExpirationDate)
|
||||
|
||||
assertEquals(
|
||||
// DEFAULT expiration date is null
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ChooseFileClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
@@ -450,7 +567,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
||||
addSendType: AddSendType = AddSendType.AddItem,
|
||||
): AddSendViewModel = AddSendViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set("state", state)
|
||||
set("state", state?.copy(addSendType = addSendType))
|
||||
set(
|
||||
"add_send_item_type",
|
||||
when (addSendType) {
|
||||
@@ -469,11 +586,14 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
||||
companion object {
|
||||
private const val ADD_SEND_STATE_EXTENSIONS_PATH: String =
|
||||
"com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.AddSendStateExtensionsKt"
|
||||
private const val ADD_SEND_VIEW_EXTENSIONS_PATH: String =
|
||||
"com.x8bit.bitwarden.ui.tools.feature.send.addsend.util.SendViewExtensionsKt"
|
||||
private const val SEND_VIEW_EXTENSIONS_PATH: String =
|
||||
"com.x8bit.bitwarden.ui.tools.feature.send.util.SendViewExtensionsKt"
|
||||
|
||||
private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common(
|
||||
name = "",
|
||||
currentAccessCount = null,
|
||||
maxAccessCount = null,
|
||||
passwordInput = "",
|
||||
noteInput = "",
|
||||
@@ -481,6 +601,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
||||
isDeactivateChecked = false,
|
||||
deletionDate = ZonedDateTime.parse("2023-11-03T00:00Z"),
|
||||
expirationDate = null,
|
||||
sendUrl = null,
|
||||
)
|
||||
|
||||
private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text(
|
||||
|
||||
@@ -61,6 +61,7 @@ private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
|
||||
private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common(
|
||||
name = "mockName-1",
|
||||
currentAccessCount = 1,
|
||||
maxAccessCount = 1,
|
||||
passwordInput = "mockPassword-1",
|
||||
noteInput = "mockNotes-1",
|
||||
@@ -68,6 +69,7 @@ private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common(
|
||||
isDeactivateChecked = false,
|
||||
deletionDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
|
||||
expirationDate = ZonedDateTime.parse("2023-10-27T12:00:00Z"),
|
||||
sendUrl = null,
|
||||
)
|
||||
|
||||
private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text(
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addsend.util
|
||||
|
||||
import com.bitwarden.core.SendType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendState
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class SendViewExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `toViewState should create an appropriate ViewState for file type`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.FILE)
|
||||
|
||||
val result = sendView.toViewState(
|
||||
clock = FIXED_CLOCK,
|
||||
baseWebSendUrl = "www.test.com/",
|
||||
)
|
||||
|
||||
assertEquals(DEFAULT_STATE, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should create an appropriate ViewState for text type`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
|
||||
val result = sendView.toViewState(
|
||||
clock = FIXED_CLOCK,
|
||||
baseWebSendUrl = "www.test.com/",
|
||||
)
|
||||
|
||||
assertEquals(DEFAULT_STATE.copy(selectedType = DEFAULT_TEXT_TYPE), result)
|
||||
}
|
||||
}
|
||||
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
private val DEFAULT_COMMON: AddSendState.ViewState.Content.Common =
|
||||
AddSendState.ViewState.Content.Common(
|
||||
name = "mockName-1",
|
||||
currentAccessCount = 1,
|
||||
maxAccessCount = 1,
|
||||
passwordInput = "",
|
||||
noteInput = "mockNotes-1",
|
||||
isHideEmailChecked = false,
|
||||
isDeactivateChecked = false,
|
||||
deletionDate = ZonedDateTime.ofInstant(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
),
|
||||
expirationDate = ZonedDateTime.ofInstant(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
),
|
||||
sendUrl = "www.test.com/mockAccessId-1/mockKey-1",
|
||||
)
|
||||
|
||||
private val DEFAULT_TEXT_TYPE: AddSendState.ViewState.Content.SendType.Text =
|
||||
AddSendState.ViewState.Content.SendType.Text(
|
||||
input = "mockText-1",
|
||||
isHideByDefaultChecked = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_FILE_TYPE: AddSendState.ViewState.Content.SendType.File =
|
||||
AddSendState.ViewState.Content.SendType.File
|
||||
|
||||
private val DEFAULT_STATE: AddSendState.ViewState.Content =
|
||||
AddSendState.ViewState.Content(
|
||||
common = DEFAULT_COMMON,
|
||||
selectedType = DEFAULT_FILE_TYPE,
|
||||
)
|
||||
@@ -18,6 +18,12 @@ import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
||||
/**
|
||||
* A [SemanticsMatcher] used to find editable text nodes.
|
||||
*/
|
||||
val isEditableText: SemanticsMatcher
|
||||
get() = SemanticsMatcher.keyIsDefined(SemanticsProperties.EditableText)
|
||||
|
||||
/**
|
||||
* A [SemanticsMatcher] used to find progressbar nodes.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user