mirror of
https://github.com/bitwarden/android.git
synced 2026-06-03 03:06:21 -05:00
BIT-814, BIT-815: Add UI for Enterprise Single Sign On screen (#437)
This commit is contained in:
@@ -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 = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user