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
This commit is contained in:
Benoit Marty 2025-05-12 14:45:07 +02:00 committed by GitHub
parent 525d72d85b
commit fe91895c9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 213 additions and 8 deletions

View file

@ -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("", "")

View file

@ -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<LoginPasswordState> {
override val values: Sequence<LoginPasswordState>
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<SessionId> = 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,
)

View file

@ -201,6 +201,7 @@ private fun LoginForm(
{
Box(Modifier.clickable {
loginFieldState = ""
eventSink(LoginPasswordEvents.SetLogin(""))
}) {
Icon(
imageVector = CompoundIcons.Close(),

View file

@ -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<ComponentActivity>()
@Test
fun `clicking on back invoke back callback`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setLoginPasswordView(
aLoginPasswordState(
eventSink = eventsRecorder
),
onBackClick = callback,
)
rule.pressBack()
}
}
@Test
fun `changing login invokes the expected event`() {
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
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<LoginPasswordEvents>()
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<LoginPasswordEvents>()
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<LoginPasswordEvents>()
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<LoginPasswordEvents>(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<LoginPasswordEvents>(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<LoginPasswordEvents>(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<LoginPasswordEvents>()
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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLoginPasswordView(
state: LoginPasswordState,
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
LoginPasswordView(
state = state,
onBackClick = onBackClick,
)
}
}