From 6149d5d60071e8ce2e448056d12b4d6064d5cc0e Mon Sep 17 00:00:00 2001 From: Ismaillat Date: Wed, 11 Dec 2024 17:42:51 +0100 Subject: [PATCH 1/6] feat: add edit description feature and related tests --- .../lookup/ui/image/EditImageKtTest.kt | 50 +++++++++++ .../lookup/ui/profile/CollectionKtTest.kt | 1 + .../lookupgroup27/lookup/MainActivity.kt | 8 +- .../lookup/model/post/PostsRepository.kt | 14 ++++ .../model/post/PostsRepositoryFirestore.kt | 19 +++++ .../lookup/ui/image/EditImage.kt | 70 +++++++++++++++- .../lookup/ui/navigation/NavigationActions.kt | 4 +- .../lookup/ui/post/PostsViewModel.kt | 16 ++++ .../lookup/ui/profile/Collection.kt | 1 + .../post/PostsRepositoryFirestoreTest.kt | 47 +++++++++++ .../lookup/ui/post/PostsViewModelTest.kt | 84 +++++++++++++++++++ 11 files changed, 307 insertions(+), 7 deletions(-) diff --git a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt index 7fcdf35dd..0b7a0d66e 100644 --- a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt +++ b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt @@ -65,6 +65,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -85,6 +86,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -106,6 +108,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -124,6 +127,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -146,6 +150,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -166,6 +171,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -187,6 +193,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -205,6 +212,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -223,6 +231,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -241,6 +250,7 @@ class EditImageScreenTest { postAverageStar = 0.0, postRatedByNb = 0, postUid = "mock_uid", + postDescription = "mock_description", editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, navigationActions = mockNavigationActions, @@ -249,4 +259,44 @@ class EditImageScreenTest { composeTestRule.onNodeWithTag("rated_by_collection").assertIsDisplayed() } + + @Test + fun testDescriptionBoxIsDisplayed() { + composeTestRule.setContent { + EditImageScreen( + postUri = "mock_image_url", + postAverageStar = 4.5, + postRatedByNb = 20, + postUid = "mock_uid", + postDescription = "mock_description", + editImageViewModel = editImageViewModel, + collectionViewModel = collectionViewModel, + navigationActions = mockNavigationActions, + postsViewModel = postsViewModel) + } + + composeTestRule.onNodeWithTag("description_text").assertIsDisplayed() + } + + @Test + fun testEditFieldAppearsOnClick() { + composeTestRule.setContent { + EditImageScreen( + postUri = "mock_image_url", + postAverageStar = 4.5, + postRatedByNb = 20, + postUid = "mock_uid", + postDescription = "mock_description", + editImageViewModel = editImageViewModel, + collectionViewModel = collectionViewModel, + navigationActions = mockNavigationActions, + postsViewModel = postsViewModel) + } + + // Simulate clicking the description box + composeTestRule.onNodeWithTag("description_text").performClick() + + // Verify that the edit field appears + composeTestRule.onNodeWithTag("edit_description_field").assertIsDisplayed() + } } diff --git a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/profile/CollectionKtTest.kt b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/profile/CollectionKtTest.kt index 7a777ca74..6a4b70c45 100644 --- a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/profile/CollectionKtTest.kt +++ b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/profile/CollectionKtTest.kt @@ -205,6 +205,7 @@ class CollectionScreenTest { postAverageStar: Float, postRatedByNb: Int, postUid: String, + postDescription: String, route: String ) { if (route == Route.EDIT_IMAGE && diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/MainActivity.kt b/app/src/main/java/com/github/lookupgroup27/lookup/MainActivity.kt index c6428629c..a20a1f639 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/MainActivity.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/MainActivity.kt @@ -142,17 +142,20 @@ fun LookUpApp() { } composable( - route = "${Route.EDIT_IMAGE}/{postUri}/{postAverageStar}/{postRatedByNb}/{postUid}", + route = + "${Route.EDIT_IMAGE}/{postUri}/{postAverageStar}/{postRatedByNb}/{postUid}/{postDescription}", arguments = listOf( navArgument("postUri") { type = NavType.StringType }, navArgument("postAverageStar") { type = NavType.FloatType }, navArgument("postRatedByNb") { type = NavType.IntType }, - navArgument("postUid") { type = NavType.StringType })) { backStackEntry -> + navArgument("postUid") { type = NavType.StringType }, + navArgument("postDescription") { type = NavType.StringType })) { backStackEntry -> val postUri = backStackEntry.arguments?.getString("postUri") ?: "" val postAverageStar = backStackEntry.arguments?.getFloat("postAverageStar") ?: 0.0f val postRatedByNb = backStackEntry.arguments?.getInt("postRatedByNb") ?: 0 val postUid = backStackEntry.arguments?.getString("postUid") ?: "" + val postDescription = backStackEntry.arguments?.getString("postDescription") ?: "" EditImageScreen( postUri = postUri, @@ -162,6 +165,7 @@ fun LookUpApp() { editImageViewModel = editImageViewModel, collectionViewModel = collectionViewModel, postsViewModel = postsViewModel, + postDescription = postDescription, navigationActions = navigationActions) } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepository.kt b/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepository.kt index 54c44fc08..9c44eeb0c 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepository.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepository.kt @@ -60,4 +60,18 @@ interface PostsRepository { * @param onFailure Callback function invoked with an exception if the operation fails. */ fun updatePost(post: Post, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) + + /** + * Updates the description of an existing post in the repository. + * + * @param postUid The UID of the post to update. + * @param onSuccess Callback function invoked when the operation is successful. + * @param onFailure Callback function invoked with an exception if the operation fails. + */ + fun updateDescription( + postUid: String, + newDescription: String, + onSuccess: () -> Unit, + onFailure: (Exception) -> Unit + ) } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt b/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt index ffc45193f..ab24b6430 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestore.kt @@ -123,4 +123,23 @@ class PostsRepositoryFirestore(private val db: FirebaseFirestore) : PostsReposit onFailure(it) } } + + override fun updateDescription( + postUid: String, + newDescription: String, + onSuccess: () -> Unit, + onFailure: (Exception) -> Unit + ) { + collection + .document(postUid) + .update("description", newDescription) + .addOnSuccessListener { + Log.d(tag, "Description updated successfully") + onSuccess() + } + .addOnFailureListener { + Log.e(tag, "Error updating description", it) + onFailure(it) + } + } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt index af926bb76..e98bfe18e 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt @@ -3,7 +3,10 @@ package com.github.lookupgroup27.lookup.ui.image import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Person @@ -12,6 +15,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.BiasAlignment import androidx.compose.ui.Modifier @@ -21,6 +27,10 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import com.github.lookupgroup27.lookup.R @@ -51,6 +61,7 @@ fun EditImageScreen( postAverageStar: Double, postRatedByNb: Int, postUid: String, + postDescription: String, editImageViewModel: EditImageViewModel, collectionViewModel: CollectionViewModel, postsViewModel: PostsViewModel, @@ -59,6 +70,9 @@ fun EditImageScreen( val editImageState by editImageViewModel.editImageState.collectAsState() val context = LocalContext.current + var description by remember { mutableStateOf(postDescription) } + var isEditing by remember { mutableStateOf(false) } + Box( modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.TopStart) { @@ -89,15 +103,63 @@ fun EditImageScreen( contentDescription = "Edit Image", modifier = Modifier.fillMaxWidth() - .align(BiasAlignment(0f, -0.5f)) // Center the image + .align(BiasAlignment(0f, -0.5f)) .padding(16.dp) .testTag("display_image")) + // Description box + Column( + modifier = + Modifier.fillMaxWidth() + .padding(16.dp) + .align(BiasAlignment(0f, 0.55f)) // Consistent alignment for both states + ) { + if (isEditing) { + OutlinedTextField( + textStyle = TextStyle(color = Color.White, fontWeight = FontWeight.Bold), + placeholder = { + Text( + "Add the description", + style = TextStyle(color = Color.White, fontStyle = FontStyle.Italic)) + }, + value = description, + onValueChange = { description = it }, + label = { Text("Description", color = Color.White) }, + modifier = + Modifier.fillMaxWidth() + .background(Color.Gray.copy(alpha = 0.5f)) + .padding(8.dp) + .testTag("edit_description_field"), + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), + keyboardActions = + KeyboardActions( + onDone = { + postsViewModel.updateDescription(postUid, description) + isEditing = false + })) + } else { + Text( + text = if (description.isEmpty()) "Add the description" else description, + color = Color.White, + style = + if (description.isEmpty()) + TextStyle(color = Color.White, fontStyle = FontStyle.Italic) + else TextStyle(fontWeight = FontWeight.Bold), + modifier = + Modifier.fillMaxWidth() + .height(55.dp) + .background(Color.Gray.copy(alpha = 0.5f)) + .padding(8.dp) + .clickable { isEditing = true } + .testTag("description_text")) + } + } + // Edit buttons aligned to the bottom center Column( modifier = Modifier.fillMaxWidth() - .align(BiasAlignment(0f, 0.60f)) // Align to the bottom center of the Box + .align(BiasAlignment(0f, 0.85f)) .padding(16.dp) .testTag("edit_buttons_column"), verticalArrangement = Arrangement.spacedBy(12.dp), @@ -109,7 +171,7 @@ fun EditImageScreen( modifier = Modifier.testTag("star_collection").size(28.dp)) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Average Rating: ${postAverageStar}", + text = "Average Rating: $postAverageStar", color = Color.White, modifier = Modifier.testTag("average_rating_collection")) Spacer(modifier = Modifier.width(8.dp)) @@ -120,7 +182,7 @@ fun EditImageScreen( modifier = Modifier.testTag("user_icon_collection")) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Rated by : ${postRatedByNb} users", + text = "Rated by : $postRatedByNb users", color = Color.White, modifier = Modifier.testTag("rated_by_collection")) } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActions.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActions.kt index 860c38378..0c42673ca 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActions.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/navigation/NavigationActions.kt @@ -128,8 +128,10 @@ open class NavigationActions( postAverageStar: Float, postRatedByNb: Int, postUid: String, + postDescription: String, route: String ) { - navController.navigate("${route}/$encodedUri/$postAverageStar/$postRatedByNb/$postUid") + navController.navigate( + "${route}/$encodedUri/$postAverageStar/$postRatedByNb/$postUid/$postDescription") } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt index 301cfd013..e1ec07abb 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt @@ -130,6 +130,22 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { repository.updatePost(post, onSuccess, onFailure) } + /** + * Updates the description of an existing post in the repository. + * + * @param postUid The UID of the modified post. + * @param onSuccess Callback executed on successful update. + * @param onFailure Callback executed on update failure. + */ + fun updateDescription( + postUid: String, + newDescription: String, + onSuccess: () -> Unit = {}, + onFailure: (Exception) -> Unit = {} + ) { + repository.updateDescription(postUid, newDescription, onSuccess, onFailure) + } + companion object { /** * Factory for creating instances of [PostsViewModel]. diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/profile/Collection.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/profile/Collection.kt index 440b9bf7e..a0ea468bf 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/profile/Collection.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/profile/Collection.kt @@ -124,6 +124,7 @@ fun CollectionScreen( route = Route.EDIT_IMAGE, postUid = post.uid, postAverageStar = post.averageStars.toFloat(), + postDescription = post.description, postRatedByNb = post.ratedBy.size) } .background( diff --git a/app/src/test/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestoreTest.kt b/app/src/test/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestoreTest.kt index 46d36bc6a..f4da5099b 100644 --- a/app/src/test/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestoreTest.kt +++ b/app/src/test/java/com/github/lookupgroup27/lookup/model/post/PostsRepositoryFirestoreTest.kt @@ -242,4 +242,51 @@ class PostsRepositoryFirestoreTest { assertThat(errorMessage, `is`("Deletion failed")) verify(mockDocumentReference).delete() } + + @Test + fun `test updateDescription calls onSuccess when firestore update succeeds`() { + val postUid = "1" + val newDescription = "Updated description" + var successCalled = false + + `when`(mockDocumentReference.update("description", newDescription)) + .thenReturn(Tasks.forResult(null)) + + postsRepositoryFirestore.updateDescription( + postUid = postUid, + newDescription = newDescription, + onSuccess = { successCalled = true }, + onFailure = { fail("onFailure should not be called") }) + + shadowOf(Looper.getMainLooper()).idle() + + assert(successCalled) { "onSuccess callback was not called" } + verify(mockDocumentReference).update("description", newDescription) + } + + @Test + fun `test updateDescription calls onFailure when firestore update fails`() { + val postUid = "1" + val newDescription = "Updated description" + val exception = + FirebaseFirestoreException("Update failed", FirebaseFirestoreException.Code.ABORTED) + var failureCalled = false + + `when`(mockDocumentReference.update("description", newDescription)) + .thenReturn(Tasks.forException(exception)) + + postsRepositoryFirestore.updateDescription( + postUid = postUid, + newDescription = newDescription, + onSuccess = { fail("onSuccess should not be called") }, + onFailure = { error -> + failureCalled = true + assert(error.message == "Update failed") { "Error message mismatch" } + }) + + shadowOf(Looper.getMainLooper()).idle() + + assert(failureCalled) { "onFailure callback was not called" } + verify(mockDocumentReference).update("description", newDescription) + } } diff --git a/app/src/test/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModelTest.kt b/app/src/test/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModelTest.kt index f9c955c5f..255383c61 100644 --- a/app/src/test/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModelTest.kt +++ b/app/src/test/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModelTest.kt @@ -188,4 +188,88 @@ class PostsViewModelTest { assertThat(successCalled, `is`(true)) assertThat(failureCalled, `is`(false)) } + + @Test + fun `test updateDescription calls repository with correct parameters`() { + val postUid = "1" + val newDescription = "Updated description" + var successCalled = false + var failureCalled = false + + // Mock repository behavior for success + doAnswer { invocation -> + val onSuccess = invocation.arguments[2] as () -> Unit + onSuccess() // Simulate success callback + null + } + .whenever(postsRepository) + .updateDescription( + org.mockito.kotlin.eq(postUid), + org.mockito.kotlin.eq(newDescription), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + + // Call the method + postsViewModel.updateDescription( + postUid = postUid, + newDescription = newDescription, + onSuccess = { successCalled = true }, + onFailure = { failureCalled = true }) + + // Verify repository method was called with correct parameters + verify(postsRepository) + .updateDescription( + org.mockito.kotlin.eq(postUid), + org.mockito.kotlin.eq(newDescription), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + + // Assert callbacks + assert(successCalled) { "onSuccess callback was not called" } + assert(!failureCalled) { "onFailure callback should not have been called" } + } + + @Test + fun `test updateDescription calls onFailure on repository error`() { + val postUid = "1" + val newDescription = "Updated description" + val exception = Exception("Update failed") + var successCalled = false + var failureCalled = false + + // Mock repository behavior for failure + doAnswer { invocation -> + val onFailure = invocation.arguments[3] as (Exception) -> Unit + onFailure(exception) // Simulate failure callback + null + } + .whenever(postsRepository) + .updateDescription( + org.mockito.kotlin.eq(postUid), + org.mockito.kotlin.eq(newDescription), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + + // Call the method + postsViewModel.updateDescription( + postUid = postUid, + newDescription = newDescription, + onSuccess = { successCalled = true }, + onFailure = { error -> + failureCalled = true + assert(error.message == "Update failed") { "Error message mismatch" } + }) + + // Verify repository method was called with correct parameters + verify(postsRepository) + .updateDescription( + org.mockito.kotlin.eq(postUid), + org.mockito.kotlin.eq(newDescription), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + + // Assert callbacks + assert(!successCalled) { "onSuccess callback should not have been called" } + assert(failureCalled) { "onFailure callback was not called" } + } } From c13c4c3bb9ef6b4f07c13741e0a44ce50469fc7e Mon Sep 17 00:00:00 2001 From: Ismaillat Date: Thu, 12 Dec 2024 17:27:43 +0100 Subject: [PATCH 2/6] fix: add test for the updateDescription method in the editImageViewModel --- .../lookup/ui/post/PostsViewModel.kt | 6 +- .../lookup/ui/post/PostsViewModelTest.kt | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt index e1ec07abb..5ff1db5de 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModel.kt @@ -143,7 +143,11 @@ class PostsViewModel(private val repository: PostsRepository) : ViewModel() { onSuccess: () -> Unit = {}, onFailure: (Exception) -> Unit = {} ) { - repository.updateDescription(postUid, newDescription, onSuccess, onFailure) + try { + repository.updateDescription(postUid, newDescription, onSuccess, onFailure) + } catch (exception: Exception) { + onFailure(exception) + } } companion object { diff --git a/app/src/test/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModelTest.kt b/app/src/test/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModelTest.kt index 255383c61..9048fde73 100644 --- a/app/src/test/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModelTest.kt +++ b/app/src/test/java/com/github/lookupgroup27/lookup/ui/post/PostsViewModelTest.kt @@ -4,6 +4,7 @@ import com.github.lookupgroup27.lookup.model.post.Post import com.github.lookupgroup27.lookup.model.post.PostsRepository import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.FirebaseFirestore +import junit.framework.TestCase.fail import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.hamcrest.CoreMatchers.`is` @@ -272,4 +273,83 @@ class PostsViewModelTest { assert(!successCalled) { "onSuccess callback should not have been called" } assert(failureCalled) { "onFailure callback was not called" } } + + @Test + fun `test updateDescription calls onSuccess after repository method completes`() { + val postUid = "validUid" + val newDescription = "Valid description" + var callbackTriggered = false + + doAnswer { invocation -> + val onSuccess = invocation.arguments[2] as () -> Unit + onSuccess() // Simulate success callback + assert(callbackTriggered) { "onSuccess should be called after repository method" } + null + } + .whenever(postsRepository) + .updateDescription( + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + + postsViewModel.updateDescription( + postUid = postUid, + newDescription = newDescription, + onSuccess = { callbackTriggered = true }, + onFailure = { fail("onFailure should not be called") }) + + verify(postsRepository) + .updateDescription( + org.mockito.kotlin.eq(postUid), + org.mockito.kotlin.eq(newDescription), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + } + + @Test + fun `test updateDescription handles exceptions thrown by repository`() { + val postUid = "validUid" + val newDescription = "Valid description" + + doThrow(RuntimeException("Repository exception")) + .whenever(postsRepository) + .updateDescription( + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + + var failureTriggered = false + + postsViewModel.updateDescription( + postUid = postUid, + newDescription = newDescription, + onSuccess = { fail("onSuccess should not be called") }, + onFailure = { exception -> + failureTriggered = true + assert(exception.message == "Repository exception") { "Unexpected exception message" } + }) + + assert(failureTriggered) { "onFailure should be triggered when repository throws an exception" } + } + + @Test + fun `test updateDescription calls repository with exact arguments`() { + val postUid = "validUid" + val newDescription = "Valid description" + + postsViewModel.updateDescription( + postUid = postUid, + newDescription = newDescription, + onSuccess = {}, + onFailure = { fail("onFailure should not be called") }) + + verify(postsRepository) + .updateDescription( + org.mockito.kotlin.eq(postUid), + org.mockito.kotlin.eq(newDescription), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + } } From 64d8a9b4b50f9fbaf42b51fd48cfb0f1a7144ff6 Mon Sep 17 00:00:00 2001 From: Ismaillat Date: Thu, 12 Dec 2024 19:01:03 +0100 Subject: [PATCH 3/6] fix: add test for the EditImageScreen --- .../lookup/ui/image/EditImageKtTest.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt index 0b7a0d66e..0e0be4a10 100644 --- a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt +++ b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt @@ -1,9 +1,13 @@ package com.github.lookupgroup27.lookup.ui.image import androidx.compose.ui.test.assertIsDisplayed +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.performImeAction +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.lookupgroup27.lookup.model.collection.CollectionRepository import com.github.lookupgroup27.lookup.model.image.EditImageRepository @@ -299,4 +303,51 @@ class EditImageScreenTest { // Verify that the edit field appears composeTestRule.onNodeWithTag("edit_description_field").assertIsDisplayed() } + + @Test + fun testOnDoneKeyboardActionUpdatesDescriptionAndExitsEditingMode() { + val testPostUid = "mock_uid" + val initialDescription = "mock_description" + val updatedDescription = "updated_description" + + composeTestRule.setContent { + EditImageScreen( + postUri = "mock_image_url", + postAverageStar = 4.5, + postRatedByNb = 20, + postUid = testPostUid, + postDescription = initialDescription, + editImageViewModel = editImageViewModel, + collectionViewModel = collectionViewModel, + navigationActions = mockNavigationActions, + postsViewModel = postsViewModel) + } + + // Click on the description text to enter editing mode + composeTestRule.onNodeWithTag("description_text").performClick() + + // Verify the edit field appears + composeTestRule.onNodeWithTag("edit_description_field").assertIsDisplayed() + + // Input a new description (resetting any previous value first) + composeTestRule.onNodeWithTag("edit_description_field").performTextClearance() + composeTestRule.onNodeWithTag("edit_description_field").performTextInput(updatedDescription) + + // Simulate the "Done" action + composeTestRule.onNodeWithTag("edit_description_field").performImeAction() + + // Verify that `updateDescription` is called with correct arguments + verify(postsRepository) + .updateDescription( + org.mockito.kotlin.eq(testPostUid), + org.mockito.kotlin.eq(updatedDescription), + org.mockito.kotlin.any(), + org.mockito.kotlin.any()) + + // Verify that the description text displays the updated description + composeTestRule.onNodeWithTag("description_text").assertTextEquals(updatedDescription) + + // Verify that the editing mode has exited + composeTestRule.onNodeWithTag("edit_description_field").assertDoesNotExist() + } } From 5fdf50a81bc683370270ac210e0a0e7d9367987b Mon Sep 17 00:00:00 2001 From: Ismaillat Date: Sun, 15 Dec 2024 20:28:54 +0100 Subject: [PATCH 4/6] refactor: enhance UI and functionality of EditImage screen: - add user confirmation dialog for description changes - improve UI consistency with ConstraintLayout - update tests of the description modification flow --- app/build.gradle.kts | 1 + .../lookup/ui/image/EditImageKtTest.kt | 75 +++- .../lookup/ui/image/EditImage.kt | 336 ++++++++++-------- gradle/libs.versions.toml | 2 + 4 files changed, 267 insertions(+), 147 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fbf3ff193..22cf3dc81 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -165,6 +165,7 @@ dependencies { implementation(libs.androidx.navigation.runtime.ktx) implementation(libs.androidx.navigation.testing) implementation(libs.test.core.ktx) + implementation (libs.androidx.constraintlayout.compose) //Camera implementation (libs.androidx.camera.camera2) diff --git a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt index 0e0be4a10..b126adcdf 100644 --- a/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt +++ b/app/src/androidTest/java/com/github/lookupgroup27/lookup/ui/image/EditImageKtTest.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextClearance @@ -21,7 +22,10 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.times import org.mockito.kotlin.verify /** @@ -305,7 +309,7 @@ class EditImageScreenTest { } @Test - fun testOnDoneKeyboardActionUpdatesDescriptionAndExitsEditingMode() { + fun testOnDoneKeyboardActionDisplaysConfirmationDialogAndSavesDescription() { val testPostUid = "mock_uid" val initialDescription = "mock_description" val updatedDescription = "updated_description" @@ -336,18 +340,75 @@ class EditImageScreenTest { // Simulate the "Done" action composeTestRule.onNodeWithTag("edit_description_field").performImeAction() + // Verify that the confirmation dialog appears + composeTestRule.onNodeWithText("Save Changes?").assertIsDisplayed() + composeTestRule.onNodeWithText("Do you want to save the new description?").assertIsDisplayed() + + // Click on the "Save" button in the dialog + composeTestRule.onNodeWithText("Save").performClick() + // Verify that `updateDescription` is called with correct arguments - verify(postsRepository) - .updateDescription( - org.mockito.kotlin.eq(testPostUid), - org.mockito.kotlin.eq(updatedDescription), - org.mockito.kotlin.any(), - org.mockito.kotlin.any()) + verify(postsRepository).updateDescription(eq(testPostUid), eq(updatedDescription), any(), any()) // Verify that the description text displays the updated description composeTestRule.onNodeWithTag("description_text").assertTextEquals(updatedDescription) // Verify that the editing mode has exited composeTestRule.onNodeWithTag("edit_description_field").assertDoesNotExist() + + // Ensure the confirmation dialog is dismissed + composeTestRule.onNodeWithText("Save Changes?").assertDoesNotExist() + } + + @Test + fun testOnDoneKeyboardActionDisplaysConfirmationDialogAndDiscardsDescription() { + val testPostUid = "mock_uid" + val initialDescription = "mock_description" + val updatedDescription = "updated_description" + + composeTestRule.setContent { + EditImageScreen( + postUri = "mock_image_url", + postAverageStar = 4.5, + postRatedByNb = 20, + postUid = testPostUid, + postDescription = initialDescription, + editImageViewModel = editImageViewModel, + collectionViewModel = collectionViewModel, + navigationActions = mockNavigationActions, + postsViewModel = postsViewModel) + } + + // Click on the description text to enter editing mode + composeTestRule.onNodeWithTag("description_text").performClick() + + // Verify the edit field appears + composeTestRule.onNodeWithTag("edit_description_field").assertIsDisplayed() + + // Input a new description (resetting any previous value first) + composeTestRule.onNodeWithTag("edit_description_field").performTextClearance() + composeTestRule.onNodeWithTag("edit_description_field").performTextInput(updatedDescription) + + // Simulate the "Done" action + composeTestRule.onNodeWithTag("edit_description_field").performImeAction() + + // Verify that the confirmation dialog appears + composeTestRule.onNodeWithText("Save Changes?").assertIsDisplayed() + composeTestRule.onNodeWithText("Do you want to save the new description?").assertIsDisplayed() + + // Click on the "Discard" button in the dialog + composeTestRule.onNodeWithText("Discard").performClick() + + // Verify that `updateDescription` is NOT called + verify(postsRepository, times(0)).updateDescription(any(), any(), any(), any()) + + // Verify that the description text displays the initial description + composeTestRule.onNodeWithTag("description_text").assertTextEquals(initialDescription) + + // Verify that the editing mode has exited + composeTestRule.onNodeWithTag("edit_description_field").assertDoesNotExist() + + // Ensure the confirmation dialog is dismissed + composeTestRule.onNodeWithText("Save Changes?").assertDoesNotExist() } } diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt index e98bfe18e..2c73161ff 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt @@ -5,8 +5,11 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Person @@ -19,9 +22,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -32,6 +35,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout import coil.compose.rememberAsyncImagePainter import com.github.lookupgroup27.lookup.R import com.github.lookupgroup27.lookup.ui.image.components.ActionButton @@ -72,157 +76,209 @@ fun EditImageScreen( var description by remember { mutableStateOf(postDescription) } var isEditing by remember { mutableStateOf(false) } + var showSaveDialog by remember { mutableStateOf(false) } + var originalDescription by remember { mutableStateOf(description) } - Box( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), - contentAlignment = Alignment.TopStart) { - // Background image - Image( - painter = painterResource(id = R.drawable.landing_screen_bckgrnd), - contentDescription = "Background", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize().blur(10.dp).testTag("background_image")) + // Enable vertical scrolling + val scrollState = rememberScrollState() - // Back button aligned to top start - IconButton( - onClick = { navigationActions.navigateTo(Screen.COLLECTION) }, - modifier = - Modifier.align(Alignment.TopStart) - .padding(16.dp) - .testTag("go_back_button_collection")) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - tint = Color.White, - modifier = Modifier.testTag("back_icon")) - } + Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) { + // Background image + Image( + painter = painterResource(id = R.drawable.landing_screen_bckgrnd), + contentDescription = "Background", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize().blur(10.dp).testTag("background_image")) - // Display the image - Image( - painter = rememberAsyncImagePainter(postUri), - contentDescription = "Edit Image", - modifier = - Modifier.fillMaxWidth() - .align(BiasAlignment(0f, -0.5f)) - .padding(16.dp) - .testTag("display_image")) + // Layout using ConstraintLayout for consistency + ConstraintLayout(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { + val (backButton, image, descriptionBox, buttonsColumn, starRow) = createRefs() - // Description box - Column( - modifier = - Modifier.fillMaxWidth() - .padding(16.dp) - .align(BiasAlignment(0f, 0.55f)) // Consistent alignment for both states - ) { - if (isEditing) { - OutlinedTextField( - textStyle = TextStyle(color = Color.White, fontWeight = FontWeight.Bold), - placeholder = { - Text( - "Add the description", - style = TextStyle(color = Color.White, fontStyle = FontStyle.Italic)) - }, - value = description, - onValueChange = { description = it }, - label = { Text("Description", color = Color.White) }, - modifier = - Modifier.fillMaxWidth() - .background(Color.Gray.copy(alpha = 0.5f)) - .padding(8.dp) - .testTag("edit_description_field"), - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = - KeyboardActions( - onDone = { - postsViewModel.updateDescription(postUid, description) - isEditing = false - })) - } else { - Text( - text = if (description.isEmpty()) "Add the description" else description, - color = Color.White, - style = - if (description.isEmpty()) - TextStyle(color = Color.White, fontStyle = FontStyle.Italic) - else TextStyle(fontWeight = FontWeight.Bold), - modifier = - Modifier.fillMaxWidth() - .height(55.dp) - .background(Color.Gray.copy(alpha = 0.5f)) - .padding(8.dp) - .clickable { isEditing = true } - .testTag("description_text")) - } - } + // Back button + IconButton( + onClick = { navigationActions.navigateTo(Screen.COLLECTION) }, + modifier = + Modifier.constrainAs(backButton) { + top.linkTo(parent.top, margin = 16.dp) + start.linkTo(parent.start, margin = 16.dp) + } + .testTag("go_back_button_collection")) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = Color.White, + modifier = Modifier.testTag("back_icon")) + } - // Edit buttons aligned to the bottom center - Column( - modifier = - Modifier.fillMaxWidth() - .align(BiasAlignment(0f, 0.85f)) - .padding(16.dp) - .testTag("edit_buttons_column"), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalAlignment = Alignment.CenterHorizontally) { - Row { - Image( - painter = painterResource(id = R.drawable.full_star), - contentDescription = "Star Rating", - modifier = Modifier.testTag("star_collection").size(28.dp)) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Average Rating: $postAverageStar", - color = Color.White, - modifier = Modifier.testTag("average_rating_collection")) - Spacer(modifier = Modifier.width(8.dp)) - Icon( - imageVector = Icons.Default.Person, - contentDescription = "User Icon", - tint = Color.White, - modifier = Modifier.testTag("user_icon_collection")) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Rated by : $postRatedByNb users", - color = Color.White, - modifier = Modifier.testTag("rated_by_collection")) - } + Image( + painter = rememberAsyncImagePainter(postUri), + contentDescription = "Edit Image", + contentScale = ContentScale.Crop, + modifier = + Modifier.fillMaxWidth(0.85f) // Smaller width in landscape + .aspectRatio(3f / 4f) // Adjust height proportionally + .padding(16.dp) + .constrainAs(image) { + top.linkTo(backButton.bottom, margin = 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .testTag("display_image")) - ActionButton( - text = "Delete Image", - onClick = { - editImageViewModel.deleteImage(postUri) - navigationActions.navigateTo(Screen.COLLECTION) + // Description box + Column( + modifier = + Modifier.fillMaxWidth().padding(16.dp).constrainAs(descriptionBox) { + top.linkTo(image.bottom, margin = 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }) { + // Title for the Description Box + Text( + text = "Description", + style = + TextStyle( + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = MaterialTheme.typography.bodySmall.fontSize)) + if (isEditing) { + OutlinedTextField( + textStyle = TextStyle(color = Color.White), + placeholder = { + Text( + "Add the description", + style = TextStyle(color = Color.White, fontStyle = FontStyle.Italic)) }, - color = Color.Red, - modifier = Modifier.testTag("delete_button")) - } + value = description, + onValueChange = { description = it }, + label = { Text("Description", color = Color.White) }, + modifier = + Modifier.fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(Color.Gray.copy(alpha = 0.5f)) + .padding(8.dp) + .testTag("edit_description_field"), + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { showSaveDialog = true })) - // Loading indicator and state handling - when (editImageState) { - is EditImageState.Loading -> { - CircularProgressIndicator( - modifier = - Modifier.align(Alignment.Center) - .padding(top = 16.dp) - .testTag("loading_indicator")) - } - is EditImageState.Error -> { - val errorMessage = (editImageState as EditImageState.Error).message - LaunchedEffect(editImageState) { - Toast.makeText(context, "Error: $errorMessage", Toast.LENGTH_SHORT).show() + if (showSaveDialog) { + AlertDialog( + onDismissRequest = { showSaveDialog = false }, + title = { Text("Save Changes?") }, + text = { Text("Do you want to save the new description?") }, + confirmButton = { + TextButton( + onClick = { + originalDescription = description + postsViewModel.updateDescription(postUid, description) + isEditing = false + showSaveDialog = false + }) { + Text("Save") + } + }, + dismissButton = { + TextButton( + onClick = { + description = originalDescription + isEditing = false + showSaveDialog = false + }) { + Text("Discard") + } + }) + } + } else { + Text( + text = if (description.isEmpty()) "Add the description" else description, + color = Color.White, + style = + if (description.isEmpty()) + TextStyle(color = Color.White, fontStyle = FontStyle.Italic) + else TextStyle(color = Color.White), + modifier = + Modifier.fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .height(55.dp) + .background(Color.Gray.copy(alpha = 0.5f)) + .padding(8.dp) + .clickable { isEditing = true } + .testTag("description_text")) } } - is EditImageState.Deleted -> { - collectionViewModel.updateImages() - postsViewModel.deletePost(postUid) - LaunchedEffect(editImageState) { - Toast.makeText(context, "Image deleted successfully.", Toast.LENGTH_SHORT).show() - editImageViewModel.resetState() - } + + // Star rating and user info + Row( + modifier = + Modifier.fillMaxWidth().padding(horizontal = 16.dp).constrainAs(starRow) { + top.linkTo(descriptionBox.bottom, margin = 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.full_star), + contentDescription = "Star Rating", + modifier = Modifier.testTag("star_collection").size(28.dp)) + Text( + text = "Average Rating: $postAverageStar", + color = Color.White, + modifier = Modifier.testTag("average_rating_collection")) + Icon( + imageVector = Icons.Default.Person, + contentDescription = "User Icon", + tint = Color.White, + modifier = Modifier.testTag("user_icon_collection")) + Text( + text = "Rated by: $postRatedByNb users", + color = Color.White, + modifier = Modifier.testTag("rated_by_collection")) } - else -> { - // Idle state or no-op + + // Edit buttons + Column( + modifier = + Modifier.fillMaxWidth().padding(16.dp).constrainAs(buttonsColumn) { + bottom.linkTo(parent.bottom, margin = 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally) { + ActionButton( + text = "Delete Image", + onClick = { + editImageViewModel.deleteImage(postUri) + navigationActions.navigateTo(Screen.COLLECTION) + }, + color = Color.Red, + modifier = Modifier.testTag("delete_button")) } + } + + // Loading and error states + when (editImageState) { + is EditImageState.Loading -> { + CircularProgressIndicator( + modifier = + Modifier.align(Alignment.Center).padding(top = 16.dp).testTag("loading_indicator")) + } + is EditImageState.Error -> { + val errorMessage = (editImageState as EditImageState.Error).message + LaunchedEffect(editImageState) { + Toast.makeText(context, "Error: $errorMessage", Toast.LENGTH_SHORT).show() + } + } + is EditImageState.Deleted -> { + collectionViewModel.updateImages() + postsViewModel.deletePost(postUid) + LaunchedEffect(editImageState) { + Toast.makeText(context, "Image deleted successfully.", Toast.LENGTH_SHORT).show() + editImageViewModel.resetState() } } + else -> Unit + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 95e645f1b..e2d80ec30 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ cameraCamera2 = "1.1.0" cameraExtensions = "1.0.0-alpha31" coilCompose = "2.2.2" +constraintlayoutCompose = "1.1.0" firebaseStorage = "20.2.0" coilComposeVersion = "2.1.0" coreTesting = "2.1.0" @@ -93,6 +94,7 @@ androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.r androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraExtensions" } androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraCamera2" } androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraExtensions" } +androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" } androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" } From a6aa932446fa3538858c71ac38d1215b22431d76 Mon Sep 17 00:00:00 2001 From: Ismaillat Date: Mon, 16 Dec 2024 16:46:05 +0100 Subject: [PATCH 5/6] refactor: remove label from the OutlinedTextField --- .../java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt index 2c73161ff..13c08b447 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt @@ -151,7 +151,7 @@ fun EditImageScreen( }, value = description, onValueChange = { description = it }, - label = { Text("Description", color = Color.White) }, + label = {}, modifier = Modifier.fillMaxWidth() .clip(RoundedCornerShape(12.dp)) From 248b9879c41da30ddb387ff27dfb83f877a6f4ed Mon Sep 17 00:00:00 2001 From: Ismaillat Date: Mon, 16 Dec 2024 17:09:24 +0100 Subject: [PATCH 6/6] refactor: change description box color to DarkPurple --- .../com/github/lookupgroup27/lookup/ui/image/EditImage.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt index 13c08b447..daf4d1b5b 100644 --- a/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt +++ b/app/src/main/java/com/github/lookupgroup27/lookup/ui/image/EditImage.kt @@ -43,6 +43,7 @@ import com.github.lookupgroup27.lookup.ui.navigation.NavigationActions import com.github.lookupgroup27.lookup.ui.navigation.Screen import com.github.lookupgroup27.lookup.ui.post.PostsViewModel import com.github.lookupgroup27.lookup.ui.profile.CollectionViewModel +import com.github.lookupgroup27.lookup.ui.theme.DarkPurple /** * Composable function to display the Edit Image Screen. @@ -136,6 +137,7 @@ fun EditImageScreen( // Title for the Description Box Text( text = "Description", + modifier = Modifier.padding(horizontal = 8.dp), style = TextStyle( color = Color.White, @@ -155,7 +157,7 @@ fun EditImageScreen( modifier = Modifier.fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .background(Color.Gray.copy(alpha = 0.5f)) + .background(DarkPurple.copy(alpha = 0.5f)) .padding(8.dp) .testTag("edit_description_field"), keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), @@ -200,7 +202,7 @@ fun EditImageScreen( Modifier.fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .height(55.dp) - .background(Color.Gray.copy(alpha = 0.5f)) + .background(DarkPurple.copy(alpha = 0.5f)) .padding(8.dp) .clickable { isEditing = true } .testTag("description_text"))