Skip to content

Commit fe91895

Browse files
authored
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
1 parent 525d72d commit fe91895

File tree

4 files changed

+213
-8
lines changed

4 files changed

+213
-8
lines changed

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ data class LoginPasswordState(
2828
@Parcelize
2929
data class LoginFormState(
3030
val login: String,
31-
val password: String
31+
val password: String,
3232
) : Parcelable {
3333
companion object {
3434
val Default = LoginFormState("", "")

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,38 @@
88
package io.element.android.features.login.impl.screens.loginpassword
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
import io.element.android.features.login.impl.accountprovider.AccountProvider
1112
import io.element.android.features.login.impl.accountprovider.anAccountProvider
1213
import io.element.android.libraries.architecture.AsyncData
14+
import io.element.android.libraries.matrix.api.core.SessionId
1315

1416
open class LoginPasswordStateProvider : PreviewParameterProvider<LoginPasswordState> {
1517
override val values: Sequence<LoginPasswordState>
1618
get() = sequenceOf(
1719
aLoginPasswordState(),
1820
// Loading
19-
aLoginPasswordState().copy(loginAction = AsyncData.Loading()),
21+
aLoginPasswordState(loginAction = AsyncData.Loading()),
2022
// Error
21-
aLoginPasswordState().copy(loginAction = AsyncData.Failure(Exception("An error occurred"))),
23+
aLoginPasswordState(loginAction = AsyncData.Failure(Exception("An error occurred"))),
2224
)
2325
}
2426

25-
fun aLoginPasswordState() = LoginPasswordState(
26-
accountProvider = anAccountProvider(),
27-
formState = LoginFormState.Default,
28-
loginAction = AsyncData.Uninitialized,
29-
eventSink = {}
27+
fun aLoginPasswordState(
28+
accountProvider: AccountProvider = anAccountProvider(),
29+
formState: LoginFormState = LoginFormState.Default,
30+
loginAction: AsyncData<SessionId> = AsyncData.Uninitialized,
31+
eventSink: (LoginPasswordEvents) -> Unit = {},
32+
) = LoginPasswordState(
33+
accountProvider = accountProvider,
34+
formState = formState,
35+
loginAction = loginAction,
36+
eventSink = eventSink,
37+
)
38+
39+
fun aLoginFormState(
40+
login: String = "",
41+
password: String = "",
42+
) = LoginFormState(
43+
login = login,
44+
password = password,
3045
)

features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ private fun LoginForm(
201201
{
202202
Box(Modifier.clickable {
203203
loginFieldState = ""
204+
eventSink(LoginPasswordEvents.SetLogin(""))
204205
}) {
205206
Icon(
206207
imageVector = CompoundIcons.Close(),
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.login.impl.screens.loginpassword
9+
10+
import androidx.activity.ComponentActivity
11+
import androidx.compose.ui.test.assertIsEnabled
12+
import androidx.compose.ui.test.assertIsNotEnabled
13+
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
14+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
15+
import androidx.compose.ui.test.onNodeWithContentDescription
16+
import androidx.compose.ui.test.onNodeWithText
17+
import androidx.compose.ui.test.performClick
18+
import androidx.compose.ui.test.performTextInput
19+
import androidx.test.ext.junit.runners.AndroidJUnit4
20+
import io.element.android.libraries.matrix.test.A_PASSWORD
21+
import io.element.android.libraries.matrix.test.A_USER_NAME
22+
import io.element.android.libraries.ui.strings.CommonStrings
23+
import io.element.android.tests.testutils.EnsureNeverCalled
24+
import io.element.android.tests.testutils.EventsRecorder
25+
import io.element.android.tests.testutils.clickOn
26+
import io.element.android.tests.testutils.ensureCalledOnce
27+
import io.element.android.tests.testutils.pressBack
28+
import org.junit.Rule
29+
import org.junit.Test
30+
import org.junit.rules.TestRule
31+
import org.junit.runner.RunWith
32+
import org.robolectric.annotation.Config
33+
34+
@RunWith(AndroidJUnit4::class)
35+
class LoginPasswordViewTest {
36+
@get:Rule
37+
val rule = createAndroidComposeRule<ComponentActivity>()
38+
39+
@Test
40+
fun `clicking on back invoke back callback`() {
41+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
42+
ensureCalledOnce { callback ->
43+
rule.setLoginPasswordView(
44+
aLoginPasswordState(
45+
eventSink = eventsRecorder
46+
),
47+
onBackClick = callback,
48+
)
49+
rule.pressBack()
50+
}
51+
}
52+
53+
@Test
54+
fun `changing login invokes the expected event`() {
55+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
56+
rule.setLoginPasswordView(
57+
aLoginPasswordState(
58+
eventSink = eventsRecorder,
59+
),
60+
)
61+
val userNameHint = rule.activity.getString(CommonStrings.common_username)
62+
rule.onNodeWithText(userNameHint).performTextInput(A_USER_NAME)
63+
eventsRecorder.assertSingle(
64+
LoginPasswordEvents.SetLogin(A_USER_NAME)
65+
)
66+
}
67+
68+
@Test
69+
fun `changing login removes new lines the expected event`() {
70+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
71+
rule.setLoginPasswordView(
72+
aLoginPasswordState(
73+
eventSink = eventsRecorder,
74+
),
75+
)
76+
val userNameHint = rule.activity.getString(CommonStrings.common_username)
77+
rule.onNodeWithText(userNameHint).performTextInput("a\nb")
78+
eventsRecorder.assertSingle(
79+
LoginPasswordEvents.SetLogin("ab")
80+
)
81+
}
82+
83+
@Test
84+
fun `clearing login invokes the expected event`() {
85+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
86+
rule.setLoginPasswordView(
87+
aLoginPasswordState(
88+
formState = aLoginFormState(A_USER_NAME),
89+
eventSink = eventsRecorder,
90+
),
91+
)
92+
val a11yClear = rule.activity.getString(CommonStrings.action_clear)
93+
rule.onNodeWithContentDescription(a11yClear).performClick()
94+
eventsRecorder.assertSingle(
95+
LoginPasswordEvents.SetLogin("")
96+
)
97+
}
98+
99+
@Test
100+
fun `changing password invokes the expected event`() {
101+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
102+
rule.setLoginPasswordView(
103+
aLoginPasswordState(
104+
eventSink = eventsRecorder,
105+
),
106+
)
107+
val userNameHint = rule.activity.getString(CommonStrings.common_password)
108+
rule.onNodeWithText(userNameHint).performTextInput(A_PASSWORD)
109+
eventsRecorder.assertSingle(
110+
LoginPasswordEvents.SetPassword(A_PASSWORD)
111+
)
112+
}
113+
114+
@Test
115+
fun `reveal password makes the password visible`() {
116+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
117+
rule.setLoginPasswordView(
118+
aLoginPasswordState(
119+
formState = aLoginFormState(password = A_PASSWORD),
120+
eventSink = eventsRecorder,
121+
),
122+
)
123+
rule.onNodeWithText(A_PASSWORD).assertDoesNotExist()
124+
// Show password
125+
val a11yShowPassword = rule.activity.getString(CommonStrings.a11y_show_password)
126+
rule.onNodeWithContentDescription(a11yShowPassword).performClick()
127+
rule.onNodeWithText(A_PASSWORD).assertExists()
128+
// Hide password
129+
val a11yHidePassword = rule.activity.getString(CommonStrings.a11y_hide_password)
130+
rule.onNodeWithContentDescription(a11yHidePassword).performClick()
131+
rule.onNodeWithText(A_PASSWORD).assertDoesNotExist()
132+
}
133+
134+
@Test
135+
fun `when login is empty, continue button is not enabled`() {
136+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
137+
rule.setLoginPasswordView(
138+
aLoginPasswordState(
139+
formState = aLoginFormState(password = A_PASSWORD),
140+
eventSink = eventsRecorder,
141+
),
142+
)
143+
val continueStr = rule.activity.getString(CommonStrings.action_continue)
144+
rule.onNodeWithText(continueStr).assertIsNotEnabled()
145+
}
146+
147+
@Test
148+
fun `when password is empty, continue button is not enabled`() {
149+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>(expectEvents = false)
150+
rule.setLoginPasswordView(
151+
aLoginPasswordState(
152+
formState = aLoginFormState(login = A_USER_NAME),
153+
eventSink = eventsRecorder,
154+
),
155+
)
156+
val continueStr = rule.activity.getString(CommonStrings.action_continue)
157+
rule.onNodeWithText(continueStr).assertIsNotEnabled()
158+
}
159+
160+
@Config(qualifiers = "h1024dp")
161+
@Test
162+
fun `clicking on Continue sends expected event`() {
163+
val eventsRecorder = EventsRecorder<LoginPasswordEvents>()
164+
rule.setLoginPasswordView(
165+
aLoginPasswordState(
166+
formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD),
167+
eventSink = eventsRecorder,
168+
),
169+
)
170+
val continueStr = rule.activity.getString(CommonStrings.action_continue)
171+
rule.onNodeWithText(continueStr).assertIsEnabled()
172+
rule.clickOn(CommonStrings.action_continue)
173+
eventsRecorder.assertSingle(
174+
LoginPasswordEvents.Submit
175+
)
176+
}
177+
}
178+
179+
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLoginPasswordView(
180+
state: LoginPasswordState,
181+
onBackClick: () -> Unit = EnsureNeverCalled(),
182+
) {
183+
setContent {
184+
LoginPasswordView(
185+
state = state,
186+
onBackClick = onBackClick,
187+
)
188+
}
189+
}

0 commit comments

Comments
 (0)