diff --git a/app/src/androidTest/java/com/android/periodpals/endtoend/EndToEndAlert.kt b/app/src/androidTest/java/com/android/periodpals/endtoend/EndToEndAlert.kt new file mode 100644 index 00000000..efdb2811 --- /dev/null +++ b/app/src/androidTest/java/com/android/periodpals/endtoend/EndToEndAlert.kt @@ -0,0 +1,474 @@ +package com.android.periodpals.endtoend + +import android.Manifest +import android.util.Log +import androidx.compose.ui.test.assertContentDescriptionEquals +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertHasNoClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import androidx.test.rule.GrantPermissionRule +import com.android.periodpals.BuildConfig +import com.android.periodpals.MainActivity +import com.android.periodpals.model.alert.AlertModelSupabase +import com.android.periodpals.model.alert.AlertViewModel +import com.android.periodpals.model.alert.LIST_OF_PRODUCTS +import com.android.periodpals.model.alert.LIST_OF_URGENCIES +import com.android.periodpals.model.authentication.AuthenticationModelSupabase +import com.android.periodpals.model.authentication.AuthenticationViewModel +import com.android.periodpals.model.location.Location +import com.android.periodpals.model.user.User +import com.android.periodpals.model.user.UserRepositorySupabase +import com.android.periodpals.model.user.UserViewModel +import com.android.periodpals.resources.C +import com.android.periodpals.resources.C.Tag.AlertInputs +import com.android.periodpals.resources.C.Tag.AlertListsScreen +import com.android.periodpals.resources.C.Tag.AlertListsScreen.MyAlertItem +import com.android.periodpals.resources.C.Tag.AuthenticationScreens +import com.android.periodpals.resources.C.Tag.AuthenticationScreens.SignInScreen +import com.android.periodpals.resources.C.Tag.BottomNavigationMenu +import com.android.periodpals.resources.C.Tag.CreateAlertScreen +import com.android.periodpals.resources.C.Tag.EditAlertScreen +import com.android.periodpals.resources.C.Tag.ProfileScreens.ProfileScreen +import com.android.periodpals.ui.navigation.TopLevelDestinations.PROFILE +import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.auth.Auth +import io.github.jan.supabase.createSupabaseClient +import io.github.jan.supabase.postgrest.Postgrest +import io.github.jan.supabase.storage.Storage +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +private const val TAG = "EndToEndAlert" +private const val TIMEOUT = 60_000L + +class EndToEndAlert : TestCase() { + @get:Rule val composeTestRule = createAndroidComposeRule() + @get:Rule + val permissionRule: GrantPermissionRule = + GrantPermissionRule.grant( + Manifest.permission.POST_NOTIFICATIONS, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION) + + companion object { + private val randomNumber = (0..999).random() + private val EMAIL = "e2e.alert.$randomNumber@test.ch" + private const val PASSWORD = "iLoveSwent1234!" + private val NAME = "E2E Alert $randomNumber" + private const val IMAGE_URL = "" + private val DESCRIPTION = "I'm a test user $randomNumber for the alert end-to-end test" + private const val DOB = "31/01/2001" + private const val PREFERRED_DISTANCE = 500 + private val user = + User( + name = NAME, + imageUrl = IMAGE_URL, + description = DESCRIPTION, + dob = DOB, + preferredDistance = PREFERRED_DISTANCE, + ) + + private lateinit var supabaseClient: SupabaseClient + private lateinit var authenticationViewModel: AuthenticationViewModel + private lateinit var userViewModel: UserViewModel + private lateinit var alertViewModel: AlertViewModel + + private const val EDIT_ALERT_INDEX = 0 + private val PRODUCT = LIST_OF_PRODUCTS[1].textId // Pad + private val PRODUCT_EDIT = LIST_OF_PRODUCTS[2].textId // No preference + private val URGENCY = LIST_OF_URGENCIES[0].textId // High + private val URGENCY_EDIT = LIST_OF_URGENCIES[2].textId // Low + private const val MESSAGE = "I need pads urgently" + private const val MESSAGE_EDIT = "I need a tampon or pad, please!" + } + + @Before + fun setUp() = runBlocking { + supabaseClient = + createSupabaseClient( + supabaseUrl = BuildConfig.SUPABASE_URL, + supabaseKey = BuildConfig.SUPABASE_KEY, + ) { + install(Auth) + install(Postgrest) + install(Storage) + } + val authenticationModel = AuthenticationModelSupabase(supabaseClient) + authenticationViewModel = AuthenticationViewModel(authenticationModel) + + val userModel = UserRepositorySupabase(supabaseClient) + userViewModel = UserViewModel(userModel) + + val alertModel = AlertModelSupabase(supabaseClient) + alertViewModel = AlertViewModel(alertModel) + + authenticationViewModel.isUserLoggedIn( + onSuccess = { + Log.d(TAG, "setUp: user is already logged in") + authenticationViewModel.logOut( + onSuccess = { Log.d(TAG, "setUp: successfully logged out") }, + onFailure = { Log.d(TAG, "setUp: failed to log out: ${it.message}") }, + ) + }, + onFailure = { Log.d(TAG, "setUp: failed to check if user is logged in: ${it.message}") }, + ) + + authenticationViewModel.signUpWithEmail( + EMAIL, + PASSWORD, + onSuccess = { + Log.d(TAG, "Successfully signed up with email and password") + userViewModel.saveUser( + user, + onSuccess = { + Log.d(TAG, "Successfully saved user") + authenticationViewModel.logOut() + }, + onFailure = { e: Exception -> Log.e(TAG, "Failed to save user: $e") }, + ) + }, + onFailure = { e: Exception -> Log.e(TAG, "Failed to sign up with email and password: $e") }, + ) + } + + @Test + fun test() = run { + step("User signs in") { + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag(SignInScreen.SCREEN).assertIsDisplayed() + + composeTestRule + .onNodeWithTag(AuthenticationScreens.EMAIL_FIELD) + .performScrollTo() + .assertIsDisplayed() + .performTextInput(EMAIL) + composeTestRule + .onNodeWithTag(AuthenticationScreens.PASSWORD_FIELD) + .performScrollTo() + .assertIsDisplayed() + .performTextInput(PASSWORD) + composeTestRule + .onNodeWithTag(SignInScreen.SIGN_IN_BUTTON) + .performScrollTo() + .assertIsDisplayed() + .performClick() + } + + step("User navigates to CreateAlert screen") { + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule.onNodeWithTag(ProfileScreen.SCREEN).assertIsDisplayed() + true + } catch (e: AssertionError) { + false + } + } + composeTestRule + .onNodeWithTag(BottomNavigationMenu.BOTTOM_NAVIGATION_MENU_ITEM + "Alert") + .assertIsDisplayed() + .performClick() + } + + step("User creates an alert") { + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule.onNodeWithTag(CreateAlertScreen.SCREEN).assertIsDisplayed() + true + } catch (e: AssertionError) { + false + } + } + composeTestRule + .onNodeWithTag(AlertInputs.PRODUCT_FIELD) + .performScrollTo() + .assertIsDisplayed() + .performClick() + composeTestRule.onNodeWithText(PRODUCT).performScrollTo().assertIsDisplayed().performClick() + + composeTestRule + .onNodeWithTag(AlertInputs.URGENCY_FIELD) + .performScrollTo() + .assertIsDisplayed() + .performClick() + composeTestRule.onNodeWithText(URGENCY).performScrollTo().assertIsDisplayed().performClick() + + composeTestRule + .onNodeWithTag(AlertInputs.LOCATION_FIELD) + .performScrollTo() + .assertIsDisplayed() + .performClick() + composeTestRule + .onNodeWithTag(AlertInputs.DROPDOWN_ITEM + AlertInputs.CURRENT_LOCATION) + .performScrollTo() + .assertIsDisplayed() + .performClick() + composeTestRule + .onNodeWithTag(AlertInputs.LOCATION_FIELD) + .performScrollTo() + .assertIsDisplayed() + .assertTextContains(Location.CURRENT_LOCATION_NAME) + + composeTestRule + .onNodeWithTag(AlertInputs.MESSAGE_FIELD) + .performScrollTo() + .assertIsDisplayed() + .performTextInput(MESSAGE) + + composeTestRule + .onNodeWithTag(CreateAlertScreen.SUBMIT_BUTTON) + .performScrollTo() + .assertIsDisplayed() + .performClick() + } + + step("User arrives at AlertLists screen and edits the first alert") { + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule.onNodeWithTag(AlertListsScreen.SCREEN).assertIsDisplayed() + true + } catch (e: AssertionError) { + false + } + } + + composeTestRule.onNodeWithTag(AlertListsScreen.MY_ALERTS_TAB).assertIsSelected() + composeTestRule.onNodeWithTag(AlertListsScreen.PALS_ALERTS_TAB).assertIsNotSelected() + + composeTestRule + .onNodeWithTag( + MyAlertItem.MY_ALERT + EDIT_ALERT_INDEX) // only 1 alert in myAlerts (just created) + .performScrollTo() + .assertIsDisplayed() + .assertHasNoClickAction() + composeTestRule + .onNodeWithTag( + AlertListsScreen.ALERT_PROFILE_PICTURE + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + + composeTestRule + .onNodeWithTag( + AlertListsScreen.ALERT_TIME_AND_LOCATION + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + composeTestRule + .onNodeWithTag( + AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + + composeTestRule + .onNodeWithTag( + AlertListsScreen.ALERT_PRODUCT_TYPE + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + .assertContentDescriptionEquals(PRODUCT) + composeTestRule + .onNodeWithTag(AlertListsScreen.ALERT_URGENCY + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + .assertContentDescriptionEquals(URGENCY) + + composeTestRule + .onNodeWithTag(MyAlertItem.MY_EDIT_BUTTON + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + step("User edits the alert") { + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule.onNodeWithTag(EditAlertScreen.SCREEN).assertIsDisplayed() + true + } catch (e: AssertionError) { + false + } + } + + composeTestRule.onNodeWithTag(AlertInputs.PRODUCT_FIELD).performScrollTo().performClick() + composeTestRule.onNodeWithText(PRODUCT_EDIT).performScrollTo().performClick() + + composeTestRule.onNodeWithTag(AlertInputs.URGENCY_FIELD).performScrollTo().performClick() + composeTestRule.onNodeWithText(URGENCY_EDIT).performScrollTo().performClick() + + composeTestRule + .onNodeWithTag(AlertInputs.MESSAGE_FIELD) + .performScrollTo() + .performTextInput(MESSAGE_EDIT) + + composeTestRule.onNodeWithTag(EditAlertScreen.SAVE_BUTTON).performScrollTo().performClick() + } + + step("User is back at AlertLists screen and deletes the first alert") { + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule.onNodeWithTag(AlertListsScreen.SCREEN).assertIsDisplayed() + true + } catch (e: AssertionError) { + false + } + } + composeTestRule.onNodeWithTag(AlertListsScreen.MY_ALERTS_TAB).assertIsSelected() + composeTestRule.onNodeWithTag(AlertListsScreen.PALS_ALERTS_TAB).assertIsNotSelected() + + composeTestRule + .onNodeWithTag(MyAlertItem.MY_ALERT + EDIT_ALERT_INDEX) + .performScrollTo() + .assertIsDisplayed() + .assertHasNoClickAction() + composeTestRule + .onNodeWithTag( + AlertListsScreen.ALERT_PROFILE_PICTURE + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + + composeTestRule + .onNodeWithTag( + AlertListsScreen.ALERT_TIME_AND_LOCATION + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + composeTestRule + .onNodeWithTag( + AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + + composeTestRule + .onNodeWithTag( + AlertListsScreen.ALERT_PRODUCT_TYPE + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + .assertContentDescriptionEquals(PRODUCT_EDIT) + composeTestRule + .onNodeWithTag(AlertListsScreen.ALERT_URGENCY + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + .assertContentDescriptionEquals(URGENCY_EDIT) + + composeTestRule + .onNodeWithTag(MyAlertItem.MY_EDIT_BUTTON + EDIT_ALERT_INDEX, useUnmergedTree = true) + .performScrollTo() + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + step("User deletes the alert") { + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule.onNodeWithTag(EditAlertScreen.SCREEN).assertIsDisplayed() + true + } catch (e: AssertionError) { + false + } + } + + composeTestRule + .onNodeWithTag(EditAlertScreen.DELETE_BUTTON) + .performScrollTo() + .assertIsDisplayed() + .performClick() + } + + step("User is back at AlertLists screen and the alert is deleted") { + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule.onNodeWithTag(AlertListsScreen.SCREEN).assertIsDisplayed() + true + } catch (e: AssertionError) { + false + } + } + + composeTestRule.onNodeWithTag(AlertListsScreen.MY_ALERTS_TAB).assertIsSelected() + composeTestRule.onNodeWithTag(AlertListsScreen.PALS_ALERTS_TAB).assertIsNotSelected() + + composeTestRule.onNodeWithTag(MyAlertItem.MY_ALERT + EDIT_ALERT_INDEX).assertDoesNotExist() + composeTestRule.onNodeWithTag(AlertListsScreen.NO_ALERTS_CARD).assertIsDisplayed() + } + + step("Navigate back to Profile Screen") { + composeTestRule.waitForIdle() + composeTestRule + .onNodeWithTag(BottomNavigationMenu.BOTTOM_NAVIGATION_MENU_ITEM + PROFILE.textId) + .assertIsDisplayed() + .performClick() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule + .onNodeWithTag(ProfileScreen.NAME_FIELD) + .performScrollTo() + .assertIsDisplayed() + .assertTextEquals(NAME) + true + } catch (e: AssertionError) { + false + } + } + } + + step("User navigates to Settings Screen to delete their account") { + composeTestRule + .onNodeWithTag(C.Tag.TopAppBar.SETTINGS_BUTTON) + .assertIsDisplayed() + .performClick() + + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule + .onAllNodesWithTag(C.Tag.SettingsScreen.SCREEN) + .fetchSemanticsNodes() + .size == 1 + } catch (e: AssertionError) { + false + } + } + composeTestRule.onNodeWithTag(C.Tag.SettingsScreen.SCREEN).assertIsDisplayed() + + composeTestRule + .onNodeWithTag(C.Tag.SettingsScreen.DELETE_ACCOUNT_ICON) + .performScrollTo() + .assertIsDisplayed() + .performClick() + composeTestRule + .onNodeWithTag(C.Tag.SettingsScreen.DELETE_BUTTON) + .assertIsDisplayed() + .performClick() + } + + step("User is lead back to the Sign In Screen") { + composeTestRule.waitForIdle() + composeTestRule.waitUntil(TIMEOUT) { + try { + composeTestRule.onAllNodesWithTag(SignInScreen.SCREEN).fetchSemanticsNodes().size == 1 + } catch (e: AssertionError) { + false + } + } + } + } +} diff --git a/app/src/main/java/com/android/periodpals/ui/alert/AlertLists.kt b/app/src/main/java/com/android/periodpals/ui/alert/AlertLists.kt index e32e1f7f..767f8583 100644 --- a/app/src/main/java/com/android/periodpals/ui/alert/AlertLists.kt +++ b/app/src/main/java/com/android/periodpals/ui/alert/AlertLists.kt @@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -47,6 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -292,9 +293,10 @@ fun AlertListsScreen( if (myAlertsList.isEmpty()) { item { NoAlertDialog(context.getString(R.string.alert_lists_no_my_alerts_dialog)) } } else { - items(myAlertsList) { alert -> + itemsIndexed(myAlertsList) { index, alert -> MyAlertItem( alert = alert, + indexTestTag = index, alertViewModel = alertViewModel, userViewModel = userViewModel, navigationActions = navigationActions, @@ -311,9 +313,10 @@ fun AlertListsScreen( Modifier.padding(bottom = MaterialTheme.dimens.small2) .testTag(AlertListsScreen.ACCEPTED_ALERTS_TEXT)) } - items(acceptedAlerts.value) { alert -> + itemsIndexed(acceptedAlerts.value) { index, alert -> PalsAlertItem( alert = alert, + indexTestTag = index, alertViewModel = alertViewModel, userViewModel = userViewModel, chatViewModel = chatViewModel, @@ -332,9 +335,10 @@ fun AlertListsScreen( if (palsAlertsList.value.isEmpty()) { item { NoAlertDialog(context.getString(R.string.alert_lists_no_pals_alerts_dialog)) } } else { - items(palsAlertsList.value) { alert -> + itemsIndexed(palsAlertsList.value) { index, alert -> PalsAlertItem( alert = alert, + indexTestTag = index, alertViewModel = alertViewModel, userViewModel = userViewModel, chatViewModel = chatViewModel, @@ -352,6 +356,7 @@ fun AlertListsScreen( * profile picture, time, location, product type, urgency, and an edit button. * * @param alert The alert to be displayed. + * @param indexTestTag The index of the alert in the list. * @param alertViewModel The view model for managing alert data. * @param userViewModel The view model fro managing user data * @param navigationActions The navigation actions for handling navigation events. @@ -360,15 +365,15 @@ fun AlertListsScreen( @Composable private fun MyAlertItem( alert: Alert, + indexTestTag: Int, alertViewModel: AlertViewModel, userViewModel: UserViewModel, navigationActions: NavigationActions, context: Context, ) { - val idTestTag = alert.id Card( modifier = - Modifier.fillMaxWidth().wrapContentHeight().testTag(MyAlertItem.MY_ALERT + idTestTag), + Modifier.fillMaxWidth().wrapContentHeight().testTag(MyAlertItem.MY_ALERT + indexTestTag), shape = RoundedCornerShape(size = MaterialTheme.dimens.cardRoundedSize), colors = getPrimaryCardColors(), elevation = CardDefaults.cardElevation(defaultElevation = MaterialTheme.dimens.cardElevation), @@ -385,7 +390,7 @@ private fun MyAlertItem( verticalAlignment = Alignment.CenterVertically, ) { // My profile picture - AlertProfilePicture(alert, userViewModel) + AlertProfilePicture(alert, indexTestTag, userViewModel) Column( modifier = Modifier.fillMaxWidth().wrapContentHeight().weight(1f), @@ -394,10 +399,10 @@ private fun MyAlertItem( Arrangement.spacedBy(MaterialTheme.dimens.small1, Alignment.CenterVertically), ) { // Time, location - AlertTimeAndLocation(alert, idTestTag) + AlertTimeAndLocation(alert, indexTestTag) // Product type and urgency - AlertProductAndUrgency(alert, idTestTag) + AlertProductAndUrgency(alert, indexTestTag) } // Edit alert button @@ -406,7 +411,7 @@ private fun MyAlertItem( alertViewModel.selectAlert(alert) navigationActions.navigateTo(Screen.EDIT_ALERT) }, - modifier = Modifier.wrapContentSize().testTag(MyAlertItem.MY_EDIT_BUTTON + idTestTag), + modifier = Modifier.wrapContentSize().testTag(MyAlertItem.MY_EDIT_BUTTON + indexTestTag), colors = getFilledPrimaryButtonColors(), ) { Row( @@ -439,26 +444,28 @@ private fun MyAlertItem( * buttons. * * @param alert The alert to be displayed. + * @param indexTestTag The id of the alert used to create unique test tags for each alert card. * @param alertViewModel The view model for managing alert data. * @param userViewModel The view model for managing user data. + * @param chatViewModel The view model fro managing the chat * @param isAccepted A boolean indicating whether the alert has been accepted. */ @Composable fun PalsAlertItem( alert: Alert, + indexTestTag: Int, alertViewModel: AlertViewModel, userViewModel: UserViewModel, chatViewModel: ChatViewModel, authenticationViewModel: AuthenticationViewModel, isAccepted: Boolean = false ) { - val idTestTag = alert.id var isClicked by remember { mutableStateOf(false) } val testTag = if (!isAccepted) { - PalsAlertItem.PAL_ALERT + idTestTag + PalsAlertItem.PAL_ALERT + indexTestTag } else { - PalsAlertItem.PAL_ACCEPTED_ALERT + idTestTag + PalsAlertItem.PAL_ACCEPTED_ALERT + indexTestTag } Card( modifier = Modifier.fillMaxWidth().wrapContentHeight().testTag(testTag), @@ -485,7 +492,7 @@ fun PalsAlertItem( Arrangement.spacedBy(MaterialTheme.dimens.small3, Alignment.Start), verticalAlignment = Alignment.CenterVertically) { // Pal's profile picture - AlertProfilePicture(alert, userViewModel) + AlertProfilePicture(alert, indexTestTag, userViewModel) Column( modifier = Modifier.fillMaxWidth().wrapContentHeight().weight(1f), @@ -494,7 +501,7 @@ fun PalsAlertItem( Arrangement.spacedBy(MaterialTheme.dimens.small1, Alignment.CenterVertically), ) { // Pal's time, location - AlertTimeAndLocation(alert, idTestTag) + AlertTimeAndLocation(alert, indexTestTag) // Pal's name Text( @@ -504,7 +511,7 @@ fun PalsAlertItem( modifier = Modifier.fillMaxWidth() .wrapContentHeight() - .testTag(PalsAlertItem.PAL_NAME + idTestTag), + .testTag(PalsAlertItem.PAL_NAME + indexTestTag), ) // Pal's message @@ -516,10 +523,10 @@ fun PalsAlertItem( modifier = Modifier.fillMaxWidth() .wrapContentHeight() - .testTag(PalsAlertItem.PAL_MESSAGE + idTestTag)) + .testTag(PalsAlertItem.PAL_MESSAGE + indexTestTag)) } } - AlertProductAndUrgency(alert, idTestTag) + AlertProductAndUrgency(alert, indexTestTag) } if (isClicked && alert.status == Status.CREATED) { @@ -527,15 +534,17 @@ fun PalsAlertItem( modifier = Modifier.fillMaxWidth() .wrapContentHeight() - .testTag(PalsAlertItem.PAL_DIVIDER + idTestTag), + .testTag(PalsAlertItem.PAL_DIVIDER + indexTestTag), thickness = MaterialTheme.dimens.borderLine, color = MaterialTheme.colorScheme.onSecondaryContainer, ) if (isAccepted) { - AlertUnAcceptButton(alert, onClick = { alertViewModel.unAcceptAlert(alert) }) + AlertUnAcceptButton( + alert, indexTestTag, onClick = { alertViewModel.unAcceptAlert(alert) }) } else { AlertAcceptButtons( alert, + indexTestTag, chatViewModel, authenticationViewModel, userViewModel, @@ -553,11 +562,12 @@ fun PalsAlertItem( * Composable function that displays the profile picture of an alert. * * @param alert The alert to be displayed. + * @param indexTestTag The id of the alert used to create unique test tags for each alert card. * @param userViewModel `UserViewModel` used to fetch the profile picture of the user. */ @OptIn(ExperimentalGlideComposeApi::class) @Composable -private fun AlertProfilePicture(alert: Alert, userViewModel: UserViewModel) { +private fun AlertProfilePicture(alert: Alert, indexTestTag: Int, userViewModel: UserViewModel) { val user = userViewModel.users.value?.find { it.name == alert.name } // TODO: match by uid not by name val imageUrl = user?.imageUrl ?: "" @@ -571,11 +581,12 @@ private fun AlertProfilePicture(alert: Alert, userViewModel: UserViewModel) { GlideImage( model = model ?: DEFAULT_PROFILE_PICTURE, contentDescription = "Profile picture", + contentScale = ContentScale.Crop, modifier = Modifier.size(MaterialTheme.dimens.iconButtonSize) .clip(shape = CircleShape) .wrapContentSize() - .testTag(AlertListsScreen.ALERT_PROFILE_PICTURE + alert.id), + .testTag(AlertListsScreen.ALERT_PROFILE_PICTURE + indexTestTag), ) } @@ -583,16 +594,16 @@ private fun AlertProfilePicture(alert: Alert, userViewModel: UserViewModel) { * Composable function that displays the time and location of an alert. * * @param alert The alert to be displayed. - * @param idTestTag The id of the alert used to create unique test tags for each alert card. + * @param indexTestTag The id of the alert used to create unique test tags for each alert card. */ @Composable -private fun AlertTimeAndLocation(alert: Alert, idTestTag: String) { +private fun AlertTimeAndLocation(alert: Alert, indexTestTag: Int) { val formattedTime = formatAlertTime(alert.createdAt) Text( modifier = Modifier.fillMaxWidth() .wrapContentHeight() - .testTag(AlertListsScreen.ALERT_TIME_AND_LOCATION + idTestTag), + .testTag(AlertListsScreen.ALERT_TIME_AND_LOCATION + indexTestTag), text = "${formattedTime}, ${trimLocationText(Location.fromString(alert.location).name)}", fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Left, @@ -604,14 +615,14 @@ private fun AlertTimeAndLocation(alert: Alert, idTestTag: String) { * Composable function that displays the product type and urgency of an alert. * * @param alert The alert to be displayed. - * @param idTestTag The id of the alert used to create unique test tags for each alert card. + * @param indexTestTag The id of the alert used to create unique test tags for each alert card. */ @Composable -private fun AlertProductAndUrgency(alert: Alert, idTestTag: String) { +private fun AlertProductAndUrgency(alert: Alert, indexTestTag: Int) { Row( modifier = Modifier.wrapContentSize() - .testTag(AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + idTestTag), + .testTag(AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + indexTestTag), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.dimens.small1, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, @@ -619,18 +630,18 @@ private fun AlertProductAndUrgency(alert: Alert, idTestTag: String) { // Product type Icon( painter = painterResource(productToPeriodPalsIcon(alert.product).icon), - contentDescription = "Menstrual Product Type", + contentDescription = productToPeriodPalsIcon(alert.product).textId, modifier = Modifier.size(MaterialTheme.dimens.iconSize) - .testTag(AlertListsScreen.ALERT_PRODUCT_TYPE + idTestTag), + .testTag(AlertListsScreen.ALERT_PRODUCT_TYPE + indexTestTag), ) // Urgency Icon( painter = painterResource(urgencyToPeriodPalsIcon(alert.urgency).icon), - contentDescription = "Urgency of the Alert", + contentDescription = urgencyToPeriodPalsIcon(alert.urgency).textId, modifier = Modifier.size(MaterialTheme.dimens.iconSize) - .testTag(AlertListsScreen.ALERT_URGENCY + idTestTag), + .testTag(AlertListsScreen.ALERT_URGENCY + indexTestTag), ) } } @@ -639,11 +650,12 @@ private fun AlertProductAndUrgency(alert: Alert, idTestTag: String) { * Composable function that displays the accept buttons for a pal's alert. * * @param alert The alert to be accepted. - * @param alertViewModel The view model for managing alert data. + * @param indexTestTag The id of the alert used to create unique test tags for each alert card */ @Composable private fun AlertAcceptButtons( alert: Alert, + indexTestTag: Int, chatViewModel: ChatViewModel, authenticationViewModel: AuthenticationViewModel, userViewModel: UserViewModel, @@ -652,7 +664,9 @@ private fun AlertAcceptButtons( val context = LocalContext.current Row( modifier = - Modifier.fillMaxWidth().wrapContentHeight().testTag(PalsAlertItem.PAL_BUTTONS + alert.id), + Modifier.fillMaxWidth() + .wrapContentHeight() + .testTag(PalsAlertItem.PAL_BUTTONS + indexTestTag), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.dimens.small2, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically, @@ -687,13 +701,13 @@ private fun AlertAcceptButtons( containerColor = MaterialTheme.colorScheme.tertiary, contentColor = MaterialTheme.colorScheme.onTertiary, ), - testTag = PalsAlertItem.PAL_ACCEPT_BUTTON + alert.id, + testTag = PalsAlertItem.PAL_ACCEPT_BUTTON + indexTestTag, ) } } @Composable -private fun AlertUnAcceptButton(alert: Alert, onClick: (Alert) -> Unit) { +private fun AlertUnAcceptButton(alert: Alert, indexTestTag: Int, onClick: (Alert) -> Unit) { val context = LocalContext.current Row( modifier = @@ -713,7 +727,7 @@ private fun AlertUnAcceptButton(alert: Alert, onClick: (Alert) -> Unit) { containerColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.onError, ), - testTag = PalsAlertItem.PAL_UNACCEPT_BUTTON + alert.id, + testTag = PalsAlertItem.PAL_UNACCEPT_BUTTON + indexTestTag, ) } } diff --git a/app/src/test/java/com/android/periodpals/ui/alert/AlertListsScreenTest.kt b/app/src/test/java/com/android/periodpals/ui/alert/AlertListsScreenTest.kt index ed9203ef..3784aed3 100644 --- a/app/src/test/java/com/android/periodpals/ui/alert/AlertListsScreenTest.kt +++ b/app/src/test/java/com/android/periodpals/ui/alert/AlertListsScreenTest.kt @@ -342,32 +342,30 @@ class AlertListsScreenTest { composeTestRule.onNodeWithTag(AlertListsScreen.PALS_ALERTS_TAB).assertIsNotSelected() composeTestRule.onNodeWithTag(AlertListsScreen.NO_ALERTS_CARD).assertDoesNotExist() - MY_ALERTS_LIST.forEach { alert -> - val alertId: String = alert.id + MY_ALERTS_LIST.forEachIndexed { index, alert -> composeTestRule - .onNodeWithTag(MyAlertItem.MY_ALERT + alertId) + .onNodeWithTag(MyAlertItem.MY_ALERT + index) .performScrollTo() .assertIsDisplayed() .assertHasNoClickAction() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_TIME_AND_LOCATION + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_TIME_AND_LOCATION + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag( - AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_TYPE + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_TYPE + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_URGENCY + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_URGENCY + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(MyAlertItem.MY_EDIT_BUTTON + alertId, useUnmergedTree = true) + .onNodeWithTag(MyAlertItem.MY_EDIT_BUTTON + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() .assertHasClickAction() @@ -390,8 +388,8 @@ class AlertListsScreenTest { composeTestRule.onNodeWithTag(AlertListsScreen.MY_ALERTS_TAB).assertIsSelected() - val alertId = MY_ALERTS_LIST.first().id - composeTestRule.onNodeWithTag(MyAlertItem.MY_EDIT_BUTTON + alertId).performClick() + val index = 0 + composeTestRule.onNodeWithTag(MyAlertItem.MY_EDIT_BUTTON + index).performClick() verify(navigationActions).navigateTo(Screen.EDIT_ALERT) } @@ -477,50 +475,48 @@ class AlertListsScreenTest { composeTestRule.onNodeWithTag(AlertListsScreen.MY_ALERTS_TAB).assertIsNotSelected() composeTestRule.onNodeWithTag(AlertListsScreen.NO_ALERTS_CARD).assertDoesNotExist() - PALS_ALERTS_LIST.value.forEach { alert -> - val alertId: String = alert.id + PALS_ALERTS_LIST.value.forEachIndexed() { index, alert -> composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ALERT + alertId) + .onNodeWithTag(PalsAlertItem.PAL_ALERT + index) .performScrollTo() .assertIsDisplayed() .assertHasClickAction() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_PROFILE_PICTURE + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_PROFILE_PICTURE + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_TIME_AND_LOCATION + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_TIME_AND_LOCATION + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_NAME + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_NAME + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag( - AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_TYPE + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_TYPE + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_URGENCY + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_URGENCY + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_MESSAGE + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_MESSAGE + index, useUnmergedTree = true) .assertIsNotDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_DIVIDER + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_DIVIDER + index, useUnmergedTree = true) .assertIsNotDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_BUTTONS + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_BUTTONS + index, useUnmergedTree = true) .assertIsNotDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + index, useUnmergedTree = true) .assertIsNotDisplayed() } } @@ -548,70 +544,68 @@ class AlertListsScreenTest { composeTestRule.onNodeWithTag(AlertListsScreen.MY_ALERTS_TAB).assertIsNotSelected() composeTestRule.onNodeWithTag(AlertListsScreen.NO_ALERTS_CARD).assertDoesNotExist() - PALS_ALERTS_LIST.value.forEach { alert -> - val alertId: String = alert.id + PALS_ALERTS_LIST.value.forEachIndexed { index, alert -> composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ALERT + alertId) + .onNodeWithTag(PalsAlertItem.PAL_ALERT + index) .performScrollTo() .assertIsDisplayed() .assertHasClickAction() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_PROFILE_PICTURE + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_PROFILE_PICTURE + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_TIME_AND_LOCATION + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_TIME_AND_LOCATION + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_NAME + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_NAME + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag( - AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_AND_URGENCY + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_TYPE + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_PRODUCT_TYPE + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(AlertListsScreen.ALERT_URGENCY + alertId, useUnmergedTree = true) + .onNodeWithTag(AlertListsScreen.ALERT_URGENCY + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() // First click to display the alert's details - composeTestRule.onNodeWithTag(PalsAlertItem.PAL_ALERT + alertId).performClick() + composeTestRule.onNodeWithTag(PalsAlertItem.PAL_ALERT + index).performClick() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_MESSAGE + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_MESSAGE + index, useUnmergedTree = true) .assertIsDisplayed() if (alert.status == Status.CREATED) { composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_DIVIDER + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_DIVIDER + index, useUnmergedTree = true) .assertIsDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_BUTTONS + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_BUTTONS + index, useUnmergedTree = true) .assertIsDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + index, useUnmergedTree = true) .assertIsDisplayed() .assertHasClickAction() } // Second click to toggle the alert - composeTestRule.onNodeWithTag(PalsAlertItem.PAL_ALERT + alertId).performClick() + composeTestRule.onNodeWithTag(PalsAlertItem.PAL_ALERT + index).performClick() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_MESSAGE + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_MESSAGE + index, useUnmergedTree = true) .assertIsNotDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_DIVIDER + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_DIVIDER + index, useUnmergedTree = true) .assertIsNotDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_BUTTONS + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_BUTTONS + index, useUnmergedTree = true) .assertIsNotDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + index, useUnmergedTree = true) .assertIsNotDisplayed() } } @@ -642,15 +636,15 @@ class AlertListsScreenTest { .performClick() .assertIsSelected() - val alertId = PALS_ALERTS_LIST.value.first().id + val index = 0 println("Before accepting alert: ${alertViewModel.palAlerts.value}") composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ALERT + alertId) + .onNodeWithTag(PalsAlertItem.PAL_ALERT + index) .performScrollTo() .assertIsDisplayed() .performClick() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + index, useUnmergedTree = true) .performScrollTo() .performClick() @@ -662,16 +656,13 @@ class AlertListsScreenTest { .performScrollTo() .assertIsDisplayed() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ACCEPTED_ALERT + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_ACCEPTED_ALERT + index, useUnmergedTree = true) .performScrollTo() .assertIsDisplayed() composeTestRule .onNodeWithTag(AlertListsScreen.ACCEPTED_ALERTS_DIVIDER) .performScrollTo() .assertIsDisplayed() - composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ALERT + alertId, useUnmergedTree = true) - .assertDoesNotExist() } @Test @@ -702,7 +693,7 @@ class AlertListsScreenTest { .performClick() .assertIsSelected() - val alertId = acceptedAlerts.value.first().id + val alertId = 0 composeTestRule .onNodeWithTag(AlertListsScreen.ACCEPTED_ALERTS_TEXT) @@ -861,11 +852,11 @@ class AlertListsScreenTest { .assertIsSelected() composeTestRule.onNodeWithTag(AlertListsScreen.MY_ALERTS_TAB).assertIsNotSelected() - val alertId = PALS_ALERTS_LIST.value[0].id - composeTestRule.onNodeWithTag(PalsAlertItem.PAL_ALERT + alertId).performClick() + val index = 0 + composeTestRule.onNodeWithTag(PalsAlertItem.PAL_ALERT + index).performClick() composeTestRule - .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + alertId, useUnmergedTree = true) + .onNodeWithTag(PalsAlertItem.PAL_ACCEPT_BUTTON + index, useUnmergedTree = true) .assertIsDisplayed() .performClick()