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 android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler 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. * The top level composable for the Login screen.
@@ -46,73 +52,118 @@ fun LoginScreen(
// TODO Show proper error Dialog // TODO Show proper error Dialog
Toast.makeText(context, event.messageRes, Toast.LENGTH_SHORT).show() 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( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 16.dp, vertical = 32.dp), .verticalScroll(scrollState),
) { ) {
BitwardenOverflowTopAppBar(
BitwardenTextField( title = stringResource(id = R.string.app_name),
modifier = Modifier navigationIcon = painterResource(id = R.drawable.ic_close),
.fillMaxWidth() navigationIconContentDescription = stringResource(id = R.string.close),
.testTag("Master password"), onNavigationIconClick = remember(viewModel) {
value = state.passwordInput, { viewModel.trySendAction(LoginAction.CloseButtonClick) }
onValueChange = { viewModel.trySendAction(LoginAction.PasswordInputChanged(it)) }, },
label = stringResource(id = R.string.master_password), dropdownMenuItemContent = {
DropdownMenuItem(
text = {
Text(text = stringResource(id = R.string.get_password_hint))
},
onClick = remember(viewModel) {
{ viewModel.trySendAction(LoginAction.MasterPasswordHintClick) }
},
)
},
) )
Button( Column(
onClick = { viewModel.trySendAction(LoginAction.LoginButtonClick) }, modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.testTag("Login button"),
enabled = state.isLoginButtonEnabled,
) { ) {
Text(
text = stringResource(id = R.string.log_in_with_master_password),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodyMedium,
)
}
Button( BitwardenPasswordField(
onClick = { viewModel.trySendAction(LoginAction.SingleSignOnClick) }, modifier = Modifier
modifier = Modifier .fillMaxWidth(),
.fillMaxWidth() value = state.passwordInput,
.padding(horizontal = 16.dp) onValueChange = remember(viewModel) {
.testTag("Single sign-on button"), { viewModel.trySendAction(LoginAction.PasswordInputChanged(it)) }
enabled = state.isLoginButtonEnabled, },
) { label = stringResource(id = R.string.master_password),
)
// TODO: Need to figure out better handling for very small clickable text (BIT-724)
Text( 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, 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, 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) { override fun handleAction(action: LoginAction) {
when (action) { when (action) {
is LoginAction.CloseButtonClick -> handleCloseButtonClicked()
LoginAction.LoginButtonClick -> handleLoginButtonClicked() LoginAction.LoginButtonClick -> handleLoginButtonClicked()
LoginAction.MasterPasswordHintClick -> handleMasterPasswordHintClicked()
LoginAction.NotYouButtonClick -> handleNotYouButtonClicked() LoginAction.NotYouButtonClick -> handleNotYouButtonClicked()
LoginAction.SingleSignOnClick -> handleSingleSignOnClicked() LoginAction.SingleSignOnClick -> handleSingleSignOnClicked()
is LoginAction.PasswordInputChanged -> handlePasswordInputChanged(action) is LoginAction.PasswordInputChanged -> handlePasswordInputChanged(action)
@@ -75,6 +77,10 @@ class LoginViewModel @Inject constructor(
} }
} }
private fun handleCloseButtonClicked() {
sendEvent(LoginEvent.NavigateBack)
}
private fun handleLoginButtonClicked() { private fun handleLoginButtonClicked() {
attemptLogin(captchaToken = null) 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() { private fun handleNotYouButtonClicked() {
sendEvent(LoginEvent.NavigateBack) sendEvent(LoginEvent.NavigateBack)
} }
private fun handleSingleSignOnClicked() { private fun handleSingleSignOnClicked() {
// TODO BIT-204 navigate to single sign on // TODO BIT-204 navigate to single sign on
sendEvent(LoginEvent.ShowToast("Not yet implemented."))
} }
private fun handlePasswordInputChanged(action: LoginAction.PasswordInputChanged) { private fun handlePasswordInputChanged(action: LoginAction.PasswordInputChanged) {
@@ -144,12 +156,22 @@ sealed class LoginEvent {
* Shows an error pop up with a given message * Shows an error pop up with a given message
*/ */
data class ShowErrorDialog(@StringRes val messageRes: Int) : LoginEvent() 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. * Models actions for the login screen.
*/ */
sealed class LoginAction { sealed class LoginAction {
/**
* Indicates that the top-bar close button was clicked.
*/
data object CloseButtonClick : LoginAction()
/** /**
* Indicates that the Login button has been clicked. * Indicates that the Login button has been clicked.
*/ */
@@ -160,6 +182,11 @@ sealed class LoginAction {
*/ */
data object NotYouButtonClick : 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. * Indicates that the Enterprise single sign-on button has been clicked.
*/ */

View File

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

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 package com.x8bit.bitwarden.ui.auth.feature.login
import android.content.Intent 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.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest 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.IntentHandler
@@ -17,6 +25,30 @@ import org.junit.Test
class LoginScreenTest : BaseComposeTest() { 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 @Test
fun `Not you text click should send NotYouButtonClick action`() { fun `Not you text click should send NotYouButtonClick action`() {
val viewModel = mockk<LoginViewModel>(relaxed = true) { val viewModel = mockk<LoginViewModel>(relaxed = true) {
@@ -35,12 +67,71 @@ class LoginScreenTest : BaseComposeTest() {
viewModel = viewModel, viewModel = viewModel,
) )
} }
composeTestRule.onNodeWithText("Not you?").performClick() composeTestRule.onNodeWithText("Not you?").performScrollTo().performClick()
verify { verify {
viewModel.trySendAction(LoginAction.NotYouButtonClick) 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 @Test
fun `password input change should send PasswordInputChanged action`() { fun `password input change should send PasswordInputChanged action`() {
val input = "input" 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 @Test
fun `LoginButtonClick login returns error should do nothing`() = runTest { fun `LoginButtonClick login returns error should do nothing`() = runTest {
// TODO: handle and display errors (BIT-320) // TODO: handle and display errors (BIT-320)
@@ -147,7 +164,25 @@ class LoginViewModelTest : BaseViewModelTest() {
} }
@Test @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( val viewModel = LoginViewModel(
authRepository = mockk { authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf() every { captchaTokenResultFlow } returns flowOf()
@@ -157,6 +192,10 @@ class LoginViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.SingleSignOnClick) viewModel.actionChannel.trySend(LoginAction.SingleSignOnClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value) assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
assertEquals(
LoginEvent.ShowToast("Not yet implemented."),
awaitItem(),
)
} }
} }