From fe91895c9aa21157433348446400596aef2d31df Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 May 2025 14:45:07 +0200 Subject: [PATCH] Disable Continue button when the login field is cleared. (#4699) * Disable Continue button when the login field is cleared. Fixes #4691 * Add tests on LoginPasswordView --- .../loginpassword/LoginPasswordState.kt | 2 +- .../LoginPasswordStateProvider.kt | 29 ++- .../loginpassword/LoginPasswordView.kt | 1 + .../loginpassword/LoginPasswordViewTest.kt | 189 ++++++++++++++++++ 4 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt index 4ed672ae92..18bac19633 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt @@ -28,7 +28,7 @@ data class LoginPasswordState( @Parcelize data class LoginFormState( val login: String, - val password: String + val password: String, ) : Parcelable { companion object { val Default = LoginFormState("", "") diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt index 5c6e973252..af6c07146e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt @@ -8,23 +8,38 @@ package io.element.android.features.login.impl.screens.loginpassword import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.SessionId open class LoginPasswordStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLoginPasswordState(), // Loading - aLoginPasswordState().copy(loginAction = AsyncData.Loading()), + aLoginPasswordState(loginAction = AsyncData.Loading()), // Error - aLoginPasswordState().copy(loginAction = AsyncData.Failure(Exception("An error occurred"))), + aLoginPasswordState(loginAction = AsyncData.Failure(Exception("An error occurred"))), ) } -fun aLoginPasswordState() = LoginPasswordState( - accountProvider = anAccountProvider(), - formState = LoginFormState.Default, - loginAction = AsyncData.Uninitialized, - eventSink = {} +fun aLoginPasswordState( + accountProvider: AccountProvider = anAccountProvider(), + formState: LoginFormState = LoginFormState.Default, + loginAction: AsyncData = AsyncData.Uninitialized, + eventSink: (LoginPasswordEvents) -> Unit = {}, +) = LoginPasswordState( + accountProvider = accountProvider, + formState = formState, + loginAction = loginAction, + eventSink = eventSink, +) + +fun aLoginFormState( + login: String = "", + password: String = "", +) = LoginFormState( + login = login, + password = password, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index ca05df5ace..4bca66dc96 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -201,6 +201,7 @@ private fun LoginForm( { Box(Modifier.clickable { loginFieldState = "" + eventSink(LoginPasswordEvents.SetLogin("")) }) { Icon( imageVector = CompoundIcons.Close(), diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt new file mode 100644 index 0000000000..45d7bc53e5 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.screens.loginpassword + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.test.A_PASSWORD +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class LoginPasswordViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invoke back callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder + ), + onBackClick = callback, + ) + rule.pressBack() + } + } + + @Test + fun `changing login invokes the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder, + ), + ) + val userNameHint = rule.activity.getString(CommonStrings.common_username) + rule.onNodeWithText(userNameHint).performTextInput(A_USER_NAME) + eventsRecorder.assertSingle( + LoginPasswordEvents.SetLogin(A_USER_NAME) + ) + } + + @Test + fun `changing login removes new lines the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder, + ), + ) + val userNameHint = rule.activity.getString(CommonStrings.common_username) + rule.onNodeWithText(userNameHint).performTextInput("a\nb") + eventsRecorder.assertSingle( + LoginPasswordEvents.SetLogin("ab") + ) + } + + @Test + fun `clearing login invokes the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(A_USER_NAME), + eventSink = eventsRecorder, + ), + ) + val a11yClear = rule.activity.getString(CommonStrings.action_clear) + rule.onNodeWithContentDescription(a11yClear).performClick() + eventsRecorder.assertSingle( + LoginPasswordEvents.SetLogin("") + ) + } + + @Test + fun `changing password invokes the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder, + ), + ) + val userNameHint = rule.activity.getString(CommonStrings.common_password) + rule.onNodeWithText(userNameHint).performTextInput(A_PASSWORD) + eventsRecorder.assertSingle( + LoginPasswordEvents.SetPassword(A_PASSWORD) + ) + } + + @Test + fun `reveal password makes the password visible`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(password = A_PASSWORD), + eventSink = eventsRecorder, + ), + ) + rule.onNodeWithText(A_PASSWORD).assertDoesNotExist() + // Show password + val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password) + rule.onNodeWithContentDescription(a11yShowPassword).performClick() + rule.onNodeWithText(A_PASSWORD).assertExists() + // Hide password + val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password) + rule.onNodeWithContentDescription(a11yHidePassword).performClick() + rule.onNodeWithText(A_PASSWORD).assertDoesNotExist() + } + + @Test + fun `when login is empty, continue button is not enabled`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(password = A_PASSWORD), + eventSink = eventsRecorder, + ), + ) + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsNotEnabled() + } + + @Test + fun `when password is empty, continue button is not enabled`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(login = A_USER_NAME), + eventSink = eventsRecorder, + ), + ) + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsNotEnabled() + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on Continue sends expected event`() { + val eventsRecorder = EventsRecorder() + rule.setLoginPasswordView( + aLoginPasswordState( + formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD), + eventSink = eventsRecorder, + ), + ) + val continueStr = rule.activity.getString(CommonStrings.action_continue) + rule.onNodeWithText(continueStr).assertIsEnabled() + rule.clickOn(CommonStrings.action_continue) + eventsRecorder.assertSingle( + LoginPasswordEvents.Submit + ) + } +} + +private fun AndroidComposeTestRule.setLoginPasswordView( + state: LoginPasswordState, + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + LoginPasswordView( + state = state, + onBackClick = onBackClick, + ) + } +}