mirror of
https://github.com/bitwarden/android.git
synced 2026-06-01 10:16:47 -05:00
BIT-956: UI for item listing screen (#356)
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
class VaultItemListingScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToVaultAddItemScreenCalled = false
|
||||
private var onNavigateToVaultItemId: String? = null
|
||||
|
||||
private val mutableEventFlow = MutableSharedFlow<VaultItemListingEvent>(
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<VaultItemListingViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
VaultItemListingScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToVaultItem = { onNavigateToVaultItemId = it },
|
||||
onNavigateToVaultAddItemScreen = { onNavigateToVaultAddItemScreenCalled = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBack event should invoke NavigateBack`() {
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.NavigateBack)
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking back button should send BackClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(label = "Back")
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(VaultItemListingsAction.BackClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search icon click should send SearchIconClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Search vault")
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(VaultItemListingsAction.SearchIconClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `floating action button click should send AddItemClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Add item")
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add an item button click should send AddItemClick action`() {
|
||||
mutableStateFlow.update { it.copy(viewState = VaultItemListingState.ViewState.NoItems) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Add an Item")
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refresh button click should send RefreshClick action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultItemListingState.ViewState.Error(message = "".asText()))
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Try again")
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(VaultItemListingsAction.RefreshClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToAdd VaultItem event should call NavigateToVaultAddItemScreen`() {
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.NavigateToAddVaultItem)
|
||||
assertTrue(onNavigateToVaultAddItemScreenCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToVaultItem event should call NavigateToVaultItemScreen`() {
|
||||
val id = "id4321"
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.NavigateToVaultItem(id = id))
|
||||
assertEquals(id, onNavigateToVaultItemId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `progressbar should be displayed according to state`() {
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
|
||||
composeTestRule
|
||||
.onNode(isProgressBar)
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultItemListingState.ViewState.NoItems)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isProgressBar)
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error text and retry should be displayed according to state`() {
|
||||
val message = "error_message"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(message)
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
mutableStateFlow.update { it.copy(viewState = VaultItemListingState.ViewState.NoItems) }
|
||||
composeTestRule
|
||||
.onNodeWithText(message)
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultItemListingState.ViewState.Error(message.asText()))
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(message)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Add an item button should be displayed according to state`() {
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Add an Item")
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultItemListingState.ViewState.NoItems)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Add an Item")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Trash)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Add an Item")
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Folder(folderId = null))
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Add an Item")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty text should be displayed according to state`() {
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(viewState = VaultItemListingState.ViewState.NoItems)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "There are no items in your vault.")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Trash)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "There are no items in the trash.")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Folder(folderId = null))
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "There are no items in this folder.")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `floating action button should be displayed according to state`() {
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Add item")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Trash)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Add item")
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Folder(folderId = null))
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Add item")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Items text should be displayed according to state`() {
|
||||
val items = "Items"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = items)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(items))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = items)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Items text count should be displayed according to state`() {
|
||||
val items = "Items"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = items)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(items))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = items)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(items, 1.toString())
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
createDisplayItem(number = 2),
|
||||
createDisplayItem(number = 3),
|
||||
createDisplayItem(number = 4),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(items))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = items)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(items, 4.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `displayItems should be displayed according to state`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "mockTitle-1")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "mockSubtitle-1")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on a display item should send ItemClick action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "mockTitle-1")
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemListingsAction.ItemClick("mockId-1"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `topBar title should be displayed according to state`() {
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Logins")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.SecureNote)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Secure notes")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Card)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Cards")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Identity)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Identities")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(itemListingType = VaultItemListingState.ItemListingType.Trash)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Trash")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Folder(
|
||||
folderId = "mockId",
|
||||
folderName = "mockName",
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "mockName")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = VaultItemListingState(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Login,
|
||||
viewState = VaultItemListingState.ViewState.Loading,
|
||||
)
|
||||
|
||||
private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
|
||||
VaultItemListingState.DisplayItem(
|
||||
id = "mockId-$number",
|
||||
title = "mockTitle-$number",
|
||||
subtitle = "mockSubtitle-$number",
|
||||
uri = "mockUri-$number",
|
||||
iconRes = R.drawable.ic_card_item,
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val initialState = createVaultItemListingState()
|
||||
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType = VaultItemListingType.Login,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
initialState, awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BackClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultItemListingsAction.BackClick)
|
||||
assertEquals(VaultItemListingEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SearchIconClick should emit NavigateToVaultSearchScreen`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultItemListingsAction.SearchIconClick)
|
||||
assertEquals(VaultItemListingEvent.NavigateToVaultSearchScreen, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ItemClick should emit NavigateToVaultItem`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultItemListingsAction.ItemClick(id = "mock"))
|
||||
assertEquals(VaultItemListingEvent.NavigateToVaultItem(id = "mock"), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AddVaultItemClick should emit NavigateToAddVaultItem`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultItemListingsAction.AddVaultItemClick)
|
||||
assertEquals(VaultItemListingEvent.NavigateToAddVaultItem, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RefreshClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultItemListingsAction.RefreshClick)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.ShowToast("Not yet implemented".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType: VaultItemListingType,
|
||||
) = SavedStateHandle().apply {
|
||||
set(
|
||||
"vault_item_listing_type",
|
||||
when (vaultItemListingType) {
|
||||
is VaultItemListingType.Card -> "card"
|
||||
is VaultItemListingType.Folder -> "folder"
|
||||
is VaultItemListingType.Identity -> "identity"
|
||||
is VaultItemListingType.Login -> "login"
|
||||
is VaultItemListingType.SecureNote -> "secure_note"
|
||||
is VaultItemListingType.Trash -> "trash"
|
||||
},
|
||||
)
|
||||
set(
|
||||
"id",
|
||||
when (vaultItemListingType) {
|
||||
is VaultItemListingType.Card -> null
|
||||
is VaultItemListingType.Folder -> vaultItemListingType.folderId
|
||||
is VaultItemListingType.Identity -> null
|
||||
is VaultItemListingType.Login -> null
|
||||
is VaultItemListingType.SecureNote -> null
|
||||
is VaultItemListingType.Trash -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createVaultItemListingViewModel(
|
||||
savedStateHandle: SavedStateHandle = initialSavedStateHandle,
|
||||
): VaultItemListingViewModel =
|
||||
VaultItemListingViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun createVaultItemListingState(
|
||||
itemListingType: VaultItemListingState.ItemListingType = VaultItemListingState.ItemListingType.Login,
|
||||
viewState: VaultItemListingState.ViewState = VaultItemListingState.ViewState.Loading,
|
||||
): VaultItemListingState =
|
||||
VaultItemListingState(
|
||||
itemListingType = itemListingType,
|
||||
viewState = viewState,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class VaultItemListingTypeExtensionsTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toItemListingType should transform a VaultItemListingType into a VaultItemListingState ItemListingType`() {
|
||||
val itemListingTypeList = listOf(
|
||||
VaultItemListingType.Folder(folderId = "mock"),
|
||||
VaultItemListingType.Trash,
|
||||
)
|
||||
|
||||
val result = itemListingTypeList.map { it.toItemListingType() }
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
VaultItemListingState.ItemListingType.Folder(folderId = "mock"),
|
||||
VaultItemListingState.ItemListingType.Trash,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user