mirror of
https://github.com/bitwarden/android.git
synced 2024-12-18 07:11:51 +03:00
BIT-141 Setup basic Login and Landing screens (#40)
Co-authored-by: Caleb Derosier <caleb@livefront.com>
This commit is contained in:
parent
d8de9bb753
commit
024376b0d2
13 changed files with 727 additions and 30 deletions
|
@ -8,7 +8,10 @@ import androidx.navigation.navigation
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestinations
|
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestinations
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount
|
import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.landing.LANDING_ROUTE
|
import com.x8bit.bitwarden.ui.auth.feature.landing.LANDING_ROUTE
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.landing.landingDestination
|
import com.x8bit.bitwarden.ui.auth.feature.landing.landingDestinations
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.landing.navigateToLanding
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.login.loginDestinations
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.login.navigateToLogin
|
||||||
|
|
||||||
const val AUTH_ROUTE: String = "auth"
|
const val AUTH_ROUTE: String = "auth"
|
||||||
|
|
||||||
|
@ -21,8 +24,12 @@ fun NavGraphBuilder.authDestinations(navController: NavHostController) {
|
||||||
route = AUTH_ROUTE,
|
route = AUTH_ROUTE,
|
||||||
) {
|
) {
|
||||||
createAccountDestinations()
|
createAccountDestinations()
|
||||||
landingDestination(
|
landingDestinations(
|
||||||
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
||||||
|
onNavigateToLogin = { emailAddress -> navController.navigateToLogin(emailAddress) },
|
||||||
|
)
|
||||||
|
loginDestinations(
|
||||||
|
onNavigateToLanding = { navController.navigateToLanding() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,14 @@ fun NavController.navigateToLanding(navOptions: NavOptions? = null) {
|
||||||
/**
|
/**
|
||||||
* Add the Landing screen to the nav graph.
|
* Add the Landing screen to the nav graph.
|
||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.landingDestination(onNavigateToCreateAccount: () -> Unit) {
|
fun NavGraphBuilder.landingDestinations(
|
||||||
|
onNavigateToCreateAccount: () -> Unit,
|
||||||
|
onNavigateToLogin: (String) -> Unit,
|
||||||
|
) {
|
||||||
composable(route = LANDING_ROUTE) {
|
composable(route = LANDING_ROUTE) {
|
||||||
LandingScreen(onNavigateToCreateAccount = onNavigateToCreateAccount)
|
LandingScreen(
|
||||||
|
onNavigateToCreateAccount = onNavigateToCreateAccount,
|
||||||
|
onNavigateToLogin = onNavigateToLogin,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,12 +34,14 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
fun LandingScreen(
|
fun LandingScreen(
|
||||||
onNavigateToCreateAccount: () -> Unit,
|
onNavigateToCreateAccount: () -> Unit,
|
||||||
|
onNavigateToLogin: (emailAddress: String) -> Unit,
|
||||||
viewModel: LandingViewModel = hiltViewModel(),
|
viewModel: LandingViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
LandingEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
|
LandingEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
|
||||||
|
is LandingEvent.NavigateToLogin -> onNavigateToLogin(event.emailAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,8 +70,10 @@ fun LandingScreen(
|
||||||
)
|
)
|
||||||
|
|
||||||
BitwardenTextField(
|
BitwardenTextField(
|
||||||
|
modifier = Modifier.testTag("Email address"),
|
||||||
|
value = state.emailInput,
|
||||||
|
onValueChange = { viewModel.trySendAction(LandingAction.EmailInputChanged(it)) },
|
||||||
label = stringResource(id = R.string.email_address),
|
label = stringResource(id = R.string.email_address),
|
||||||
initialValue = state.initialEmailAddress,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
@ -87,6 +91,7 @@ fun LandingScreen(
|
||||||
)
|
)
|
||||||
|
|
||||||
Switch(
|
Switch(
|
||||||
|
modifier = Modifier.testTag("Remember me"),
|
||||||
checked = state.isRememberMeEnabled,
|
checked = state.isRememberMeEnabled,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
viewModel.trySendAction(LandingAction.RememberMeToggle(it))
|
viewModel.trySendAction(LandingAction.RememberMeToggle(it))
|
||||||
|
@ -95,7 +100,9 @@ fun LandingScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.trySendAction(LandingAction.ContinueButtonClick) },
|
onClick = {
|
||||||
|
viewModel.trySendAction(LandingAction.ContinueButtonClick)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
|
|
|
@ -1,34 +1,55 @@
|
||||||
package com.x8bit.bitwarden.ui.auth.feature.landing
|
package com.x8bit.bitwarden.ui.auth.feature.landing
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages application state for the initial landing screen.
|
* Manages application state for the initial landing screen.
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LandingViewModel @Inject constructor() :
|
class LandingViewModel @Inject constructor(
|
||||||
BaseViewModel<LandingState, LandingEvent, LandingAction>(
|
savedStateHandle: SavedStateHandle,
|
||||||
initialState = LandingState(
|
) : BaseViewModel<LandingState, LandingEvent, LandingAction>(
|
||||||
initialEmailAddress = "",
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
?: LandingState(
|
||||||
|
emailInput = "",
|
||||||
isContinueButtonEnabled = true,
|
isContinueButtonEnabled = true,
|
||||||
isRememberMeEnabled = false,
|
isRememberMeEnabled = false,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
// As state updates, write to saved state handle:
|
||||||
|
stateFlow
|
||||||
|
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleAction(action: LandingAction) {
|
override fun handleAction(action: LandingAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
LandingAction.ContinueButtonClick -> handleContinueButtonClicked()
|
is LandingAction.ContinueButtonClick -> handleContinueButtonClicked()
|
||||||
LandingAction.CreateAccountClick -> handleCreateAccountClicked()
|
LandingAction.CreateAccountClick -> handleCreateAccountClicked()
|
||||||
is LandingAction.RememberMeToggle -> handleRememberMeToggled(action)
|
is LandingAction.RememberMeToggle -> handleRememberMeToggled(action)
|
||||||
|
is LandingAction.EmailInputChanged -> handleEmailInputUpdated(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleEmailInputUpdated(action: LandingAction.EmailInputChanged) {
|
||||||
|
mutableStateFlow.update { it.copy(emailInput = action.input) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleContinueButtonClicked() {
|
private fun handleContinueButtonClicked() {
|
||||||
mutableStateFlow.value = mutableStateFlow.value.copy(
|
sendEvent(LandingEvent.NavigateToLogin(mutableStateFlow.value.emailInput))
|
||||||
isContinueButtonEnabled = false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCreateAccountClicked() {
|
private fun handleCreateAccountClicked() {
|
||||||
|
@ -36,20 +57,19 @@ class LandingViewModel @Inject constructor() :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRememberMeToggled(action: LandingAction.RememberMeToggle) {
|
private fun handleRememberMeToggled(action: LandingAction.RememberMeToggle) {
|
||||||
mutableStateFlow.value = mutableStateFlow.value.copy(
|
mutableStateFlow.update { it.copy(isRememberMeEnabled = action.isChecked) }
|
||||||
isRememberMeEnabled = action.isChecked,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models state of the landing screen.
|
* Models state of the landing screen.
|
||||||
*/
|
*/
|
||||||
|
@Parcelize
|
||||||
data class LandingState(
|
data class LandingState(
|
||||||
val initialEmailAddress: String,
|
val emailInput: String,
|
||||||
val isContinueButtonEnabled: Boolean,
|
val isContinueButtonEnabled: Boolean,
|
||||||
val isRememberMeEnabled: Boolean,
|
val isRememberMeEnabled: Boolean,
|
||||||
)
|
) : Parcelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models events for the landing screen.
|
* Models events for the landing screen.
|
||||||
|
@ -59,6 +79,13 @@ sealed class LandingEvent {
|
||||||
* Navigates to the Create Account screen.
|
* Navigates to the Create Account screen.
|
||||||
*/
|
*/
|
||||||
data object NavigateToCreateAccount : LandingEvent()
|
data object NavigateToCreateAccount : LandingEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the Login screen with the given email address.
|
||||||
|
*/
|
||||||
|
data class NavigateToLogin(
|
||||||
|
val emailAddress: String,
|
||||||
|
) : LandingEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,4 +108,11 @@ sealed class LandingAction {
|
||||||
data class RememberMeToggle(
|
data class RememberMeToggle(
|
||||||
val isChecked: Boolean,
|
val isChecked: Boolean,
|
||||||
) : LandingAction()
|
) : LandingAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the input on the email field has changed.
|
||||||
|
*/
|
||||||
|
data class EmailInputChanged(
|
||||||
|
val input: String,
|
||||||
|
) : LandingAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
|
||||||
|
private const val EMAIL_ADDRESS: String = "email_address"
|
||||||
|
private const val LOGIN_ROUTE: String = "login/{$EMAIL_ADDRESS}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to retrieve login arguments from the [SavedStateHandle].
|
||||||
|
*/
|
||||||
|
class LoginArgs(val emailAddress: String) {
|
||||||
|
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||||
|
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the login screen with the given email address.
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToLogin(
|
||||||
|
emailAddress: String,
|
||||||
|
navOptions: NavOptions? = null,
|
||||||
|
) {
|
||||||
|
this.navigate("login/$emailAddress", navOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the Login screen to the nav graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.loginDestinations(
|
||||||
|
onNavigateToLanding: () -> Unit,
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
route = LOGIN_ROUTE,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
LoginScreen(
|
||||||
|
onNavigateToLanding = { onNavigateToLanding() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||||
|
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top level composable for the Login screen.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
fun LoginScreen(
|
||||||
|
onNavigateToLanding: () -> Unit,
|
||||||
|
viewModel: LoginViewModel = viewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
LoginEvent.NavigateToLanding -> onNavigateToLanding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 32.dp),
|
||||||
|
) {
|
||||||
|
|
||||||
|
BitwardenTextField(
|
||||||
|
modifier = Modifier.testTag("Master password"),
|
||||||
|
value = state.passwordInput,
|
||||||
|
onValueChange = { viewModel.trySendAction(LoginAction.PasswordInputChanged(it)) },
|
||||||
|
label = stringResource(id = R.string.master_password),
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.trySendAction(LoginAction.LoginButtonClick) },
|
||||||
|
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(
|
||||||
|
onClick = { viewModel.trySendAction(LoginAction.SingleSignOnClick) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.testTag("Single sign-on button"),
|
||||||
|
enabled = state.isLoginButtonEnabled,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.enterprise_single_sign_on),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.logging_in_as, state.emailAddress),
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages application state for the initial login screen.
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class LoginViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
) : BaseViewModel<LoginState, LoginEvent, LoginAction>(
|
||||||
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
?: LoginState(
|
||||||
|
emailAddress = LoginArgs(savedStateHandle).emailAddress,
|
||||||
|
isLoginButtonEnabled = false,
|
||||||
|
passwordInput = "",
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
// As state updates, write to saved state handle:
|
||||||
|
stateFlow
|
||||||
|
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: LoginAction) {
|
||||||
|
when (action) {
|
||||||
|
LoginAction.LoginButtonClick -> handleLoginButtonClicked()
|
||||||
|
LoginAction.NotYouButtonClick -> handleNotYouButtonClicked()
|
||||||
|
LoginAction.SingleSignOnClick -> handleSingleSignOnClicked()
|
||||||
|
is LoginAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleLoginButtonClicked() {
|
||||||
|
// TODO BIT-133 make login request and allow user to login
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNotYouButtonClicked() {
|
||||||
|
sendEvent(LoginEvent.NavigateToLanding)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSingleSignOnClicked() {
|
||||||
|
// TODO BIT-204 navigate to single sign on
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePasswordInputChanged(action: LoginAction.PasswordInputChanged) {
|
||||||
|
mutableStateFlow.update { it.copy(passwordInput = action.input) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models state of the login screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class LoginState(
|
||||||
|
val passwordInput: String,
|
||||||
|
val emailAddress: String,
|
||||||
|
val isLoginButtonEnabled: Boolean,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models events for the login screen.
|
||||||
|
*/
|
||||||
|
sealed class LoginEvent {
|
||||||
|
/**
|
||||||
|
* Navigates to the Landing screen.
|
||||||
|
*/
|
||||||
|
data object NavigateToLanding : LoginEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models actions for the login screen.
|
||||||
|
*/
|
||||||
|
sealed class LoginAction {
|
||||||
|
/**
|
||||||
|
* Indicates that the Login button has been clicked.
|
||||||
|
*/
|
||||||
|
data object LoginButtonClick : LoginAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the "Not you?" text was clicked.
|
||||||
|
*/
|
||||||
|
data object NotYouButtonClick : LoginAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the Enterprise single sign-on button has been clicked.
|
||||||
|
*/
|
||||||
|
data object SingleSignOnClick : LoginAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the password input has changed.
|
||||||
|
*/
|
||||||
|
data class PasswordInputChanged(val input: String) : LoginAction()
|
||||||
|
}
|
|
@ -19,7 +19,10 @@ import androidx.compose.ui.unit.dp
|
||||||
* @param label label for the text field.
|
* @param label label for the text field.
|
||||||
* @param initialValue initial input text.
|
* @param initialValue initial input text.
|
||||||
* @param onTextChange callback that is triggered when the input of the text field changes.
|
* @param onTextChange callback that is triggered when the input of the text field changes.
|
||||||
|
*
|
||||||
|
* TODO: remove deprecated version: BIT-289
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(message = "Use overloaded BitwardenTextField that takes an input instead of an initialText.")
|
||||||
@Composable
|
@Composable
|
||||||
fun BitwardenTextField(
|
fun BitwardenTextField(
|
||||||
label: String,
|
label: String,
|
||||||
|
@ -39,3 +42,28 @@ fun BitwardenTextField(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that allows the user to input text. This composable will manage the state of
|
||||||
|
* the user's input.
|
||||||
|
* @param label label for the text field.
|
||||||
|
* @param value current next on the text field.
|
||||||
|
* @param modifier modifier for the composable.
|
||||||
|
* @param onValueChange callback that is triggered when the input of the text field changes.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BitwardenTextField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
label = { Text(label) },
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -39,6 +39,13 @@
|
||||||
<string name="length">Length</string>
|
<string name="length">Length</string>
|
||||||
<string name="overflow_menu">Overflow menu</string>
|
<string name="overflow_menu">Overflow menu</string>
|
||||||
|
|
||||||
|
<!-- Login screen -->
|
||||||
|
<string name="enterprise_single_sign_on">Enterprise single sign-on</string>
|
||||||
|
<string name="log_in_with_master_password">Log in with master password</string>
|
||||||
|
<string name="logging_in_as">Logging in as %s on bitwarden.com</string>
|
||||||
|
<string name="master_password">Master password</string>
|
||||||
|
<string name="not_you">Not you?</string>
|
||||||
|
|
||||||
<!--Bottom Navigation-->
|
<!--Bottom Navigation-->
|
||||||
<string name="generator_tab_content_description">Press to navigate to the generator screen.</string>
|
<string name="generator_tab_content_description">Press to navigate to the generator screen.</string>
|
||||||
<string name="send_tab_content_description">Press to navigate to the send screen.</string>
|
<string name="send_tab_content_description">Press to navigate to the send screen.</string>
|
||||||
|
|
|
@ -1,33 +1,37 @@
|
||||||
package com.x8bit.bitwarden.ui.auth.feature.landing
|
package com.x8bit.bitwarden.ui.auth.feature.landing
|
||||||
|
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
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.BaseComposeTest
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
|
||||||
class LandingScreenTest : BaseComposeTest() {
|
class LandingScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `continue button click should send ContinueButtonClicked action`() {
|
fun `continue button click should send ContinueButtonClick action`() {
|
||||||
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||||
every { eventFlow } returns emptyFlow()
|
every { eventFlow } returns emptyFlow()
|
||||||
every { stateFlow } returns MutableStateFlow(
|
every { stateFlow } returns MutableStateFlow(
|
||||||
LandingState(
|
LandingState(
|
||||||
initialEmailAddress = "",
|
emailInput = "",
|
||||||
isContinueButtonEnabled = true,
|
isContinueButtonEnabled = true,
|
||||||
isRememberMeEnabled = false,
|
isRememberMeEnabled = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
every { trySendAction(LandingAction.ContinueButtonClick) } returns Unit
|
|
||||||
}
|
}
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
LandingScreen(
|
LandingScreen(
|
||||||
onNavigateToCreateAccount = {},
|
onNavigateToCreateAccount = {},
|
||||||
|
onNavigateToLogin = {},
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -36,4 +40,127 @@ class LandingScreenTest : BaseComposeTest() {
|
||||||
viewModel.trySendAction(LandingAction.ContinueButtonClick)
|
viewModel.trySendAction(LandingAction.ContinueButtonClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `remember me click should send RememberMeToggle action`() {
|
||||||
|
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns emptyFlow()
|
||||||
|
every { stateFlow } returns MutableStateFlow(
|
||||||
|
LandingState(
|
||||||
|
emailInput = "",
|
||||||
|
isContinueButtonEnabled = true,
|
||||||
|
isRememberMeEnabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LandingScreen(
|
||||||
|
onNavigateToCreateAccount = {},
|
||||||
|
onNavigateToLogin = {},
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithTag("Remember me").performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(LandingAction.RememberMeToggle(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `create account click should send CreateAccountClick action`() {
|
||||||
|
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns emptyFlow()
|
||||||
|
every { stateFlow } returns MutableStateFlow(
|
||||||
|
LandingState(
|
||||||
|
emailInput = "",
|
||||||
|
isContinueButtonEnabled = true,
|
||||||
|
isRememberMeEnabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LandingScreen(
|
||||||
|
onNavigateToCreateAccount = {},
|
||||||
|
onNavigateToLogin = {},
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText("Create account").performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(LandingAction.CreateAccountClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `email address change should send EmailInputChanged action`() {
|
||||||
|
val input = "email"
|
||||||
|
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns emptyFlow()
|
||||||
|
every { stateFlow } returns MutableStateFlow(
|
||||||
|
LandingState(
|
||||||
|
emailInput = "",
|
||||||
|
isContinueButtonEnabled = true,
|
||||||
|
isRememberMeEnabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LandingScreen(
|
||||||
|
onNavigateToCreateAccount = {},
|
||||||
|
onNavigateToLogin = {},
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithTag("Email address").performTextInput(input)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(LandingAction.EmailInputChanged(input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateToCreateAccount event should call onNavigateToCreateAccount`() {
|
||||||
|
var onNavigateToCreateAccountCalled = false
|
||||||
|
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns flowOf(LandingEvent.NavigateToCreateAccount)
|
||||||
|
every { stateFlow } returns MutableStateFlow(
|
||||||
|
LandingState(
|
||||||
|
emailInput = "",
|
||||||
|
isContinueButtonEnabled = true,
|
||||||
|
isRememberMeEnabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LandingScreen(
|
||||||
|
onNavigateToCreateAccount = { onNavigateToCreateAccountCalled = true },
|
||||||
|
onNavigateToLogin = {},
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assert(onNavigateToCreateAccountCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateToLogin event should call onNavigateToLogin`() {
|
||||||
|
val testEmail = "test@test.com"
|
||||||
|
var onNavigateToLoginEmail = ""
|
||||||
|
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns flowOf(LandingEvent.NavigateToLogin(testEmail))
|
||||||
|
every { stateFlow } returns MutableStateFlow(
|
||||||
|
LandingState(
|
||||||
|
emailInput = "",
|
||||||
|
isContinueButtonEnabled = true,
|
||||||
|
isRememberMeEnabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LandingScreen(
|
||||||
|
onNavigateToCreateAccount = { },
|
||||||
|
onNavigateToLogin = { onNavigateToLoginEmail = it },
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertEquals(testEmail, onNavigateToLoginEmail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.x8bit.bitwarden.ui.auth.feature.landing
|
package com.x8bit.bitwarden.ui.auth.feature.landing
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
@ -9,20 +10,42 @@ import org.junit.jupiter.api.Test
|
||||||
class LandingViewModelTest : BaseViewModelTest() {
|
class LandingViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ContinueButtonClick should disable continue button`() = runTest {
|
fun `initial state should be correct`() = runTest {
|
||||||
val viewModel = LandingViewModel()
|
val viewModel = LandingViewModel(SavedStateHandle())
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(DEFAULT_STATE, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should pull from saved state handle when present`() = runTest {
|
||||||
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
|
emailInput = "test",
|
||||||
|
isContinueButtonEnabled = false,
|
||||||
|
isRememberMeEnabled = true,
|
||||||
|
)
|
||||||
|
val handle = SavedStateHandle(mapOf("state" to expectedState))
|
||||||
|
val viewModel = LandingViewModel(handle)
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(expectedState, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ContinueButtonClick should emit NavigateToLogin`() = runTest {
|
||||||
|
val viewModel = LandingViewModel(SavedStateHandle())
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.actionChannel.trySend(LandingAction.ContinueButtonClick)
|
viewModel.actionChannel.trySend(LandingAction.ContinueButtonClick)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
viewModel.stateFlow.value,
|
LandingEvent.NavigateToLogin(""),
|
||||||
DEFAULT_STATE.copy(isContinueButtonEnabled = false),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CreateAccountClick should emit NavigateToCreateAccount`() = runTest {
|
fun `CreateAccountClick should emit NavigateToCreateAccount`() = runTest {
|
||||||
val viewModel = LandingViewModel()
|
val viewModel = LandingViewModel(SavedStateHandle())
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.actionChannel.trySend(LandingAction.CreateAccountClick)
|
viewModel.actionChannel.trySend(LandingAction.CreateAccountClick)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -34,7 +57,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `RememberMeToggle should update value of isRememberMeToggled`() = runTest {
|
fun `RememberMeToggle should update value of isRememberMeToggled`() = runTest {
|
||||||
val viewModel = LandingViewModel()
|
val viewModel = LandingViewModel(SavedStateHandle())
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.actionChannel.trySend(LandingAction.RememberMeToggle(true))
|
viewModel.actionChannel.trySend(LandingAction.RememberMeToggle(true))
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
@ -44,9 +67,23 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `EmailInputUpdated should update value of email input`() = runTest {
|
||||||
|
val input = "input"
|
||||||
|
val viewModel = LandingViewModel(SavedStateHandle())
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
awaitItem()
|
||||||
|
viewModel.trySendAction(LandingAction.EmailInputChanged(input))
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(emailInput = input),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val DEFAULT_STATE = LandingState(
|
private val DEFAULT_STATE = LandingState(
|
||||||
initialEmailAddress = "",
|
emailInput = "",
|
||||||
isContinueButtonEnabled = true,
|
isContinueButtonEnabled = true,
|
||||||
isRememberMeEnabled = false,
|
isRememberMeEnabled = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||||
|
|
||||||
|
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 io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import junit.framework.TestCase.assertTrue
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LoginScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Not you text click should send NotYouButtonClick action`() {
|
||||||
|
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns emptyFlow()
|
||||||
|
every { stateFlow } returns MutableStateFlow(
|
||||||
|
LoginState(
|
||||||
|
emailAddress = "",
|
||||||
|
isLoginButtonEnabled = false,
|
||||||
|
passwordInput = "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LoginScreen(
|
||||||
|
onNavigateToLanding = {},
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText("Not you?").performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(LoginAction.NotYouButtonClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `password input change should send PasswordInputChanged action`() {
|
||||||
|
val input = "input"
|
||||||
|
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns emptyFlow()
|
||||||
|
every { stateFlow } returns MutableStateFlow(
|
||||||
|
LoginState(
|
||||||
|
emailAddress = "",
|
||||||
|
isLoginButtonEnabled = false,
|
||||||
|
passwordInput = "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LoginScreen(
|
||||||
|
onNavigateToLanding = {},
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText("Master password").performTextInput(input)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(LoginAction.PasswordInputChanged(input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateToLanding should call onNavigateToLanding`() {
|
||||||
|
var onNavigateToLandingCalled = false
|
||||||
|
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns flowOf(LoginEvent.NavigateToLanding)
|
||||||
|
every { stateFlow } returns MutableStateFlow(
|
||||||
|
LoginState(
|
||||||
|
emailAddress = "",
|
||||||
|
isLoginButtonEnabled = false,
|
||||||
|
passwordInput = "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.setContent {
|
||||||
|
LoginScreen(
|
||||||
|
onNavigateToLanding = { onNavigateToLandingCalled = true },
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertTrue(onNavigateToLandingCalled)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class LoginViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
private val savedStateHandle = SavedStateHandle().also {
|
||||||
|
it["email_address"] = "test@gmail.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct`() = runTest {
|
||||||
|
val viewModel = LoginViewModel(savedStateHandle)
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(DEFAULT_STATE, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should pull from handle when present`() = runTest {
|
||||||
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
|
passwordInput = "input",
|
||||||
|
isLoginButtonEnabled = true,
|
||||||
|
)
|
||||||
|
val handle = SavedStateHandle(
|
||||||
|
mapOf(
|
||||||
|
"email_address" to "test@gmail.com",
|
||||||
|
"state" to expectedState,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = LoginViewModel(handle)
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(expectedState, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `LoginButtonClick should do nothing`() = runTest {
|
||||||
|
val viewModel = LoginViewModel(
|
||||||
|
savedStateHandle = savedStateHandle,
|
||||||
|
)
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(LoginAction.LoginButtonClick)
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SingleSignOnClick should do nothing`() = runTest {
|
||||||
|
val viewModel = LoginViewModel(
|
||||||
|
savedStateHandle = savedStateHandle,
|
||||||
|
)
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(LoginAction.SingleSignOnClick)
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NotYouButtonClick should emit NavigateToLanding`() = runTest {
|
||||||
|
val viewModel = LoginViewModel(
|
||||||
|
savedStateHandle = savedStateHandle,
|
||||||
|
)
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(LoginAction.NotYouButtonClick)
|
||||||
|
assertEquals(
|
||||||
|
LoginEvent.NavigateToLanding,
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PasswordInputChanged should update password input`() = runTest {
|
||||||
|
val input = "input"
|
||||||
|
val viewModel = LoginViewModel(
|
||||||
|
savedStateHandle = savedStateHandle,
|
||||||
|
)
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(LoginAction.PasswordInputChanged(input))
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(passwordInput = input),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEFAULT_STATE = LoginState(
|
||||||
|
emailAddress = "test@gmail.com",
|
||||||
|
passwordInput = "",
|
||||||
|
isLoginButtonEnabled = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue