diff --git a/app/src/androidTest/java/com/android/periodpals/ui/profile/EditProfileTest.kt b/app/src/androidTest/java/com/android/periodpals/ui/profile/EditProfileTest.kt new file mode 100644 index 000000000..b730b01f7 --- /dev/null +++ b/app/src/androidTest/java/com/android/periodpals/ui/profile/EditProfileTest.kt @@ -0,0 +1,156 @@ +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.periodpals.resources.C.Tag.BottomNavigationMenu +import com.android.periodpals.resources.C.Tag.EditProfileScreen +import com.android.periodpals.resources.C.Tag.TopAppBar +import com.android.periodpals.ui.navigation.NavigationActions +import com.android.periodpals.ui.navigation.Route +import com.android.periodpals.ui.navigation.Screen +import com.android.periodpals.ui.profile.EditProfileScreen +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.never + +@RunWith(AndroidJUnit4::class) +class EditProfileTest { + + private lateinit var navigationActions: NavigationActions + @get:Rule val composeTestRule = createComposeRule() + + @Before + fun setUp() { + navigationActions = mock(NavigationActions::class.java) + `when`(navigationActions.currentRoute()).thenReturn(Route.PROFILE) + composeTestRule.setContent { EditProfileScreen(navigationActions) } + } + + @Test + fun allComponentsAreDisplayed() { + composeTestRule.onNodeWithTag(EditProfileScreen.SCREEN).assertIsDisplayed() + composeTestRule.onNodeWithTag(EditProfileScreen.PROFILE_PICTURE).assertIsDisplayed() + composeTestRule.onNodeWithTag(EditProfileScreen.EDIT_ICON).assertIsDisplayed() + composeTestRule.onNodeWithTag(EditProfileScreen.MANDATORY_FIELD).assertIsDisplayed() + composeTestRule.onNodeWithTag(EditProfileScreen.EMAIL_FIELD).assertIsDisplayed() + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).assertIsDisplayed() + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).assertIsDisplayed() + composeTestRule.onNodeWithTag(EditProfileScreen.YOUR_PROFILE).assertIsDisplayed() + composeTestRule.onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD).assertIsDisplayed() + composeTestRule + .onNodeWithTag(EditProfileScreen.SAVE_BUTTON) + .assertIsDisplayed() + .assertTextEquals("Save") + composeTestRule.onNodeWithTag(TopAppBar.TOP_BAR).assertIsDisplayed() + composeTestRule + .onNodeWithTag(TopAppBar.TITLE_TEXT) + .assertIsDisplayed() + .assertTextEquals("Edit Your Profile") + composeTestRule.onNodeWithTag(TopAppBar.GO_BACK_BUTTON).assertIsDisplayed() + composeTestRule.onNodeWithTag(TopAppBar.EDIT_BUTTON).assertIsNotDisplayed() + composeTestRule.onNodeWithTag(BottomNavigationMenu.BOTTOM_NAVIGATION_MENU).assertDoesNotExist() + } + + @Test + fun editValidProfile() { + composeTestRule.onNodeWithTag(EditProfileScreen.EMAIL_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD).performTextClearance() + composeTestRule + .onNodeWithTag(EditProfileScreen.EMAIL_FIELD) + .performTextInput("john.doe@example.com") + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextInput("John Doe") + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextInput("01/01/1990") + composeTestRule + .onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD) + .performTextInput("A short bio") + composeTestRule.onNodeWithTag(EditProfileScreen.SAVE_BUTTON).performClick() + verify(navigationActions).navigateTo(Screen.PROFILE) + } + + @Test + fun editInvalidProfileNoName() { + composeTestRule.onNodeWithTag(EditProfileScreen.EMAIL_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD).performTextClearance() + composeTestRule + .onNodeWithTag(EditProfileScreen.EMAIL_FIELD) + .performTextInput("john.doe@example.com") + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextInput("01/01/1990") + composeTestRule + .onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD) + .performTextInput("A short bio") + composeTestRule.onNodeWithTag(EditProfileScreen.SAVE_BUTTON).performClick() + verify(navigationActions, never()).navigateTo(any()) + } + + @Test + fun editInvalidProfileNoDOB() { + composeTestRule.onNodeWithTag(EditProfileScreen.EMAIL_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD).performTextClearance() + composeTestRule + .onNodeWithTag(EditProfileScreen.EMAIL_FIELD) + .performTextInput("john.doe@example.com") + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextInput("John Doe") + composeTestRule + .onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD) + .performTextInput("A short bio") + composeTestRule.onNodeWithTag(EditProfileScreen.SAVE_BUTTON).performClick() + verify(navigationActions, never()).navigateTo(any()) + } + + @Test + fun editInvalidProfileNoDescription() { + composeTestRule.onNodeWithTag(EditProfileScreen.EMAIL_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD).performTextClearance() + composeTestRule + .onNodeWithTag(EditProfileScreen.EMAIL_FIELD) + .performTextInput("john.doe@example.com") + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextInput("John Doe") + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextInput("01/01/1990") + composeTestRule.onNodeWithTag(EditProfileScreen.SAVE_BUTTON).performClick() + verify(navigationActions, never()).navigateTo(any()) + } + + @Test + fun editInvalidProfileNoEmail() { + composeTestRule.onNodeWithTag(EditProfileScreen.EMAIL_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextInput("John Doe") + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextInput("01/01/1990") + composeTestRule + .onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD) + .performTextInput("A short bio") + composeTestRule.onNodeWithTag(EditProfileScreen.SAVE_BUTTON).performClick() + verify(navigationActions, never()).navigateTo(any()) + } + + @Test + fun editInvalidProfileAllEmptyFields() { + composeTestRule.onNodeWithTag(EditProfileScreen.EMAIL_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.NAME_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DOB_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.DESCRIPTION_FIELD).performTextClearance() + composeTestRule.onNodeWithTag(EditProfileScreen.SAVE_BUTTON).performClick() + verify(navigationActions, never()).navigateTo(any()) + } +} diff --git a/app/src/main/java/com/android/periodpals/resources/C.kt b/app/src/main/java/com/android/periodpals/resources/C.kt index 950497c2a..bdc0c9d2d 100644 --- a/app/src/main/java/com/android/periodpals/resources/C.kt +++ b/app/src/main/java/com/android/periodpals/resources/C.kt @@ -108,6 +108,15 @@ object C { /** Constants for tagging UI components in the EditProfileScreen. */ object EditProfileScreen { const val SCREEN = "screen" + const val PROFILE_PICTURE = "profilePicture" + const val EDIT_ICON = "editIcon" + const val MANDATORY_FIELD = "mandatoryField" + const val EMAIL_FIELD = "emailField" + const val NAME_FIELD = "nameField" + const val DOB_FIELD = "dobField" + const val YOUR_PROFILE = "yourProfile" + const val DESCRIPTION_FIELD = "descriptionField" + const val SAVE_BUTTON = "saveButton" } /** Constants for tagging UI components in the ProfileScreen. */ diff --git a/app/src/main/java/com/android/periodpals/ui/components/ProfileComponents.kt b/app/src/main/java/com/android/periodpals/ui/components/ProfileComponents.kt new file mode 100644 index 000000000..55062dcd5 --- /dev/null +++ b/app/src/main/java/com/android/periodpals/ui/components/ProfileComponents.kt @@ -0,0 +1,21 @@ +package com.android.periodpals.ui.components + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +/** + * A composable that displays an instruction text with [text] and [testTag] for testing purposes. + */ +@Composable +fun ProfileSection(text: String, testTag: String) { + Text( + modifier = Modifier.testTag(testTag), + text = text, + style = + MaterialTheme.typography.bodyLarge.copy(fontSize = 20.sp, fontWeight = FontWeight.Medium)) +} diff --git a/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt b/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt index bdd5925b7..2c201c0b7 100644 --- a/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt +++ b/app/src/main/java/com/android/periodpals/ui/profile/EditProfile.kt @@ -1,35 +1,222 @@ package com.android.periodpals.ui.profile +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.android.periodpals.ui.navigation.BottomNavigationMenu -import com.android.periodpals.ui.navigation.LIST_TOP_LEVEL_DESTINATION +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.android.periodpals.R +import com.android.periodpals.resources.C.Tag.EditProfileScreen.DESCRIPTION_FIELD +import com.android.periodpals.resources.C.Tag.EditProfileScreen.DOB_FIELD +import com.android.periodpals.resources.C.Tag.EditProfileScreen.EDIT_ICON +import com.android.periodpals.resources.C.Tag.EditProfileScreen.EMAIL_FIELD +import com.android.periodpals.resources.C.Tag.EditProfileScreen.MANDATORY_FIELD +import com.android.periodpals.resources.C.Tag.EditProfileScreen.NAME_FIELD +import com.android.periodpals.resources.C.Tag.EditProfileScreen.PROFILE_PICTURE +import com.android.periodpals.resources.C.Tag.EditProfileScreen.SAVE_BUTTON +import com.android.periodpals.resources.C.Tag.EditProfileScreen.SCREEN +import com.android.periodpals.resources.C.Tag.EditProfileScreen.YOUR_PROFILE +import com.android.periodpals.ui.components.ProfileSection import com.android.periodpals.ui.navigation.NavigationActions +import com.android.periodpals.ui.navigation.Screen import com.android.periodpals.ui.navigation.TopAppBar +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage private const val SCREEN_TITLE = "Edit Your Profile" /* Placeholder Screen, waiting for implementation */ +@OptIn(ExperimentalGlideComposeApi::class) @Composable fun EditProfileScreen(navigationActions: NavigationActions) { + // State variables, to remplace it with the real data + var email by remember { mutableStateOf("emilia.jones@email.com") } + var name by remember { mutableStateOf("Emilia Jones") } + var dob by remember { mutableStateOf("20/01/2001") } + var description by remember { + mutableStateOf( + "Hello guys :) I’m Emilia, I’m a student " + + "at EPFL and I’m here to participate and contribute to this amazing community !") + } + + var profileImageUri by remember { + mutableStateOf( + Uri.parse("android.resource://com.android.periodpals/" + R.drawable.generic_avatar)) + } + + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + profileImageUri = result.data?.data + } + } + + val context = LocalContext.current + Scaffold( - bottomBar = ({ - BottomNavigationMenu( - onTabSelect = { route -> navigationActions.navigateTo(route) }, - tabList = LIST_TOP_LEVEL_DESTINATION, - selectedItem = navigationActions.currentRoute()) - }), + modifier = Modifier.fillMaxSize().testTag(SCREEN), topBar = { TopAppBar( title = SCREEN_TITLE, true, - onBackButtonClick = { navigationActions.navigateTo("profile") }) + onBackButtonClick = { navigationActions.navigateTo(Screen.PROFILE) }) }, content = { pd -> - Text("Edit Profile Screen", modifier = Modifier.fillMaxSize().padding(pd)) + Column( + modifier = Modifier.padding(pd).padding(24.dp).fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + // Profile image section + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Box { + GlideImage( + model = profileImageUri, + modifier = + Modifier.padding(1.dp) + .clip(shape = RoundedCornerShape(100.dp)) + .size(190.dp) + .testTag(PROFILE_PICTURE) + .background( + color = MaterialTheme.colorScheme.background, + shape = RoundedCornerShape(100.dp)), + contentDescription = "image profile", + contentScale = ContentScale.Crop, + ) + + Icon( + Icons.Outlined.Edit, + contentDescription = "change profile picture", + modifier = + Modifier.align(Alignment.TopEnd) + .size(40.dp) + .background(color = Color(0xFF79747E), shape = CircleShape) + .padding(4.dp) + .testTag(EDIT_ICON) + .clickable { + val pickImageIntent = + Intent(Intent.ACTION_PICK).apply { type = "image/*" } + launcher.launch(pickImageIntent) + }, + ) + } + } + + // Section title + ProfileSection("Mandatory Fields", MANDATORY_FIELD) + + // Email input field + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + placeholder = { Text("Enter your email") }, + modifier = Modifier.testTag(EMAIL_FIELD).fillMaxWidth(), + ) + + // Name input field + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") }, + placeholder = { Text("Enter your name") }, + modifier = Modifier.testTag(NAME_FIELD).fillMaxWidth(), + ) + + // Date of Birth input field + OutlinedTextField( + value = dob, + onValueChange = { dob = it }, + label = { Text("Date of birth") }, + placeholder = { Text("DD/MM/YYYY") }, + modifier = Modifier.testTag(DOB_FIELD).fillMaxWidth(), + ) + + // Section title + ProfileSection("Your Profile: ", YOUR_PROFILE) + + // Description input field + OutlinedTextField( + value = description, + onValueChange = { description = it }, + label = { Text("Description") }, + placeholder = { Text("Enter a description") }, + modifier = Modifier.height(124.dp).testTag(DESCRIPTION_FIELD), + ) + + // Save Changes button + Button( + onClick = { + val errorMessage = validateFields(email, name, dob, description) + if (errorMessage != null) { + Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() + } else { + // Save the profile (future implementation) + Toast.makeText(context, "Profile saved", Toast.LENGTH_SHORT).show() + navigationActions.navigateTo(Screen.PROFILE) + } + }, + enabled = true, + modifier = + Modifier.padding(1.dp).testTag(SAVE_BUTTON).align(Alignment.CenterHorizontally), + ) { + Text("Save") + } + } }) } + +/** Validates the fields of the profile screen. */ +private fun validateFields(email: String, name: String, dob: String, description: String): String? { + return when { + validateEmail(email).isNotEmpty() -> validateEmail(email) + name.isEmpty() -> "Please enter a name" + !validateDate(dob) -> "Invalid date" + description.isEmpty() -> "Please enter a description" + else -> null + } +} + +/** Validates the email and returns an error message if the email is invalid. */ +private fun validateEmail(email: String): String { + return when { + email.isEmpty() -> "Please enter an email" + !email.contains("@") -> "Email must contain @" + else -> "" + } +}