BIT-553: Apply design reskin on current LoginScreen (#90)

This commit is contained in:
Brian Yencho 2023-10-03 15:44:58 -05:00 committed by Álison Fernandes
parent c6911be8d8
commit bc319368ed
10 changed files with 481 additions and 62 deletions

View file

@ -3,28 +3,34 @@ package com.x8bit.bitwarden.ui.auth.feature.login
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenOutlinedButtonWithIcon
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
/**
* The top level composable for the Login screen.
@ -46,73 +52,118 @@ fun LoginScreen(
// TODO Show proper error Dialog
Toast.makeText(context, event.messageRes, Toast.LENGTH_SHORT).show()
}
is LoginEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
}
}
val scrollState = rememberScrollState()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 16.dp, vertical = 32.dp),
.verticalScroll(scrollState),
) {
BitwardenTextField(
modifier = Modifier
.fillMaxWidth()
.testTag("Master password"),
value = state.passwordInput,
onValueChange = { viewModel.trySendAction(LoginAction.PasswordInputChanged(it)) },
label = stringResource(id = R.string.master_password),
BitwardenOverflowTopAppBar(
title = stringResource(id = R.string.app_name),
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(LoginAction.CloseButtonClick) }
},
dropdownMenuItemContent = {
DropdownMenuItem(
text = {
Text(text = stringResource(id = R.string.get_password_hint))
},
onClick = remember(viewModel) {
{ viewModel.trySendAction(LoginAction.MasterPasswordHintClick) }
},
)
},
)
Button(
onClick = { viewModel.trySendAction(LoginAction.LoginButtonClick) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.testTag("Login button"),
enabled = state.isLoginButtonEnabled,
Column(
modifier = Modifier.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(id = R.string.log_in_with_master_password),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodyMedium,
)
}
Button(
onClick = { viewModel.trySendAction(LoginAction.SingleSignOnClick) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.testTag("Single sign-on button"),
enabled = state.isLoginButtonEnabled,
) {
BitwardenPasswordField(
modifier = Modifier
.fillMaxWidth(),
value = state.passwordInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(LoginAction.PasswordInputChanged(it)) }
},
label = stringResource(id = R.string.master_password),
)
// TODO: Need to figure out better handling for very small clickable text (BIT-724)
Text(
text = stringResource(id = R.string.log_in_sso),
text = stringResource(id = R.string.get_password_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.clickable {
viewModel.trySendAction(LoginAction.MasterPasswordHintClick)
}
.padding(
vertical = 4.dp,
horizontal = 16.dp,
),
)
BitwardenFilledButton(
label = stringResource(id = R.string.log_in_with_master_password),
onClick = remember(viewModel) {
{ viewModel.trySendAction(LoginAction.LoginButtonClick) }
},
isEnabled = state.isLoginButtonEnabled,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
)
BitwardenOutlinedButtonWithIcon(
label = stringResource(id = R.string.log_in_sso),
icon = painterResource(id = R.drawable.ic_light_bulb),
onClick =
remember(viewModel) {
{ viewModel.trySendAction(LoginAction.SingleSignOnClick) }
},
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
isEnabled = state.isLoginButtonEnabled,
)
// TODO Get the "login target" from a dropdown (BIT-202)
Text(
text = stringResource(
id = R.string.log_in_attempt_by_x_on_y,
state.emailAddress,
"bitwarden.com",
),
textAlign = TextAlign.Start,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
)
// TODO: Need to figure out better handling for very small clickable text (BIT-724)
Text(
modifier = Modifier
.clickable { viewModel.trySendAction(LoginAction.NotYouButtonClick) },
text = stringResource(id = R.string.not_you),
textAlign = TextAlign.Start,
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
)
}
// TODO Get the "login target" from a dropdown (BIT-202)
Text(
text = stringResource(
id = R.string.log_in_attempt_by_x_on_y,
state.emailAddress,
"bitwarden.com",
),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodySmall,
)
Text(
modifier = Modifier
.clickable { viewModel.trySendAction(LoginAction.NotYouButtonClick) },
text = stringResource(id = R.string.not_you),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodySmall,
)
}
}

View file

@ -55,7 +55,9 @@ class LoginViewModel @Inject constructor(
override fun handleAction(action: LoginAction) {
when (action) {
is LoginAction.CloseButtonClick -> handleCloseButtonClicked()
LoginAction.LoginButtonClick -> handleLoginButtonClicked()
LoginAction.MasterPasswordHintClick -> handleMasterPasswordHintClicked()
LoginAction.NotYouButtonClick -> handleNotYouButtonClicked()
LoginAction.SingleSignOnClick -> handleSingleSignOnClicked()
is LoginAction.PasswordInputChanged -> handlePasswordInputChanged(action)
@ -75,6 +77,10 @@ class LoginViewModel @Inject constructor(
}
}
private fun handleCloseButtonClicked() {
sendEvent(LoginEvent.NavigateBack)
}
private fun handleLoginButtonClicked() {
attemptLogin(captchaToken = null)
}
@ -103,12 +109,18 @@ class LoginViewModel @Inject constructor(
}
}
private fun handleMasterPasswordHintClicked() {
// TODO: Navigate to master password hint screen (BIT-72)
sendEvent(LoginEvent.ShowToast("Not yet implemented."))
}
private fun handleNotYouButtonClicked() {
sendEvent(LoginEvent.NavigateBack)
}
private fun handleSingleSignOnClicked() {
// TODO BIT-204 navigate to single sign on
sendEvent(LoginEvent.ShowToast("Not yet implemented."))
}
private fun handlePasswordInputChanged(action: LoginAction.PasswordInputChanged) {
@ -144,12 +156,22 @@ sealed class LoginEvent {
* Shows an error pop up with a given message
*/
data class ShowErrorDialog(@StringRes val messageRes: Int) : LoginEvent()
/**
* Shows a toast with the given [message].
*/
data class ShowToast(val message: String) : LoginEvent()
}
/**
* Models actions for the login screen.
*/
sealed class LoginAction {
/**
* Indicates that the top-bar close button was clicked.
*/
data object CloseButtonClick : LoginAction()
/**
* Indicates that the Login button has been clicked.
*/
@ -160,6 +182,11 @@ sealed class LoginAction {
*/
data object NotYouButtonClick : LoginAction()
/**
* Indicates that the overflow option for getting a master password hint has been clicked.
*/
data object MasterPasswordHintClick : LoginAction()
/**
* Indicates that the Enterprise single sign-on button has been clicked.
*/

View file

@ -1,6 +1,6 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -28,15 +28,15 @@ fun BitwardenFilledButton(
onClick = onClick,
modifier = modifier,
enabled = isEnabled,
contentPadding = PaddingValues(
vertical = 10.dp,
horizontal = 24.dp,
),
) {
Text(
text = label,
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(
vertical = 10.dp,
horizontal = 24.dp,
),
style = MaterialTheme.typography.labelLarge,
)
}
}

View file

@ -0,0 +1,81 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
/**
* Represents a Bitwarden-styled filled [OutlinedButton] with an icon.
*
* @param label The label for the button.
* @param icon The icon for the button.
* @param onClick The callback when the button is clicked.
* @param modifier The [Modifier] to be applied to the button.
* @param isEnabled Whether or not the button is enabled.
*/
@Composable
fun BitwardenOutlinedButtonWithIcon(
label: String,
icon: Painter,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
OutlinedButton(
onClick = onClick,
modifier = modifier
.semantics(mergeDescendants = true) { },
enabled = isEnabled,
contentPadding = PaddingValues(
vertical = 10.dp,
horizontal = 24.dp,
),
) {
Icon(
painter = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = 8.dp),
)
Text(
text = label,
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,
)
}
}
@Preview
@Composable
private fun BitwardenOutlinedButtonWithIcon_preview_isEnabled() {
BitwardenOutlinedButtonWithIcon(
label = "Label",
icon = painterResource(id = R.drawable.ic_light_bulb),
onClick = {},
isEnabled = true,
)
}
@Preview
@Composable
private fun BitwardenOutlinedButtonWithIcon_preview_isNotEnabled() {
BitwardenOutlinedButtonWithIcon(
label = "Label",
icon = painterResource(id = R.drawable.ic_light_bulb),
onClick = {},
isEnabled = false,
)
}

View file

@ -0,0 +1,96 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Represents a Bitwarden-styled [TopAppBar] that assumes the following components:
*
* - a single navigation control in the upper-left defined by [navigationIcon],
* [navigationIconContentDescription], and [onNavigationIconClick].
* - a [title] in the middle.
* - a single overflow menu in the right with contents defined by the [dropdownMenuItemContent]. It
* is strongly recommended that this content be a stack of [DropdownMenuItem].
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BitwardenOverflowTopAppBar(
title: String,
navigationIcon: Painter,
navigationIconContentDescription: String,
onNavigationIconClick: () -> Unit,
dropdownMenuItemContent: @Composable ColumnScope.() -> Unit,
) {
var isOverflowMenuVisible by remember { mutableStateOf(false) }
TopAppBar(
navigationIcon = {
IconButton(
onClick = { onNavigationIconClick() },
) {
Icon(
painter = navigationIcon,
contentDescription = navigationIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
title = {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
},
actions = {
Box {
IconButton(
onClick = { isOverflowMenuVisible = !isOverflowMenuVisible },
) {
Icon(
painter = painterResource(id = R.drawable.ic_more),
contentDescription = stringResource(id = R.string.more),
tint = MaterialTheme.colorScheme.onSurface,
)
}
DropdownMenu(
expanded = isOverflowMenuVisible,
onDismissRequest = { isOverflowMenuVisible = false },
content = dropdownMenuItemContent,
)
}
},
)
}
@Preview
@Composable
private fun BitwardenOverflowTopAppBar_preview() {
BitwardenTheme {
BitwardenOverflowTopAppBar(
title = "Title",
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = {},
dropdownMenuItemContent = {},
)
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6.192,5.308C5.948,5.064 5.552,5.064 5.308,5.308C5.064,5.552 5.064,5.948 5.308,6.192L11.116,12L5.308,17.808C5.064,18.052 5.064,18.448 5.308,18.692C5.552,18.936 5.948,18.936 6.192,18.692L12,12.884L17.808,18.692C18.052,18.936 18.448,18.936 18.692,18.692C18.936,18.448 18.936,18.052 18.692,17.808L12.884,12L18.692,6.192C18.936,5.948 18.936,5.552 18.692,5.308C18.448,5.064 18.052,5.064 17.808,5.308L12,11.116L6.192,5.308Z"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M10,0.625C6.222,0.625 3.125,3.599 3.125,7.308C3.125,9.503 4.213,11.445 5.882,12.66C6.12,12.833 6.487,13.18 6.789,13.621C7.095,14.066 7.292,14.543 7.292,14.985V17.74C7.292,18.661 8.056,19.375 8.958,19.375H11.042C11.944,19.375 12.708,18.661 12.708,17.74V14.985C12.708,14.543 12.905,14.066 13.211,13.621C13.513,13.18 13.88,12.833 14.118,12.66C15.786,11.445 16.875,9.503 16.875,7.308C16.875,3.599 13.778,0.625 10,0.625ZM4.375,7.308C4.375,4.326 6.875,1.875 10,1.875C13.125,1.875 15.625,4.326 15.625,7.308C15.625,9.076 14.75,10.653 13.382,11.649C13.03,11.906 12.563,12.356 12.18,12.914C11.8,13.468 11.458,14.192 11.458,14.985V15H8.542V14.985C8.542,14.192 8.2,13.468 7.82,12.914C7.437,12.356 6.97,11.906 6.618,11.649C5.25,10.653 4.375,9.076 4.375,7.308ZM8.542,16.25V17.74C8.542,17.934 8.71,18.125 8.958,18.125H11.042C11.29,18.125 11.458,17.934 11.458,17.74V16.25H8.542Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.25,5.75C13.25,6.44 12.69,7 12,7C11.31,7 10.75,6.44 10.75,5.75C10.75,5.06 11.31,4.5 12,4.5C12.69,4.5 13.25,5.06 13.25,5.75Z"
android:fillColor="#1B1B1F"/>
<path
android:pathData="M13.25,12C13.25,12.69 12.69,13.25 12,13.25C11.31,13.25 10.75,12.69 10.75,12C10.75,11.31 11.31,10.75 12,10.75C12.69,10.75 13.25,11.31 13.25,12Z"
android:fillColor="#1B1B1F"/>
<path
android:pathData="M13.25,18.25C13.25,18.94 12.69,19.5 12,19.5C11.31,19.5 10.75,18.94 10.75,18.25C10.75,17.56 11.31,17 12,17C12.69,17 13.25,17.56 13.25,18.25Z"
android:fillColor="#1B1B1F"/>
</vector>

View file

@ -1,8 +1,16 @@
package com.x8bit.bitwarden.ui.auth.feature.login
import android.content.Intent
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.filter
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isPopup
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.performScrollTo
import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
@ -17,6 +25,30 @@ import org.junit.Test
class LoginScreenTest : BaseComposeTest() {
@Test
fun `close button click should send CloseButtonClick action`() {
val viewModel = mockk<LoginViewModel>(relaxed = true) {
every { eventFlow } returns emptyFlow()
every { stateFlow } returns MutableStateFlow(
LoginState(
emailAddress = "",
isLoginButtonEnabled = false,
passwordInput = "",
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
viewModel = viewModel,
)
}
composeTestRule.onNodeWithContentDescription("Close").performClick()
verify {
viewModel.trySendAction(LoginAction.CloseButtonClick)
}
}
@Test
fun `Not you text click should send NotYouButtonClick action`() {
val viewModel = mockk<LoginViewModel>(relaxed = true) {
@ -35,12 +67,71 @@ class LoginScreenTest : BaseComposeTest() {
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Not you?").performClick()
composeTestRule.onNodeWithText("Not you?").performScrollTo().performClick()
verify {
viewModel.trySendAction(LoginAction.NotYouButtonClick)
}
}
@Test
fun `master password hint text click should send MasterPasswordHintClick action`() {
val viewModel = mockk<LoginViewModel>(relaxed = true) {
every { eventFlow } returns emptyFlow()
every { stateFlow } returns MutableStateFlow(
LoginState(
emailAddress = "",
isLoginButtonEnabled = false,
passwordInput = "",
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Get your master password hint").performClick()
verify {
viewModel.trySendAction(LoginAction.MasterPasswordHintClick)
}
}
@Test
fun `master password hint option menu click should send MasterPasswordHintClick action`() {
val viewModel = mockk<LoginViewModel>(relaxed = true) {
every { eventFlow } returns emptyFlow()
every { stateFlow } returns MutableStateFlow(
LoginState(
emailAddress = "",
isLoginButtonEnabled = false,
passwordInput = "",
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
viewModel = viewModel,
)
}
// Confirm dropdown version of item is absent
composeTestRule
.onAllNodesWithText("Get your master password hint")
.filter(hasAnyAncestor(isPopup()))
.assertCountEquals(0)
// Open the overflow menu
composeTestRule.onNodeWithContentDescription("More").performClick()
// Click on the password hint item in the dropdown
composeTestRule
.onAllNodesWithText("Get your master password hint")
.filterToOne(hasAnyAncestor(isPopup()))
.performClick()
verify {
viewModel.trySendAction(LoginAction.MasterPasswordHintClick)
}
}
@Test
fun `password input change should send PasswordInputChanged action`() {
val input = "input"

View file

@ -73,6 +73,23 @@ class LoginViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `CloseButtonClick should emit NavigateBack`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
savedStateHandle = savedStateHandle,
)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.CloseButtonClick)
assertEquals(
LoginEvent.NavigateBack,
awaitItem(),
)
}
}
@Test
fun `LoginButtonClick login returns error should do nothing`() = runTest {
// TODO: handle and display errors (BIT-320)
@ -147,7 +164,25 @@ class LoginViewModelTest : BaseViewModelTest() {
}
@Test
fun `SingleSignOnClick should do nothing`() = runTest {
fun `MasterPasswordHintClick should emit ShowToast`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
savedStateHandle = savedStateHandle,
)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.MasterPasswordHintClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
assertEquals(
LoginEvent.ShowToast("Not yet implemented."),
awaitItem(),
)
}
}
@Test
fun `SingleSignOnClick should emit ShowToast`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
@ -157,6 +192,10 @@ class LoginViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.SingleSignOnClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
assertEquals(
LoginEvent.ShowToast("Not yet implemented."),
awaitItem(),
)
}
}