Skip to content

Commit 47945b4

Browse files
committed
test: e2e tests for PhoneAuthScreen
1 parent 4266dd6 commit 47945b4

File tree

10 files changed

+522
-79
lines changed

10 files changed

+522
-79
lines changed

auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProvider.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,11 @@ interface AuthUIStringProvider {
210210
/** Invalid verification code error */
211211
val invalidVerificationCode: String
212212

213-
/** Country code label */
214-
val countryCode: String
213+
/** Select country modal sheet title */
214+
val countrySelectorModalTitle: String
215215

216-
/** Select country dialog title */
217-
val selectCountry: String
218-
219-
/** Search countries hint */
220-
val searchCountries: String
216+
/** Select country modal sheet input field hint */
217+
val searchCountriesHint: String
221218

222219
// Provider Picker Strings
223220
/** Common button text for sign in */

auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/DefaultAuthUIStringProvider.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,11 @@ class DefaultAuthUIStringProvider(
204204
override val invalidVerificationCode: String
205205
get() = localizedContext.getString(R.string.fui_incorrect_code_dialog_body)
206206

207-
override val countryCode: String
208-
get() = localizedContext.getString(R.string.fui_country_hint)
209-
210-
override val selectCountry: String
211-
get() = localizedContext.getString(R.string.fui_country_hint)
207+
override val countrySelectorModalTitle: String
208+
get() = localizedContext.getString(R.string.fui_country_selector_title)
212209

213-
override val searchCountries: String
214-
get() = localizedContext.getString(R.string.fui_country_hint)
210+
override val searchCountriesHint: String
211+
get() = localizedContext.getString(R.string.fui_search_country_field_hint)
215212

216213
/**
217214
* Multi-Factor Authentication Strings

auth/src/main/java/com/firebase/ui/auth/compose/ui/components/CountrySelector.kt

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import androidx.compose.foundation.lazy.LazyColumn
2929
import androidx.compose.foundation.lazy.items
3030
import androidx.compose.material.icons.Icons
3131
import androidx.compose.material.icons.filled.ArrowDropDown
32+
import androidx.compose.material3.Button
33+
import androidx.compose.material3.ButtonDefaults
3234
import androidx.compose.material3.ExperimentalMaterial3Api
3335
import androidx.compose.material3.Icon
3436
import androidx.compose.material3.MaterialTheme
@@ -44,7 +46,11 @@ import androidx.compose.runtime.rememberCoroutineScope
4446
import androidx.compose.runtime.setValue
4547
import androidx.compose.ui.Alignment
4648
import androidx.compose.ui.Modifier
49+
import androidx.compose.ui.graphics.Color
4750
import androidx.compose.ui.platform.LocalContext
51+
import androidx.compose.ui.platform.testTag
52+
import androidx.compose.ui.semantics.contentDescription
53+
import androidx.compose.ui.semantics.semantics
4854
import androidx.compose.ui.unit.dp
4955
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
5056
import com.firebase.ui.auth.compose.data.ALL_COUNTRIES
@@ -67,7 +73,7 @@ fun CountrySelector(
6773
selectedCountry: CountryData,
6874
onCountrySelected: (CountryData) -> Unit,
6975
enabled: Boolean = true,
70-
allowedCountries: Set<String>? = null
76+
allowedCountries: Set<String>? = null,
7177
) {
7278
val context = LocalContext.current
7379
val stringProvider = DefaultAuthUIStringProvider(context)
@@ -101,7 +107,10 @@ fun CountrySelector(
101107
.clickable(enabled = enabled) {
102108
showBottomSheet = true
103109
}
104-
.padding(start = 8.dp),
110+
.padding(start = 8.dp)
111+
.semantics {
112+
contentDescription = "Country selector"
113+
},
105114
verticalAlignment = Alignment.CenterVertically,
106115
horizontalArrangement = Arrangement.spacedBy(4.dp)
107116
) {
@@ -111,7 +120,7 @@ fun CountrySelector(
111120
)
112121
Text(
113122
text = selectedCountry.dialCode,
114-
style = MaterialTheme.typography.bodyLarge
123+
style = MaterialTheme.typography.bodyLarge,
115124
)
116125
Icon(
117126
imageVector = Icons.Default.ArrowDropDown,
@@ -135,15 +144,15 @@ fun CountrySelector(
135144
.padding(bottom = 16.dp)
136145
) {
137146
Text(
138-
text = stringProvider.selectCountry,
147+
text = stringProvider.countrySelectorModalTitle,
139148
style = MaterialTheme.typography.headlineSmall,
140149
modifier = Modifier.padding(bottom = 16.dp)
141150
)
142151

143152
OutlinedTextField(
144153
value = searchQuery,
145154
onValueChange = { searchQuery = it },
146-
label = { Text(stringProvider.searchCountries) },
155+
label = { Text(stringProvider.searchCountriesHint) },
147156
modifier = Modifier.fillMaxWidth()
148157
)
149158

@@ -153,44 +162,50 @@ fun CountrySelector(
153162
modifier = Modifier
154163
.fillMaxWidth()
155164
.height(500.dp)
165+
.testTag("CountrySelector LazyColumn")
156166
) {
157167
items(filteredCountries) { country ->
158-
Row(
159-
modifier = Modifier
160-
.fillMaxWidth()
161-
.clickable {
162-
onCountrySelected(country)
163-
scope.launch {
164-
sheetState.hide()
165-
}.invokeOnCompletion {
166-
if (!sheetState.isVisible) {
167-
showBottomSheet = false
168-
searchQuery = ""
169-
}
170-
}
168+
Button(
169+
onClick = {
170+
onCountrySelected(country)
171+
scope.launch {
172+
sheetState.hide()
173+
showBottomSheet = false
174+
searchQuery = ""
171175
}
172-
.padding(vertical = 12.dp, horizontal = 8.dp),
173-
horizontalArrangement = Arrangement.SpaceBetween,
174-
verticalAlignment = Alignment.CenterVertically
176+
},
177+
colors = ButtonDefaults.buttonColors(
178+
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
179+
containerColor = Color.Transparent
180+
),
181+
contentPadding = PaddingValues.Zero
175182
) {
176183
Row(
184+
modifier = Modifier
185+
.fillMaxWidth()
186+
.padding(vertical = 12.dp, horizontal = 8.dp),
187+
horizontalArrangement = Arrangement.SpaceBetween,
177188
verticalAlignment = Alignment.CenterVertically
178189
) {
190+
Row(
191+
verticalAlignment = Alignment.CenterVertically
192+
) {
193+
Text(
194+
text = country.flagEmoji,
195+
style = MaterialTheme.typography.headlineMedium
196+
)
197+
Spacer(modifier = Modifier.width(12.dp))
198+
Text(
199+
text = country.name,
200+
style = MaterialTheme.typography.bodyLarge
201+
)
202+
}
179203
Text(
180-
text = country.flagEmoji,
181-
style = MaterialTheme.typography.headlineMedium
182-
)
183-
Spacer(modifier = Modifier.width(12.dp))
184-
Text(
185-
text = country.name,
186-
style = MaterialTheme.typography.bodyLarge
204+
text = country.dialCode,
205+
style = MaterialTheme.typography.bodyMedium,
206+
color = MaterialTheme.colorScheme.onSurfaceVariant
187207
)
188208
}
189-
Text(
190-
text = country.dialCode,
191-
style = MaterialTheme.typography.bodyMedium,
192-
color = MaterialTheme.colorScheme.onSurfaceVariant
193-
)
194209
}
195210
}
196211
}

auth/src/main/java/com/firebase/ui/auth/compose/ui/screens/phone/PhoneAuthScreen.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ fun PhoneAuthScreen(
150150
} ?: CountryUtils.getDefaultCountry()
151151
)
152152
}
153-
val fullPhoneNumber =
153+
val fullPhoneNumber = remember(selectedCountry.value, phoneNumberValue.value) {
154154
CountryUtils.formatPhoneNumber(selectedCountry.value.dialCode, phoneNumberValue.value)
155+
}
155156
val verificationId = rememberSaveable { mutableStateOf<String?>(null) }
156157
val forceResendingToken =
157158
rememberSaveable { mutableStateOf<PhoneAuthProvider.ForceResendingToken?>(null) }
@@ -186,7 +187,7 @@ fun PhoneAuthScreen(
186187
verificationId.value = state.verificationId
187188
forceResendingToken.value = state.forceResendingToken
188189
step.value = PhoneAuthStep.EnterVerificationCode
189-
resendTimerSeconds.intValue = 60 // Start 60-second countdown
190+
resendTimerSeconds.intValue = provider.timeout.toInt() // Start 60-second countdown
190191
}
191192

192193
is AuthState.SMSAutoVerified -> {
@@ -270,7 +271,7 @@ fun PhoneAuthScreen(
270271
phoneNumber = fullPhoneNumber,
271272
forceResendingToken = forceResendingToken.value,
272273
)
273-
resendTimerSeconds.intValue = 60 // Restart timer
274+
resendTimerSeconds.intValue = provider.timeout.toInt() // Restart timer
274275
} catch (e: Exception) {
275276
// Error will be handled by authState flow
276277
}
@@ -280,7 +281,6 @@ fun PhoneAuthScreen(
280281
resendTimer = resendTimerSeconds.intValue,
281282
onChangeNumberClick = {
282283
step.value = PhoneAuthStep.EnterPhoneNumber
283-
//phoneNumberValue.value = ""
284284
verificationCodeValue.value = ""
285285
verificationId.value = null
286286
forceResendingToken.value = null

auth/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
<string name="fui_email_hint" translation_description="Hint in email input field">Email</string>
3737
<string name="fui_phone_hint" translation_description="Hint for phone input field">Phone Number</string>
3838
<string name="fui_country_hint" translation_description="Hint for phone input field">Country</string>
39+
<string name="fui_country_selector_title" translation_description="Country selector modal title">Select a country</string>
40+
<string name="fui_search_country_field_hint" translation_description="Hint for search for country input field">Select for country e.g. +1, "US"</string>
3941
<string name="fui_password_hint" translation_description="Hint for password input field for existing passwords">Password</string>
4042
<string name="fui_confirm_password_hint" translation_description="Hint for confirm password input field for existing passwords">Confirm Password</string>
4143
<string name="fui_new_password_hint" translation_description="Hint for password input field for new passwords">New password</string>

composeapp/src/main/java/com/firebase/composeapp/MainActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class MainActivity : ComponentActivity() {
8585
defaultCountryCode = null,
8686
allowedCountries = emptyList(),
8787
smsCodeLength = 6,
88-
timeout = 60L,
88+
timeout = 120L,
8989
isInstantVerificationEnabled = true
9090
)
9191
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.firebase.ui.auth.compose.testutil
2+
3+
const val AUTH_STATE_WAIT_TIMEOUT_MS = 5_000L

e2eTest/src/test/java/com/firebase/ui/auth/compose/testutil/EmulatorApi.kt

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,26 @@ import java.net.HttpURLConnection
77
internal class EmulatorAuthApi(
88
private val projectId: String,
99
emulatorHost: String,
10-
emulatorPort: Int
10+
emulatorPort: Int,
1111
) {
1212

1313
private val httpClient = HttpClient(host = emulatorHost, port = emulatorPort)
1414

15+
/**
16+
* Clears all data from the Firebase Auth Emulator.
17+
*
18+
* This function calls the emulator's clear data endpoint to remove all accounts,
19+
* OOB codes, and other authentication data. This ensures test isolation by providing
20+
* a clean slate for each test.
21+
*/
22+
fun clearEmulatorData() {
23+
try {
24+
clearAccounts()
25+
} catch (e: Exception) {
26+
println("WARNING: Exception while clearing emulator data: ${e.message}")
27+
}
28+
}
29+
1530
fun clearAccounts() {
1631
httpClient.delete("/emulator/v1/projects/$projectId/accounts") { connection ->
1732
val responseCode = connection.responseCode
@@ -37,6 +52,36 @@ internal class EmulatorAuthApi(
3752
?: throw Exception("No VERIFY_EMAIL OOB code found for user email: $email")
3853
}
3954

55+
fun fetchVerifyPhoneCode(phone: String): String {
56+
val payload =
57+
httpClient.get("/emulator/v1/projects/$projectId/verificationCodes") { connection ->
58+
val responseCode = connection.responseCode
59+
if (responseCode != HttpURLConnection.HTTP_OK) {
60+
throw Exception("Failed to get verification codes: HTTP $responseCode")
61+
}
62+
63+
connection.inputStream.bufferedReader().use { it.readText() }
64+
}
65+
66+
val verificationCodes = JSONObject(payload).optJSONArray("verificationCodes") ?: JSONArray()
67+
68+
return (0 until verificationCodes.length())
69+
.asSequence()
70+
.mapNotNull { index -> verificationCodes.optJSONObject(index) }
71+
.lastOrNull { json ->
72+
val jsonPhone = json.optString("phoneNumber")
73+
// Try matching with and without country code prefix
74+
// The emulator may store the phone with a country code like +1, +49, etc.
75+
jsonPhone.endsWith(phone) ||
76+
phone.endsWith(jsonPhone.removePrefix("+")) ||
77+
jsonPhone == phone ||
78+
jsonPhone == "+$phone"
79+
}
80+
?.optString("code")
81+
?.takeIf { it.isNotBlank() }
82+
?: throw Exception("No phone verification code found for phone: $phone")
83+
}
84+
4085
private fun fetchOobCodes(): JSONArray {
4186
val payload = httpClient.get("/emulator/v1/projects/$projectId/oobCodes") { connection ->
4287
val responseCode = connection.responseCode

e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/EmailAuthScreenTest.kt

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.firebase.ui.auth.compose.configuration.authUIConfiguration
2222
import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider
2323
import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider
2424
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
25+
import com.firebase.ui.auth.compose.testutil.AUTH_STATE_WAIT_TIMEOUT_MS
2526
import com.firebase.ui.auth.compose.testutil.EmulatorAuthApi
2627
import com.firebase.ui.auth.compose.testutil.awaitWithLooper
2728
import com.google.common.truth.Truth.assertThat
@@ -30,8 +31,6 @@ import com.google.firebase.FirebaseOptions
3031
import com.google.firebase.auth.AuthResult
3132
import com.google.firebase.auth.FirebaseUser
3233
import com.google.firebase.auth.actionCodeSettings
33-
import org.json.JSONArray
34-
import org.json.JSONObject
3534
import org.junit.After
3635
import org.junit.Before
3736
import org.junit.Rule
@@ -41,10 +40,6 @@ import org.mockito.MockitoAnnotations
4140
import org.robolectric.RobolectricTestRunner
4241
import org.robolectric.Shadows.shadowOf
4342
import org.robolectric.annotation.Config
44-
import java.net.HttpURLConnection
45-
import java.net.URL
46-
47-
private const val AUTH_STATE_WAIT_TIMEOUT_MS = 5_000L
4843

4944
@Config(sdk = [34])
5045
@RunWith(RobolectricTestRunner::class)
@@ -93,7 +88,7 @@ class EmailAuthScreenTest {
9388
)
9489

9590
// Clear emulator data
96-
clearEmulatorData()
91+
emulatorApi.clearEmulatorData()
9792
}
9893

9994
@After
@@ -102,7 +97,7 @@ class EmailAuthScreenTest {
10297
FirebaseAuthUI.clearInstanceCache()
10398

10499
// Clear emulator data
105-
clearEmulatorData()
100+
emulatorApi.clearEmulatorData()
106101
}
107102

108103
@Test
@@ -525,7 +520,7 @@ class EmailAuthScreenTest {
525520
configuration: AuthUIConfiguration,
526521
onSuccess: ((AuthResult) -> Unit) = {},
527522
onError: ((AuthException) -> Unit) = {},
528-
onCancel: (() -> Unit) = {}
523+
onCancel: (() -> Unit) = {},
529524
) {
530525
EmailAuthScreen(
531526
context = applicationContext,
@@ -583,23 +578,6 @@ class EmailAuthScreenTest {
583578
}
584579
}
585580

586-
/**
587-
* Clears all data from the Firebase Auth Emulator.
588-
*
589-
* This function calls the emulator's clear data endpoint to remove all accounts,
590-
* OOB codes, and other authentication data. This ensures test isolation by providing
591-
* a clean slate for each test.
592-
*/
593-
private fun clearEmulatorData() {
594-
if (::emulatorApi.isInitialized) {
595-
try {
596-
emulatorApi.clearAccounts()
597-
} catch (e: Exception) {
598-
println("WARNING: Exception while clearing emulator data: ${e.message}")
599-
}
600-
}
601-
}
602-
603581
/**
604582
* Ensures a fresh user exists in the Firebase emulator with the given credentials.
605583
* If a user already exists, they will be deleted first.

0 commit comments

Comments
 (0)