BIT-844: Move to Organization UI (#638)

This commit is contained in:
Ramsey Smith
2024-01-16 15:53:45 -07:00
committed by GitHub
parent b30a4368d8
commit 110392e94c
7 changed files with 791 additions and 6 deletions

View File

@@ -1,13 +1,28 @@
package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onLast
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 com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.createMockOrganizationList
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -52,10 +67,191 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() {
)
}
}
@Test
fun `clicking move button should send MoveClick action`() {
composeTestRule
.onNodeWithText(text = "Move")
.performClick()
verify {
viewModel.trySendAction(
VaultMoveToOrganizationAction.MoveClick,
)
}
}
@Test
fun `selecting an organization should send OrganizationSelect action`() {
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 1")
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "Organization 2")
.onLast()
.performScrollTo()
.performClick()
verify {
viewModel.trySendAction(
VaultMoveToOrganizationAction.OrganizationSelect(
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "2",
name = "Organization 2",
collections = listOf(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "1",
name = "Collection 1",
isSelected = true,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "2",
name = "Collection 2",
isSelected = false,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "3",
name = "Collection 3",
isSelected = false,
),
),
),
),
)
}
}
@Test
fun `the organization option field should display according to state`() {
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 1")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "2",
),
)
}
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 2")
.assertIsDisplayed()
}
@Test
fun `selecting a collection should send CollectionSelect action`() {
composeTestRule
.onNodeWithText(text = "Collection 2")
.performClick()
verify {
viewModel.trySendAction(
VaultMoveToOrganizationAction.CollectionSelect(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "2",
name = "Collection 2",
isSelected = false,
),
),
)
}
}
@Test
fun `the collection list should display according to state`() {
composeTestRule
.onNodeWithText("Collection 1")
.assertIsOn()
composeTestRule
.onNodeWithText("Collection 2")
.assertIsOff()
composeTestRule
.onNodeWithText("Collection 3")
.assertIsOff()
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList()
.map { organization ->
organization.copy(
collections =
if (organization.id == "1") {
organization
.collections
.map { collection ->
collection.copy(isSelected = collection.id != "1")
}
} else {
organization.collections
},
)
},
selectedOrganizationId = "1",
),
)
}
composeTestRule
.onNodeWithText("Collection 1")
.assertIsOff()
composeTestRule
.onNodeWithText("Collection 2")
.assertIsOn()
composeTestRule
.onNodeWithText("Collection 3")
.assertIsOn()
}
@Test
fun `loading dialog should display according to state`() {
composeTestRule
.onAllNodesWithText("loading")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsNotDisplayed()
mutableStateFlow.update {
it.copy(
dialogState = VaultMoveToOrganizationState.DialogState.Loading("loading".asText()),
)
}
composeTestRule
.onAllNodesWithText("loading")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Test
fun `error dialog should display according to state`() {
composeTestRule
.onAllNodesWithText("error")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsNotDisplayed()
mutableStateFlow.update {
it.copy(
dialogState = VaultMoveToOrganizationState.DialogState.Error("error".asText()),
)
}
composeTestRule
.onAllNodesWithText("error")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
}
private fun createVaultMoveToOrganizationState(): VaultMoveToOrganizationState =
VaultMoveToOrganizationState(
vaultItemId = "mockId",
viewState = VaultMoveToOrganizationState.ViewState.Content,
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "1",
),
dialogState = null,
)

View File

@@ -2,7 +2,10 @@ package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.createMockOrganizationList
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -50,6 +53,119 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `OrganizationSelect should update selected Organization`() = runTest {
val viewModel = createViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = createVaultMoveToOrganizationState(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "1",
),
),
),
)
val action = VaultMoveToOrganizationAction.OrganizationSelect(
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "3",
name = "Organization 3",
collections = emptyList(),
),
)
val expectedState = createVaultMoveToOrganizationState(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "3",
),
)
viewModel.actionChannel.trySend(action)
assertEquals(
expectedState,
viewModel.stateFlow.value,
)
}
@Test
fun `CollectionSelect should update selected Collections`() = runTest {
val viewModel = createViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = createVaultMoveToOrganizationState(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "1",
),
),
),
)
val selectCollection3Action = VaultMoveToOrganizationAction.CollectionSelect(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "3",
name = "Collection 3",
isSelected = false,
),
)
val unselectCollection1Action = VaultMoveToOrganizationAction.CollectionSelect(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "1",
name = "Collection 1",
isSelected = true,
),
)
val expectedState = createVaultMoveToOrganizationState(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList()
.map { organization ->
organization.copy(
collections =
if (organization.id == "1") {
organization.collections.map {
it.copy(isSelected = it.id == "3")
}
} else {
organization.collections
},
)
},
selectedOrganizationId = "1",
),
)
viewModel.actionChannel.trySend(selectCollection3Action)
viewModel.actionChannel.trySend(unselectCollection1Action)
assertEquals(
expectedState,
viewModel.stateFlow.value,
)
}
@Test
fun `MoveClick should show dialog, and remove it once an item is moved`() = runTest {
val viewModel = createViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = initialState,
),
)
viewModel.stateFlow.test {
assertEquals(initialState, awaitItem())
viewModel.actionChannel.trySend(VaultMoveToOrganizationAction.MoveClick)
assertEquals(
initialState.copy(
dialogState = VaultMoveToOrganizationState.DialogState.Loading(
message = R.string.saving.asText(),
),
),
awaitItem(),
)
assertEquals(
initialState,
awaitItem(),
)
}
}
private fun createViewModel(
savedStateHandle: SavedStateHandle = initialSavedStateHandle,
): VaultMoveToOrganizationViewModel =
@@ -67,11 +183,13 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
private fun createVaultMoveToOrganizationState(
viewState: VaultMoveToOrganizationState.ViewState = VaultMoveToOrganizationState.ViewState.Content,
viewState: VaultMoveToOrganizationState.ViewState = VaultMoveToOrganizationState.ViewState.Empty,
vaultItemId: String = "mockId",
dialogState: VaultMoveToOrganizationState.DialogState? = null,
): VaultMoveToOrganizationState =
VaultMoveToOrganizationState(
vaultItemId = vaultItemId,
viewState = viewState,
dialogState = dialogState,
)
}

View File

@@ -0,0 +1,58 @@
package com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.VaultMoveToOrganizationState
/**
* Creates a list of mock organizations.
*/
fun createMockOrganizationList():
List<VaultMoveToOrganizationState.ViewState.Content.Organization> =
listOf(
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "1",
name = "Organization 1",
collections = listOf(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "1",
name = "Collection 1",
isSelected = true,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "2",
name = "Collection 2",
isSelected = false,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "3",
name = "Collection 3",
isSelected = false,
),
),
),
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "2",
name = "Organization 2",
collections = listOf(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "1",
name = "Collection 1",
isSelected = true,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "2",
name = "Collection 2",
isSelected = false,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "3",
name = "Collection 3",
isSelected = false,
),
),
),
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "3",
name = "Organization 3",
collections = emptyList(),
),
)