Simplify LoginScreen / LoginViewModel tests (#330)

This commit is contained in:
Brian Yencho 2023-12-06 09:23:09 -06:00 committed by Álison Fernandes
parent 43793d6d41
commit b61c796f7b
2 changed files with 108 additions and 304 deletions

View file

@ -21,35 +21,38 @@ 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.emptyFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Test
class LoginScreenTest : BaseComposeTest() {
private val intentHandler = mockk<IntentHandler>(relaxed = true) {
every { startCustomTabsActivity(any()) } returns Unit
}
private var onNavigateBackCalled = false
private val mutableEventFlow = MutableSharedFlow<LoginEvent>(
extraBufferCapacity = Int.MAX_VALUE,
)
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<LoginViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setUp() {
composeTestRule.setContent {
LoginScreen(
onNavigateBack = { onNavigateBackCalled = true },
viewModel = viewModel,
intentHandler = intentHandler,
)
}
}
@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 = "",
captchaToken = null,
isLoginButtonEnabled = false,
passwordInput = "",
environmentLabel = "".asText(),
loadingDialogState = LoadingDialogState.Hidden,
errorDialogState = BasicDialogState.Hidden,
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
viewModel = viewModel,
)
}
composeTestRule.onNodeWithContentDescription("Close").performClick()
verify {
viewModel.trySendAction(LoginAction.CloseButtonClick)
@ -58,26 +61,6 @@ 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 = "",
captchaToken = null,
isLoginButtonEnabled = false,
passwordInput = "",
environmentLabel = "".asText(),
loadingDialogState = LoadingDialogState.Hidden,
errorDialogState = BasicDialogState.Hidden,
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Not you?").performScrollTo().performClick()
verify {
viewModel.trySendAction(LoginAction.NotYouButtonClick)
@ -86,26 +69,6 @@ class LoginScreenTest : BaseComposeTest() {
@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 = "",
captchaToken = null,
isLoginButtonEnabled = false,
passwordInput = "",
environmentLabel = "".asText(),
loadingDialogState = LoadingDialogState.Hidden,
errorDialogState = BasicDialogState.Hidden,
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Get your master password hint").performClick()
verify {
viewModel.trySendAction(LoginAction.MasterPasswordHintClick)
@ -114,26 +77,6 @@ class LoginScreenTest : BaseComposeTest() {
@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 = "",
captchaToken = null,
isLoginButtonEnabled = false,
passwordInput = "",
environmentLabel = "".asText(),
loadingDialogState = LoadingDialogState.Hidden,
errorDialogState = BasicDialogState.Hidden,
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
viewModel = viewModel,
)
}
// Confirm dropdown version of item is absent
composeTestRule
.onAllNodesWithText("Get your master password hint")
@ -154,26 +97,6 @@ class LoginScreenTest : BaseComposeTest() {
@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 = "",
captchaToken = null,
isLoginButtonEnabled = false,
passwordInput = "",
environmentLabel = "".asText(),
loadingDialogState = LoadingDialogState.Hidden,
errorDialogState = BasicDialogState.Hidden,
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
viewModel = viewModel,
)
}
composeTestRule.onNodeWithText("Master password").performTextInput(input)
verify {
viewModel.trySendAction(LoginAction.PasswordInputChanged(input))
@ -182,57 +105,25 @@ class LoginScreenTest : BaseComposeTest() {
@Test
fun `NavigateBack should call onNavigateBack`() {
var onNavigateBackCalled = false
val viewModel = mockk<LoginViewModel>(relaxed = true) {
every { eventFlow } returns flowOf(LoginEvent.NavigateBack)
every { stateFlow } returns MutableStateFlow(
LoginState(
emailAddress = "",
captchaToken = null,
isLoginButtonEnabled = false,
passwordInput = "",
environmentLabel = "".asText(),
loadingDialogState = LoadingDialogState.Hidden,
errorDialogState = BasicDialogState.Hidden,
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = { onNavigateBackCalled = true },
viewModel = viewModel,
)
}
mutableEventFlow.tryEmit(LoginEvent.NavigateBack)
assertTrue(onNavigateBackCalled)
}
@Test
fun `NavigateToCaptcha should call intentHandler startCustomTabsActivity`() {
val intentHandler = mockk<IntentHandler>(relaxed = true) {
every { startCustomTabsActivity(any()) } returns Unit
}
val mockUri = mockk<Uri>()
val viewModel = mockk<LoginViewModel>(relaxed = true) {
every { eventFlow } returns flowOf(LoginEvent.NavigateToCaptcha(mockUri))
every { stateFlow } returns MutableStateFlow(
LoginState(
emailAddress = "",
captchaToken = null,
isLoginButtonEnabled = false,
passwordInput = "",
environmentLabel = "".asText(),
loadingDialogState = LoadingDialogState.Hidden,
errorDialogState = BasicDialogState.Hidden,
),
)
}
composeTestRule.setContent {
LoginScreen(
onNavigateBack = {},
intentHandler = intentHandler,
viewModel = viewModel,
)
}
mutableEventFlow.tryEmit(LoginEvent.NavigateToCaptcha(mockUri))
verify { intentHandler.startCustomTabsActivity(mockUri) }
}
}
private val DEFAULT_STATE =
LoginState(
emailAddress = "",
captchaToken = null,
isLoginButtonEnabled = false,
passwordInput = "",
environmentLabel = "".asText(),
loadingDialogState = LoadingDialogState.Hidden,
errorDialogState = BasicDialogState.Hidden,
)

View file

@ -7,10 +7,11 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
@ -21,7 +22,8 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
@ -33,6 +35,15 @@ class LoginViewModelTest : BaseViewModelTest() {
private val savedStateHandle = SavedStateHandle().also {
it["email_address"] = "test@gmail.com"
}
private val mutableCaptchaTokenResultFlow = MutableSharedFlow<CaptchaCallbackTokenResult>(
extraBufferCapacity = Int.MAX_VALUE,
)
private val mutableStateFlow = MutableStateFlow<UserState?>(null)
private val authRepository: AuthRepository = mockk(relaxed = true) {
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
every { userStateFlow } returns mutableStateFlow
}
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
@BeforeEach
fun setUp() {
@ -46,15 +57,7 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct for non-custom Environments`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = savedStateHandle,
)
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
}
@ -62,19 +65,12 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct for custom Environments with empty base URLs`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = "",
),
)
},
savedStateHandle = savedStateHandle,
fakeEnvironmentRepository.environment = Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = "",
),
)
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(
@ -88,19 +84,12 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct for custom Environments with non-empty base URLs`() =
runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = "https://abc.com/path-1/path-2",
),
)
},
savedStateHandle = savedStateHandle,
fakeEnvironmentRepository.environment = Environment.SelfHosted(
environmentUrlData = EnvironmentUrlDataJson(
base = "https://abc.com/path-1/path-2",
),
)
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(
@ -117,21 +106,8 @@ class LoginViewModelTest : BaseViewModelTest() {
passwordInput = "input",
isLoginButtonEnabled = true,
)
val handle = SavedStateHandle(
mapOf(
"email_address" to "test@gmail.com",
"state" to expectedState,
),
)
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = handle,
)
savedStateHandle["state"] = expectedState
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(expectedState, awaitItem())
}
@ -139,15 +115,7 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `CloseButtonClick should emit NavigateBack`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = savedStateHandle,
)
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.CloseButtonClick)
assertEquals(
@ -159,24 +127,14 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `LoginButtonClick login returns error should update errorDialogState`() = runTest {
val authRepository = mockk<AuthRepository> {
coEvery {
login(
email = "test@gmail.com",
password = "",
captchaToken = null,
)
} returns LoginResult.Error(errorMessage = "mock_error")
every { captchaTokenResultFlow } returns flowOf()
}
val environmentRepository = mockk<EnvironmentRepository> {
every { environment } returns Environment.Us
}
val viewModel = LoginViewModel(
authRepository = authRepository,
environmentRepository = environmentRepository,
savedStateHandle = savedStateHandle,
)
coEvery {
authRepository.login(
email = "test@gmail.com",
password = "",
captchaToken = null,
)
} returns LoginResult.Error(errorMessage = "mock_error")
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(LoginAction.LoginButtonClick)
@ -206,19 +164,14 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `LoginButtonClick login returns success should update loadingDialogState`() = runTest {
val authRepository = mockk<AuthRepository> {
coEvery {
login("test@gmail.com", "", captchaToken = null)
} returns LoginResult.Success
every { captchaTokenResultFlow } returns flowOf()
}
val viewModel = LoginViewModel(
authRepository = authRepository,
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = savedStateHandle,
)
coEvery {
authRepository.login(
email = "test@gmail.com",
password = "",
captchaToken = null,
)
} returns LoginResult.Success
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(LoginAction.LoginButtonClick)
@ -247,18 +200,14 @@ class LoginViewModelTest : BaseViewModelTest() {
every {
generateUriForCaptcha(captchaId = "mock_captcha_id")
} returns mockkUri
val authRepository = mockk<AuthRepository> {
coEvery { login("test@gmail.com", "", captchaToken = null) } returns
LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
every { captchaTokenResultFlow } returns flowOf()
}
val viewModel = LoginViewModel(
authRepository = authRepository,
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = savedStateHandle,
)
coEvery {
authRepository.login(
email = "test@gmail.com",
password = "",
captchaToken = null,
)
} returns LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.LoginButtonClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
@ -271,15 +220,7 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `MasterPasswordHintClick should emit ShowToast`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = savedStateHandle,
)
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.MasterPasswordHintClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
@ -292,15 +233,7 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `SingleSignOnClick should emit ShowToast`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = savedStateHandle,
)
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.SingleSignOnClick)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
@ -313,15 +246,7 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `NotYouButtonClick should emit NavigateBack`() = runTest {
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = savedStateHandle,
)
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.NotYouButtonClick)
assertEquals(
@ -334,15 +259,7 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `PasswordInputChanged should update password input`() = runTest {
val input = "input"
val viewModel = LoginViewModel(
authRepository = mockk {
every { captchaTokenResultFlow } returns flowOf()
},
environmentRepository = mockk {
every { environment } returns Environment.Us
},
savedStateHandle = savedStateHandle,
)
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(LoginAction.PasswordInputChanged(input))
assertEquals(
@ -354,31 +271,27 @@ class LoginViewModelTest : BaseViewModelTest() {
@Test
fun `captchaTokenFlow success update should trigger a login`() = runTest {
val authRepository = mockk<AuthRepository> {
every { captchaTokenResultFlow } returns flowOf(
CaptchaCallbackTokenResult.Success("token"),
coEvery {
authRepository.login(
email = "test@gmail.com",
password = "",
captchaToken = "token",
)
coEvery {
login(
"test@gmail.com",
"",
captchaToken = "token",
)
} returns LoginResult.Success
}
val environmentRepository = mockk<EnvironmentRepository> {
every { environment } returns Environment.Us
}
LoginViewModel(
authRepository = authRepository,
environmentRepository = environmentRepository,
savedStateHandle = savedStateHandle,
)
} returns LoginResult.Success
createViewModel()
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
coVerify {
authRepository.login(email = "test@gmail.com", password = "", captchaToken = "token")
}
}
private fun createViewModel(): LoginViewModel =
LoginViewModel(
authRepository = authRepository,
environmentRepository = fakeEnvironmentRepository,
savedStateHandle = savedStateHandle,
)
companion object {
private val DEFAULT_STATE = LoginState(
emailAddress = "test@gmail.com",