Populate the send screen with real data (#488)

This commit is contained in:
David Perez
2024-01-04 11:30:50 -06:00
committed by GitHub
parent 6751cc2e1e
commit 2fc19ea6e2
16 changed files with 1023 additions and 34 deletions

View File

@@ -9,7 +9,10 @@ import java.time.ZonedDateTime
/**
* Create a mock [SendView] with a given [number].
*/
fun createMockSendView(number: Int): SendView =
fun createMockSendView(
number: Int,
type: SendType = SendType.FILE,
): SendView =
SendView(
id = "mockId-$number",
accessId = "mockAccessId-$number",
@@ -17,7 +20,7 @@ fun createMockSendView(number: Int): SendView =
notes = "mockNotes-$number",
key = "mockKey-$number",
password = "mockPassword-$number",
type = SendType.FILE,
type = type,
file = createMockFileView(number = number),
text = createMockTextView(number = number),
maxAccessCount = 1u,

View File

@@ -1,24 +1,39 @@
package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasScrollToNodeAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.isPopup
import androidx.compose.ui.test.onAllNodesWithText
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.performScrollToNode
import androidx.core.net.toUri
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.IntentHandler
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
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
@@ -26,7 +41,12 @@ class SendScreenTest : BaseComposeTest() {
private var onNavigateToNewSendCalled = false
private val intentHandler = mockk<IntentHandler>()
private val clipboardManager = mockk<ClipboardManager> {
every { setText(any()) } just runs
}
private val intentHandler = mockk<IntentHandler> {
every { launchUri(any()) } just runs
}
private val mutableEventFlow = bufferedMutableSharedFlow<SendEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<SendViewModel>(relaxed = true) {
@@ -39,12 +59,36 @@ class SendScreenTest : BaseComposeTest() {
composeTestRule.setContent {
SendScreen(
viewModel = viewModel,
onNavigateAddSend = { onNavigateToNewSendCalled = true },
onNavigateToAddSend = { onNavigateToNewSendCalled = true },
clipboardManager = clipboardManager,
intentHandler = intentHandler,
)
}
}
@Test
fun `on CopyToClipboard should call setText on the clipboardManager`() {
val text = "copy text"
mutableEventFlow.tryEmit(SendEvent.CopyToClipboard(text.asText()))
verify {
clipboardManager.setText(text.toAnnotatedString())
}
}
@Test
fun `on NavigateToNewSend should call onNavigateToNewSend`() {
mutableEventFlow.tryEmit(SendEvent.NavigateNewSend)
assertTrue(onNavigateToNewSendCalled)
}
@Test
fun `on NavigateToAboutSend should call launchUri on intentHandler`() {
mutableEventFlow.tryEmit(SendEvent.NavigateToAboutSend)
verify {
intentHandler.launchUri("https://bitwarden.com/products/send".toUri())
}
}
@Test
fun `on overflow item click should display menu`() {
composeTestRule
@@ -131,7 +175,7 @@ class SendScreenTest : BaseComposeTest() {
composeTestRule.onNodeWithContentDescription("Add item").assertDoesNotExist()
mutableStateFlow.update {
it.copy(viewState = SendState.ViewState.Content)
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
}
composeTestRule.onNodeWithContentDescription("Add item").assertIsDisplayed()
}
@@ -166,12 +210,6 @@ class SendScreenTest : BaseComposeTest() {
verify { viewModel.trySendAction(SendAction.SearchClick) }
}
@Test
fun `on NavigateToNewSend should call onNavigateToNewSend`() {
mutableEventFlow.tryEmit(SendEvent.NavigateNewSend)
assert(onNavigateToNewSendCalled)
}
@Test
fun `progressbar should be displayed according to state`() {
mutableStateFlow.update {
@@ -190,7 +228,7 @@ class SendScreenTest : BaseComposeTest() {
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
mutableStateFlow.update {
it.copy(viewState = SendState.ViewState.Content)
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
}
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
}
@@ -216,8 +254,265 @@ class SendScreenTest : BaseComposeTest() {
viewModel.trySendAction(SendAction.RefreshClick)
}
}
@Test
fun `text type count should be updated according to state`() {
val rowText = "Text"
mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
}
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText))
composeTestRule
.onAllNodes(hasText(rowText))
.filterToOne(hasClickAction())
.assertTextEquals(rowText, 1.toString())
mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE.copy(textTypeCount = 3))
}
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText))
composeTestRule
.onAllNodes(hasText(rowText))
.filterToOne(hasClickAction())
.assertTextEquals(rowText, 3.toString())
}
@Test
fun `text type row click should send TextTypeClick`() {
val rowText = "Text"
mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
}
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText))
composeTestRule
.onAllNodes(hasText(rowText))
.filterToOne(hasClickAction())
.performClick()
verify {
viewModel.trySendAction(SendAction.TextTypeClick)
}
}
@Test
fun `file type count should be updated according to state`() {
val rowText = "File"
mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
}
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText))
composeTestRule
.onAllNodes(hasText(rowText))
.filterToOne(hasClickAction())
.assertTextEquals(rowText, 1.toString())
mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE.copy(fileTypeCount = 3))
}
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText))
composeTestRule
.onAllNodes(hasText(rowText))
.filterToOne(hasClickAction())
.assertTextEquals(rowText, 3.toString())
}
@Test
fun `file type row click should send FileTypeClick`() {
val rowText = "File"
mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
}
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText))
composeTestRule
.onAllNodes(hasText(rowText))
.filterToOne(hasClickAction())
.performClick()
verify {
viewModel.trySendAction(SendAction.FileTypeClick)
}
}
@Test
fun `on send item click should send SendClick`() {
val rowText = "mockName-1"
mutableStateFlow.update {
it.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
}
composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(rowText))
composeTestRule
.onAllNodes(hasText(rowText))
.filterToOne(hasClickAction())
.performClick()
verify {
viewModel.trySendAction(SendAction.SendClick(DEFAULT_SEND_ITEM))
}
}
@Test
fun `on send item overflow click should display dialog`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNode(isDialog())
.onChildren()
.filterToOne(hasText(DEFAULT_SEND_ITEM.name))
.assertIsDisplayed()
}
@Test
fun `on send item overflow dialog edit click should send SendClick`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Edit")
.assert(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(SendAction.SendClick(DEFAULT_SEND_ITEM))
}
composeTestRule.assertNoDialogExists()
}
@Test
fun `on send item overflow dialog copy click should send CopyClick`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Copy link")
.assert(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(SendAction.CopyClick(DEFAULT_SEND_ITEM))
}
composeTestRule.assertNoDialogExists()
}
@Test
fun `on send item overflow dialog share link click should send ShareClick`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Share link")
.assert(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(SendAction.ShareClick(DEFAULT_SEND_ITEM))
}
composeTestRule.assertNoDialogExists()
}
@Test
fun `on send item overflow dialog cancel click should close the dialog`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Cancel")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
}
}
private val DEFAULT_STATE: SendState = SendState(
viewState = SendState.ViewState.Loading,
)
private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem =
SendState.ViewState.Content.SendItem(
id = "mockId-1",
name = "mockName-1",
deletionDate = "1",
type = SendState.ViewState.Content.SendItem.Type.FILE,
)
private val DEFAULT_CONTENT_VIEW_STATE: SendState.ViewState.Content = SendState.ViewState.Content(
textTypeCount = 1,
fileTypeCount = 1,
sendItems = listOf(
DEFAULT_SEND_ITEM,
SendState.ViewState.Content.SendItem(
id = "mockId-2",
name = "mockName-2",
deletionDate = "1",
type = SendState.ViewState.Content.SendItem.Type.TEXT,
),
),
)

View File

@@ -110,6 +110,54 @@ class SendViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `CopyClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
val sendItem = mockk<SendState.ViewState.Content.SendItem>()
viewModel.eventFlow.test {
viewModel.trySendAction(SendAction.CopyClick(sendItem))
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
}
}
@Test
fun `SendClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
val sendItem = mockk<SendState.ViewState.Content.SendItem>()
viewModel.eventFlow.test {
viewModel.trySendAction(SendAction.SendClick(sendItem))
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
}
}
@Test
fun `ShareClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
val sendItem = mockk<SendState.ViewState.Content.SendItem>()
viewModel.eventFlow.test {
viewModel.trySendAction(SendAction.ShareClick(sendItem))
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
}
}
@Test
fun `FileTypeClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SendAction.FileTypeClick)
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
}
}
@Test
fun `TextTypeClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SendAction.TextTypeClick)
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
}
}
@Test
fun `VaultRepository SendData Error should update view state to Error`() {
val viewModel = createViewModel()
@@ -129,7 +177,7 @@ class SendViewModelTest : BaseViewModelTest() {
@Test
fun `VaultRepository SendData Loaded should update view state`() {
val viewModel = createViewModel()
val viewState = SendState.ViewState.Content
val viewState = mockk<SendState.ViewState.Content>()
val sendData = mockk<SendData> {
every { toViewState() } returns viewState
}
@@ -172,7 +220,7 @@ class SendViewModelTest : BaseViewModelTest() {
@Test
fun `VaultRepository SendData Pending should update view state`() {
val viewModel = createViewModel()
val viewState = SendState.ViewState.Content
val viewState = mockk<SendState.ViewState.Content>()
val sendData = mockk<SendData> {
every { toViewState() } returns viewState
}

View File

@@ -1,13 +1,29 @@
package com.x8bit.bitwarden.ui.tools.feature.send.util
import com.bitwarden.core.SendType
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.ui.tools.feature.send.SendState
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.TimeZone
class SendDataExtensionsTest {
@BeforeEach
fun setup() {
// Setting the timezone so the tests pass consistently no matter the environment.
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
}
@AfterEach
fun tearDown() {
// Clearing the timezone after the test.
TimeZone.setDefault(null)
}
@Test
fun `toViewState should return Empty when SendData is empty`() {
val sendData = SendData(emptyList())
@@ -20,12 +36,33 @@ class SendDataExtensionsTest {
@Test
fun `toViewState should return Content when SendData is not empty`() {
val list = listOf(
createMockSendView(number = 1),
createMockSendView(number = 1, type = SendType.FILE),
createMockSendView(number = 2, type = SendType.TEXT),
)
val sendData = SendData(list)
val result = sendData.toViewState()
assertEquals(SendState.ViewState.Content, result)
assertEquals(
SendState.ViewState.Content(
textTypeCount = 1,
fileTypeCount = 1,
sendItems = listOf(
SendState.ViewState.Content.SendItem(
id = "mockId-1",
name = "mockName-1",
deletionDate = "Oct 27, 2023, 12:00 PM",
type = SendState.ViewState.Content.SendItem.Type.FILE,
),
SendState.ViewState.Content.SendItem(
id = "mockId-2",
name = "mockName-2",
deletionDate = "Oct 27, 2023, 12:00 PM",
type = SendState.ViewState.Content.SendItem.Type.TEXT,
),
),
),
result,
)
}
}