BIT-814, BIT-815: Add UI for Enterprise Single Sign On screen (#437)

This commit is contained in:
Caleb Derosier
2023-12-27 16:35:48 -06:00
committed by GitHub
parent 1ae0b57574
commit 15a8fa417d
12 changed files with 663 additions and 9 deletions

View File

@@ -0,0 +1,159 @@
package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
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.isDialog
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Before
import org.junit.Test
class EnterpriseSignOnScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private val mutableEventFlow = MutableSharedFlow<EnterpriseSignOnEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<EnterpriseSignOnViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setup() {
composeTestRule.setContent {
EnterpriseSignOnScreen(
onNavigateBack = { onNavigateBackCalled = true },
viewModel = viewModel,
)
}
}
@Test
fun `app bar log in click should send LogInClick action`() {
composeTestRule.onNodeWithText("Log In").performClick()
verify { viewModel.trySendAction(EnterpriseSignOnAction.LogInClick) }
}
@Test
fun `close button click should send CloseButtonClick action`() {
composeTestRule.onNodeWithContentDescription("Close").performClick()
verify {
viewModel.trySendAction(EnterpriseSignOnAction.CloseButtonClick)
}
}
@Test
fun `organization identifier input change should send OrgIdentifierInputChange action`() {
val input = "input"
composeTestRule.onNodeWithText("Organization identifier").performTextInput(input)
verify {
viewModel.trySendAction(EnterpriseSignOnAction.OrgIdentifierInputChange(input))
}
}
@Test
fun `organization identifier should change according to state`() {
composeTestRule
.onNodeWithText("Organization identifier")
.assertTextEquals("Organization identifier", "")
mutableStateFlow.update { it.copy(orgIdentifierInput = "test") }
composeTestRule
.onNodeWithText("Organization identifier")
.assertTextEquals("Organization identifier", "test")
}
@Test
fun `NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateBack)
assertTrue(onNavigateBackCalled)
}
@Test
fun `error dialog should be shown or hidden according to the state`() {
composeTestRule.onNode(isDialog()).assertDoesNotExist()
mutableStateFlow.update {
it.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
message = "Error dialog message".asText(),
),
)
}
composeTestRule.onNode(isDialog()).assertIsDisplayed()
composeTestRule
.onNodeWithText("An error has occurred.")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Error dialog message")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Ok")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Test
fun `loading dialog should be displayed according to state`() {
composeTestRule.onNode(isDialog()).assertDoesNotExist()
composeTestRule.onNodeWithText("Loading").assertDoesNotExist()
mutableStateFlow.update {
it.copy(
dialogState = EnterpriseSignOnState.DialogState.Loading(
message = "Loading".asText(),
),
)
}
composeTestRule
.onNodeWithText("Loading")
.assertIsDisplayed()
.assert(hasAnyAncestor(isDialog()))
}
@Test
fun `error dialog OK click should send DialogDismiss action`() {
mutableStateFlow.update {
DEFAULT_STATE.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
message = "message".asText(),
),
)
}
composeTestRule
.onAllNodesWithText("Ok")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify { viewModel.trySendAction(EnterpriseSignOnAction.DialogDismiss) }
}
companion object {
private val DEFAULT_STATE = EnterpriseSignOnState(
dialogState = null,
orgIdentifierInput = "",
)
}
}

View File

@@ -0,0 +1,129 @@
package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
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 kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
private val savedStateHandle = SavedStateHandle()
@Test
fun `initial state should be correct when not pulling from handle`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
}
}
@Test
fun `initial state should pull from handle when present`() = runTest {
val expectedState = DEFAULT_STATE.copy(
orgIdentifierInput = "test",
)
val viewModel = createViewModel(expectedState)
viewModel.stateFlow.test {
assertEquals(expectedState, awaitItem())
}
}
@Test
fun `CloseButtonClick should emit NavigateBack`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(EnterpriseSignOnAction.CloseButtonClick)
assertEquals(
EnterpriseSignOnEvent.NavigateBack,
awaitItem(),
)
}
}
@Test
fun `LogInClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(EnterpriseSignOnAction.LogInClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
assertEquals(
EnterpriseSignOnEvent.ShowToast("Not yet implemented."),
awaitItem(),
)
}
}
@Test
fun `OrgIdentifierInputChange should update organization identifier`() = runTest {
val input = "input"
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(EnterpriseSignOnAction.OrgIdentifierInputChange(input))
assertEquals(
DEFAULT_STATE.copy(orgIdentifierInput = input),
viewModel.stateFlow.value,
)
}
}
@Test
fun `DialogDismiss should clear the active dialog when DialogState is Error`() {
val initialState = DEFAULT_STATE.copy(
dialogState = EnterpriseSignOnState.DialogState.Error(
message = "Error".asText(),
),
)
val viewModel = createViewModel(initialState)
assertEquals(
initialState,
viewModel.stateFlow.value,
)
viewModel.trySendAction(EnterpriseSignOnAction.DialogDismiss)
assertEquals(
initialState.copy(dialogState = null),
viewModel.stateFlow.value,
)
}
@Test
fun `DialogDismiss should clear the active dialog when DialogState is Loading`() {
val initialState = DEFAULT_STATE.copy(
dialogState = EnterpriseSignOnState.DialogState.Loading(
message = "Loading".asText(),
),
)
val viewModel = createViewModel(initialState)
assertEquals(
initialState,
viewModel.stateFlow.value,
)
viewModel.trySendAction(EnterpriseSignOnAction.DialogDismiss)
assertEquals(
initialState.copy(dialogState = null),
viewModel.stateFlow.value,
)
}
private fun createViewModel(
initialState: EnterpriseSignOnState? = null,
savedStateHandle: SavedStateHandle = SavedStateHandle(
initialState = mapOf("state" to initialState),
),
): EnterpriseSignOnViewModel = EnterpriseSignOnViewModel(
savedStateHandle = savedStateHandle,
)
companion object {
private val DEFAULT_STATE = EnterpriseSignOnState(
dialogState = null,
orgIdentifierInput = "",
)
}
}

View File

@@ -45,6 +45,7 @@ class LoginScreenTest : BaseComposeTest() {
every { startCustomTabsActivity(any()) } returns Unit
}
private var onNavigateBackCalled = false
private var onNavigateToEnterpriseSignOnCalled = false
private val mutableEventFlow = MutableSharedFlow<LoginEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
@@ -59,6 +60,7 @@ class LoginScreenTest : BaseComposeTest() {
composeTestRule.setContent {
LoginScreen(
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToEnterpriseSignOn = { onNavigateToEnterpriseSignOnCalled = true },
viewModel = viewModel,
intentHandler = intentHandler,
)
@@ -265,6 +267,12 @@ class LoginScreenTest : BaseComposeTest() {
mutableEventFlow.tryEmit(LoginEvent.NavigateToCaptcha(mockUri))
verify { intentHandler.startCustomTabsActivity(mockUri) }
}
@Test
fun `NavigateToEnterpriseSignOn should call onNavigateToEnterpriseSignOn`() {
mutableEventFlow.tryEmit(LoginEvent.NavigateToEnterpriseSignOn)
assertTrue(onNavigateToEnterpriseSignOnCalled)
}
}
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(

View File

@@ -318,13 +318,13 @@ class LoginViewModelTest : BaseViewModelTest() {
}
@Test
fun `SingleSignOnClick should emit ShowToast`() = runTest {
fun `SingleSignOnClick should emit NavigateToEnterpriseSignOn`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.SingleSignOnClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
assertEquals(
LoginEvent.ShowToast("Not yet implemented."),
LoginEvent.NavigateToEnterpriseSignOn,
awaitItem(),
)
}