@@ -33,6 +33,7 @@ import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringPr
3333import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
3434import com.firebase.ui.auth.compose.testutil.AUTH_STATE_WAIT_TIMEOUT_MS
3535import com.firebase.ui.auth.compose.testutil.EmulatorAuthApi
36+ import com.firebase.ui.auth.compose.testutil.ensureFreshUser
3637import com.google.common.truth.Truth.assertThat
3738import com.google.firebase.FirebaseApp
3839import com.google.firebase.FirebaseOptions
@@ -275,6 +276,133 @@ class AnonymousAuthScreenTest {
275276 .isEqualTo(email)
276277 }
277278
279+ @Test
280+ fun `anonymous upgrade with existing email shows dialog with AccountLinking message` () {
281+ val name = " Existing User"
282+ 283+ val password = " Password123!"
284+
285+ // Step 1: Create an email/password account first
286+ println (" TEST: Creating email/password account..." )
287+ ensureFreshUser(authUI, email, password)
288+ println (" TEST: Email/password account created" )
289+
290+ // Step 2: Sign out
291+ authUI.auth.signOut()
292+ shadowOf(Looper .getMainLooper()).idle()
293+ assertThat(authUI.auth.currentUser).isNull()
294+ println (" TEST: Signed out, but email account still exists in emulator" )
295+
296+ // Step 3: Sign in anonymously
297+ val configuration = authUIConfiguration {
298+ context = applicationContext
299+ providers {
300+ provider(AuthProvider .Anonymous )
301+ provider(
302+ AuthProvider .Email (
303+ emailLinkActionCodeSettings = null ,
304+ passwordValidationRules = emptyList()
305+ )
306+ )
307+ }
308+ isAnonymousUpgradeEnabled = true
309+ }
310+
311+ var currentAuthState: AuthState = AuthState .Idle
312+
313+ composeTestRule.setContent {
314+ TestAuthScreen (configuration = configuration)
315+ val authState by authUI.authStateFlow().collectAsState(AuthState .Idle )
316+ currentAuthState = authState
317+ }
318+
319+ composeTestRule.waitForIdle()
320+ shadowOf(Looper .getMainLooper()).idle()
321+
322+ println (" TEST: Clicking anonymous sign-in button..." )
323+ composeTestRule.onNodeWithText(stringProvider.signInAnonymously)
324+ .assertIsDisplayed()
325+ .performClick()
326+
327+ composeTestRule.waitForIdle()
328+ shadowOf(Looper .getMainLooper()).idle()
329+
330+ // Wait for anonymous auth to complete
331+ println (" TEST: Waiting for anonymous auth..." )
332+ composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS ) {
333+ shadowOf(Looper .getMainLooper()).idle()
334+ println (" TEST: Auth state: $currentAuthState " )
335+ currentAuthState is AuthState .Success
336+ }
337+
338+ assertThat(authUI.auth.currentUser!! .isAnonymous).isTrue()
339+ val anonymousUserUID = authUI.auth.currentUser!! .uid
340+ println (" TEST: Anonymous user UID: $anonymousUserUID " )
341+
342+ // Step 4: Try to upgrade with existing email
343+ println (" TEST: Clicking 'Upgrade with Email' button..." )
344+ composeTestRule.onNodeWithText(" Upgrade with Email" )
345+ .assertIsDisplayed()
346+ .performClick()
347+
348+ composeTestRule.waitForIdle()
349+ shadowOf(Looper .getMainLooper()).idle()
350+
351+ // Navigate to sign-up
352+ println (" TEST: Navigating to sign-up..." )
353+ composeTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase())
354+ .assertIsDisplayed()
355+ .performClick()
356+
357+ // Enter the existing email
358+ println (" TEST: Entering existing email..." )
359+ composeTestRule.onNodeWithText(stringProvider.emailHint)
360+ .assertIsDisplayed()
361+ .performTextInput(email)
362+ composeTestRule.onNodeWithText(stringProvider.nameHint)
363+ .assertIsDisplayed()
364+ .performTextInput(name)
365+ composeTestRule.onNodeWithText(stringProvider.passwordHint)
366+ .performScrollTo()
367+ .assertIsDisplayed()
368+ .performTextInput(password)
369+ composeTestRule.onNodeWithText(stringProvider.confirmPasswordHint)
370+ .performScrollTo()
371+ .assertIsDisplayed()
372+ .performTextInput(password)
373+
374+ // Click sign-up button
375+ println (" TEST: Clicking sign-up button..." )
376+ composeTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase())
377+ .performScrollTo()
378+ .assertIsDisplayed()
379+ .performClick()
380+
381+ composeTestRule.waitForIdle()
382+ shadowOf(Looper .getMainLooper()).idle()
383+
384+ // Step 5: Wait for error state (AccountLinkingRequiredException)
385+ println (" TEST: Waiting for AccountLinkingRequiredException..." )
386+ composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS ) {
387+ shadowOf(Looper .getMainLooper()).idle()
388+ println (" TEST: Auth state: $currentAuthState " )
389+ currentAuthState is AuthState .Error
390+ }
391+
392+ // Step 6: Verify ErrorRecoveryDialog is displayed
393+ println (" TEST: Verifying ErrorRecoveryDialog is displayed..." )
394+ composeTestRule.onNodeWithText(stringProvider.errorDialogTitle)
395+ .assertIsDisplayed()
396+
397+ // Verify exception
398+ assertThat(currentAuthState).isInstanceOf(AuthState .Error ::class .java)
399+ val errorState = currentAuthState as AuthState .Error
400+ assertThat(errorState.exception).isInstanceOf(AuthException .AccountLinkingRequiredException ::class .java)
401+
402+ val linkingException = errorState.exception as AuthException .AccountLinkingRequiredException
403+ assertThat(linkingException.email).isEqualTo(email)
404+ }
405+
278406 @Composable
279407 private fun TestAuthScreen (configuration : AuthUIConfiguration ) {
280408 composeTestRule.waitForIdle()
0 commit comments