Modify Add Sends UI to allow for editing existing sends (#575)

This commit is contained in:
David Perez
2024-01-11 15:44:47 -06:00
committed by GitHub
parent 6598eba398
commit f1ea1bfa02
10 changed files with 677 additions and 55 deletions

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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,
)

View File

@@ -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.
*/