diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/AddActivityScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/AddActivityScreenTest.kt index 3414a67a..f8a8a639 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/AddActivityScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/AddActivityScreenTest.kt @@ -13,11 +13,14 @@ import androidx.compose.ui.test.performTextInput import com.github.se.travelpouch.model.activity.Activity import com.github.se.travelpouch.model.activity.ActivityRepository import com.github.se.travelpouch.model.activity.ActivityViewModel +import com.github.se.travelpouch.model.events.EventRepository +import com.github.se.travelpouch.model.events.EventViewModel import com.github.se.travelpouch.model.location.LocationRepository import com.github.se.travelpouch.model.location.LocationViewModel import com.github.se.travelpouch.model.travels.Location import com.github.se.travelpouch.ui.navigation.NavigationActions import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentReference import org.junit.Before import org.junit.Rule import org.junit.Test @@ -26,12 +29,15 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.never +import org.mockito.kotlin.whenever class AddActivityScreenTest { private lateinit var mockActivityRepositoryFirebase: ActivityRepository private lateinit var mockActivityModelView: ActivityViewModel private lateinit var navigationActions: NavigationActions private lateinit var mockLocationViewModel: LocationViewModel + private lateinit var eventRepository: EventRepository + private lateinit var eventViewModel: EventViewModel class FakeLocationRepository : LocationRepository { @@ -67,13 +73,17 @@ class AddActivityScreenTest { mockActivityRepositoryFirebase = mock(ActivityRepository::class.java) mockActivityModelView = ActivityViewModel(mockActivityRepositoryFirebase) mockLocationViewModel = LocationViewModel(FakeLocationRepository()) + eventRepository = mock() + eventViewModel = EventViewModel(eventRepository) `when`(mockActivityModelView.getNewUid()).thenReturn("uid") } @Test fun everythingIsDisplayed() { - composeTestRule.setContent { AddActivityScreen(navigationActions, mockActivityModelView) } + composeTestRule.setContent { + AddActivityScreen(navigationActions, mockActivityModelView, eventViewModel = eventViewModel) + } composeTestRule.onNodeWithTag("AddActivityScreen").isDisplayed() composeTestRule.onNodeWithTag("titleField").isDisplayed() @@ -101,7 +111,9 @@ class AddActivityScreenTest { @Test fun doesNotSaveWhenFieldsAreEmpty() { - composeTestRule.setContent { AddActivityScreen(navigationActions, mockActivityModelView) } + composeTestRule.setContent { + AddActivityScreen(navigationActions, mockActivityModelView, eventViewModel = eventViewModel) + } completeAllFields(composeTestRule) // todo: add a test for location. Not now because location defined later. @@ -110,34 +122,43 @@ class AddActivityScreenTest { composeTestRule.onNodeWithTag("titleField").performTextClearance() composeTestRule.onNodeWithTag("saveButton").performClick() verify(mockActivityRepositoryFirebase, never()) - .addActivity(anyOrNull(), anyOrNull(), anyOrNull()) + .addActivity(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) // verify no saving when blank description completeAllFields(composeTestRule) composeTestRule.onNodeWithTag("descriptionField").performTextClearance() composeTestRule.onNodeWithTag("saveButton").performClick() verify(mockActivityRepositoryFirebase, never()) - .addActivity(anyOrNull(), anyOrNull(), anyOrNull()) + .addActivity(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) // verify no saving when blank date completeAllFields(composeTestRule) composeTestRule.onNodeWithTag("dateField").performTextClearance() composeTestRule.onNodeWithTag("saveButton").performClick() verify(mockActivityRepositoryFirebase, never()) - .addActivity(anyOrNull(), anyOrNull(), anyOrNull()) + .addActivity(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test fun doesSaveWhenFieldsAreFull() { - composeTestRule.setContent { AddActivityScreen(navigationActions, mockActivityModelView) } + composeTestRule.setContent { + AddActivityScreen(navigationActions, mockActivityModelView, eventViewModel = eventViewModel) + } completeAllFields(composeTestRule) + + whenever(eventViewModel.getNewDocumentReference()) + .thenReturn(mock(DocumentReference::class.java)) + composeTestRule.onNodeWithTag("saveButton").performClick() - verify(mockActivityRepositoryFirebase).addActivity(anyOrNull(), anyOrNull(), anyOrNull()) + verify(mockActivityRepositoryFirebase) + .addActivity(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test fun dateFormattingWorksCorrectly() { - composeTestRule.setContent { AddActivityScreen(navigationActions, mockActivityModelView) } + composeTestRule.setContent { + AddActivityScreen(navigationActions, mockActivityModelView, eventViewModel = eventViewModel) + } composeTestRule.onNodeWithTag("dateField").performTextClearance() composeTestRule.onNodeWithTag("dateField").performTextInput("00000000") val result = @@ -147,19 +168,23 @@ class AddActivityScreenTest { @Test fun doesNotSaveWhenDateIsWrong() { - composeTestRule.setContent { AddActivityScreen(navigationActions, mockActivityModelView) } + composeTestRule.setContent { + AddActivityScreen(navigationActions, mockActivityModelView, eventViewModel = eventViewModel) + } completeAllFields(composeTestRule) composeTestRule.onNodeWithTag("dateField").performTextClearance() composeTestRule.onNodeWithTag("dateField").performTextInput("00000000") composeTestRule.onNodeWithTag("saveButton").performClick() verify(mockActivityRepositoryFirebase, never()) - .addActivity(anyOrNull(), anyOrNull(), anyOrNull()) + .addActivity(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test fun noCharacterAllowedInDateField() { - composeTestRule.setContent { AddActivityScreen(navigationActions, mockActivityModelView) } + composeTestRule.setContent { + AddActivityScreen(navigationActions, mockActivityModelView, eventViewModel = eventViewModel) + } composeTestRule.onNodeWithTag("dateField").performTextClearance() composeTestRule.onNodeWithTag("dateField").performTextInput("mdkdk") var result = @@ -178,7 +203,11 @@ class AddActivityScreenTest { @Test fun limitOfEightCharactersInDateField() { composeTestRule.setContent { - AddActivityScreen(navigationActions, mockActivityModelView, mockLocationViewModel) + AddActivityScreen( + navigationActions, + mockActivityModelView, + mockLocationViewModel, + eventViewModel = eventViewModel) } composeTestRule.onNodeWithTag("dateField").performTextClearance() composeTestRule.onNodeWithTag("dateField").performTextInput("01234567") @@ -192,7 +221,11 @@ class AddActivityScreenTest { fun locationDropdownAppearsAndSelectionWorks() { val testQuery = "Paris" composeTestRule.setContent { - AddActivityScreen(navigationActions, mockActivityModelView, mockLocationViewModel) + AddActivityScreen( + navigationActions, + mockActivityModelView, + mockLocationViewModel, + eventViewModel = eventViewModel) } // Type in the location field @@ -219,7 +252,11 @@ class AddActivityScreenTest { // TimePickerDialog. composeTestRule.setContent { - AddActivityScreen(navigationActions, mockActivityModelView, mockLocationViewModel) + AddActivityScreen( + navigationActions, + mockActivityModelView, + mockLocationViewModel, + eventViewModel = eventViewModel) } // The time field should be displayed @@ -232,7 +269,11 @@ class AddActivityScreenTest { fun datePickerDialogOpensOnClick() { composeTestRule.setContent { - AddActivityScreen(navigationActions, mockActivityModelView, mockLocationViewModel) + AddActivityScreen( + navigationActions, + mockActivityModelView, + mockLocationViewModel, + eventViewModel = eventViewModel) } // The date field should be displayed diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/EditActivityScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/EditActivityScreenTest.kt index 0ad3347a..835ea4ff 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/EditActivityScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/EditActivityScreenTest.kt @@ -147,7 +147,7 @@ class EditActivityScreenTest { composeTestRule.onNodeWithTag("saveButton").performClick() verify(mockActivityRepositoryFirebase, never()) - .addActivity(anyOrNull(), anyOrNull(), anyOrNull()) + .updateActivity(anyOrNull(), anyOrNull(), anyOrNull()) } @Test diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/TimelineScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/TimelineScreenTest.kt index 7d8df772..c03a6c1a 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/TimelineScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/dashboard/TimelineScreenTest.kt @@ -10,6 +10,7 @@ import com.github.se.travelpouch.model.events.Event import com.github.se.travelpouch.model.events.EventRepository import com.github.se.travelpouch.model.events.EventType import com.github.se.travelpouch.model.events.EventViewModel +import com.github.se.travelpouch.ui.navigation.NavigationActions import com.google.firebase.Timestamp import org.junit.Before import org.junit.Rule @@ -22,6 +23,7 @@ class TimelineScreenTest { private lateinit var mockEventRepository: EventRepository private lateinit var mockEventViewModel: EventViewModel + private lateinit var navigationActions: NavigationActions @get:Rule val composeTestRule = createComposeRule() @@ -31,35 +33,31 @@ class TimelineScreenTest { */ val events_test = listOf( - Event( - "1", - EventType.NEW_DOCUMENT, - Timestamp(0, 0), - "eventTitle", - "eventDescription", - null, - null), - Event("2", EventType.START_OF_JOURNEY, Timestamp(0, 0), "it", "it", null, null), - Event("3", EventType.NEW_PARTICIPANT, Timestamp(0, 0), "it", "it", null, null), - Event("3", EventType.OTHER_EVENT, Timestamp(0, 0), "it", "it", null, null)) + Event("1", EventType.NEW_ACTIVITY, Timestamp(0, 0), "eventTitle", "eventDescription"), + Event("2", EventType.START_OF_JOURNEY, Timestamp(0, 0), "it", "it"), + Event("3", EventType.NEW_PARTICIPANT, Timestamp(0, 0), "it", "it"), + Event("3", EventType.PARTICIPANT_REMOVED, Timestamp(0, 0), "it", "it")) @Before fun setUp() { mockEventRepository = mock(EventRepository::class.java) mockEventViewModel = EventViewModel(mockEventRepository) - + navigationActions = mock() // `when`(mockEventViewModel.events.value).thenReturn(events_test) } @Test fun everythingDisplayed() { - composeTestRule.setContent { TimelineScreen(mockEventViewModel) } + composeTestRule.setContent { TimelineScreen(mockEventViewModel, navigationActions) } `when`(mockEventRepository.getEvents(any(), any())).then { it.getArgument<(List) -> Unit>(0)(events_test) } + composeTestRule.onNodeWithTag("screenTitle").assertTextEquals("Your travel Milestone") + composeTestRule.onNodeWithTag("goBackButton").assertIsDisplayed() + mockEventViewModel.getEvents() composeTestRule.onAllNodes(hasTestTag("boxContainingEvent")).apply { @@ -76,12 +74,12 @@ class TimelineScreenTest { composeTestRule.setContent { TimelineItem(events_test[0], Modifier) } composeTestRule.onNodeWithTag("eventType").assertIsDisplayed() - composeTestRule.onNodeWithTag("eventType").assertTextEquals("NEW_DOCUMENT") + composeTestRule.onNodeWithTag("eventType").assertTextEquals("NEW_ACTIVITY") composeTestRule.onNodeWithTag("eventTitle").assertIsDisplayed() - composeTestRule.onNodeWithTag("eventTitle").assertTextEquals("eventTitle") + composeTestRule.onNodeWithTag("eventTitle").assertTextEquals("eventDescription") composeTestRule.onNodeWithTag("eventDate").assertIsDisplayed() - composeTestRule.onNodeWithTag("eventDate").assertTextEquals("1/1/1970") + composeTestRule.onNodeWithTag("eventDate").assertTextEquals("01/01/1970 at 12:00:00 AM") } } diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/home/AddTravelScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/home/AddTravelScreenTest.kt index 05943c6b..d0792b5b 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/home/AddTravelScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/home/AddTravelScreenTest.kt @@ -9,6 +9,8 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput +import com.github.se.travelpouch.model.events.EventRepository +import com.github.se.travelpouch.model.events.EventViewModel import com.github.se.travelpouch.model.location.LocationRepository import com.github.se.travelpouch.model.location.LocationViewModel import com.github.se.travelpouch.model.profile.Profile @@ -20,6 +22,7 @@ import com.github.se.travelpouch.model.travels.TravelRepository import com.github.se.travelpouch.ui.navigation.NavigationActions import com.github.se.travelpouch.ui.navigation.Screen import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentReference import org.junit.Before import org.junit.Rule import org.junit.Test @@ -28,7 +31,9 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doThrow +import org.mockito.kotlin.whenever class AddTravelScreenTest { @@ -57,6 +62,10 @@ class AddTravelScreenTest { private lateinit var profileRepository: ProfileRepository private lateinit var profileModelView: ProfileModelView + private lateinit var eventRepository: EventRepository + private lateinit var eventViewModel: EventViewModel + private lateinit var eventDocumentReference: DocumentReference + @get:Rule val composeTestRule = createComposeRule() @Before @@ -77,6 +86,10 @@ class AddTravelScreenTest { profileRepository = mock(ProfileRepository::class.java) profileModelView = ProfileModelView((profileRepository)) + eventRepository = mock() + eventViewModel = EventViewModel(eventRepository) + eventDocumentReference = mock() + listTravelViewModel = ListTravelViewModel(travelRepository) // Use a real LocationViewModel with a fake repository locationViewModel = LocationViewModel(FakeLocationRepository()) @@ -84,12 +97,18 @@ class AddTravelScreenTest { // Mock the current route to be the add travel screen `when`(navigationActions.currentRoute()).thenReturn(Screen.AUTH) `when`(listTravelViewModel.getNewUid()).thenReturn("validMockUid12345678") + whenever(eventViewModel.getNewDocumentReferenceForNewTravel("validMockUid12345678")) + .thenReturn(eventDocumentReference) } @Test fun displayAllComponents() { composeTestRule.setContent { - AddTravelScreen(listTravelViewModel, navigationActions, profileModelView = profileModelView) + AddTravelScreen( + listTravelViewModel, + navigationActions, + profileModelView = profileModelView, + eventViewModel = eventViewModel) } composeTestRule.onNodeWithTag("addTravelScreen").assertIsDisplayed() @@ -116,7 +135,8 @@ class AddTravelScreenTest { listTravelViewModel, navigationActions, locationViewModel, - profileModelView = profileModelView) + profileModelView = profileModelView, + eventViewModel = eventViewModel) } // Input valid title and description @@ -136,7 +156,7 @@ class AddTravelScreenTest { composeTestRule.onNodeWithTag("travelSaveButton").performScrollTo().performClick() // Verify that the repository method is not called to add a travel - verify(travelRepository, never()).addTravel(any(), any(), any()) + verify(travelRepository, never()).addTravel(any(), any(), any(), anyOrNull()) } @Test @@ -146,7 +166,8 @@ class AddTravelScreenTest { listTravelViewModel, navigationActions, locationViewModel, - profileModelView = profileModelView) + profileModelView = profileModelView, + eventViewModel = eventViewModel) } // Input valid title and description @@ -162,7 +183,7 @@ class AddTravelScreenTest { composeTestRule.onNodeWithTag("travelSaveButton").performScrollTo().performClick() // Verify that the repository method is not called to add a travel - verify(travelRepository, never()).addTravel(any(), any(), any()) + verify(travelRepository, never()).addTravel(any(), any(), any(), anyOrNull()) } @Test @@ -172,7 +193,8 @@ class AddTravelScreenTest { listTravelViewModel, navigationActions, locationViewModel, - profileModelView = profileModelView) + profileModelView = profileModelView, + eventViewModel = eventViewModel) } // Input valid title and description @@ -192,7 +214,7 @@ class AddTravelScreenTest { composeTestRule.onNodeWithTag("travelSaveButton").performScrollTo().performClick() // Verify that the repository method is not called to add a travel - verify(travelRepository, never()).addTravel(any(), any(), any()) + verify(travelRepository, never()).addTravel(any(), any(), any(), anyOrNull()) } @Test @@ -202,7 +224,8 @@ class AddTravelScreenTest { listTravelViewModel, navigationActions, locationViewModel, - profileModelView = profileModelView) + profileModelView = profileModelView, + eventViewModel = eventViewModel) } // Input valid title and description @@ -222,7 +245,7 @@ class AddTravelScreenTest { composeTestRule.onNodeWithTag("travelSaveButton").performScrollTo().performClick() // Verify that the repository method is not called to add a travel - verify(travelRepository, never()).addTravel(any(), any(), any()) + verify(travelRepository, never()).addTravel(any(), any(), any(), anyOrNull()) } @Test @@ -234,7 +257,8 @@ class AddTravelScreenTest { listTravelViewModel, navigationActions, locationViewModel, - profileModelView = profileModelView) + profileModelView = profileModelView, + eventViewModel = eventViewModel) } composeTestRule.waitForIdle() // Ensures inputs are registered @@ -256,13 +280,17 @@ class AddTravelScreenTest { composeTestRule.onNodeWithTag("travelSaveButton").performScrollTo().performClick() // Verify that the repository method is called to add a travel - verify(travelRepository).addTravel(any(), any(), any()) + verify(travelRepository).addTravel(any(), any(), any(), anyOrNull()) } @Test fun backButtonNavigatesCorrectly() { composeTestRule.setContent { - AddTravelScreen(listTravelViewModel, navigationActions, profileModelView = profileModelView) + AddTravelScreen( + listTravelViewModel, + navigationActions, + profileModelView = profileModelView, + eventViewModel = eventViewModel) } // Click the go back button @@ -275,7 +303,11 @@ class AddTravelScreenTest { @Test fun saveButtonNotEnabled() { composeTestRule.setContent { - AddTravelScreen(listTravelViewModel, navigationActions, profileModelView = profileModelView) + AddTravelScreen( + listTravelViewModel, + navigationActions, + profileModelView = profileModelView, + eventViewModel = eventViewModel) } // Initially, the save button should be disabled @@ -287,7 +319,7 @@ class AddTravelScreenTest { // Mock the repository's addTravel() method to throw an exception doThrow(RuntimeException("Mocked exception during travel saving")) .`when`(travelRepository) - .addTravel(any(), any(), any()) + .addTravel(any(), any(), any(), anyOrNull()) // Set up the content for the test composeTestRule.setContent { @@ -295,7 +327,8 @@ class AddTravelScreenTest { listTravelViewModel, navigationActions, locationViewModel, - profileModelView = profileModelView) + profileModelView = profileModelView, + eventViewModel = eventViewModel) } // Input valid travel details @@ -320,7 +353,8 @@ class AddTravelScreenTest { listTravelViewModel, navigationActions, locationViewModel, - profileModelView = profileModelView) + profileModelView = profileModelView, + eventViewModel = eventViewModel) } // The startDate picker button should be displayed diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/notification/NotificationItemTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/notification/NotificationItemTest.kt index aceb866f..77c86d6a 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/notification/NotificationItemTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/notification/NotificationItemTest.kt @@ -121,7 +121,12 @@ class NotificationItemTest { composeTestRule.setContent { InvitationButtons( - notification1, listTravelViewModel, profileModelView, notificationViewModel, context) + notification1, + listTravelViewModel, + profileModelView, + notificationViewModel, + context, + eventViewModel) } composeTestRule.onNodeWithTag("notification_item_buttons").assertIsDisplayed() @@ -135,7 +140,12 @@ class NotificationItemTest { composeTestRule.setContent { AcceptButton( - notification1, listTravelViewModel, profileModelView, notificationViewModel, context) + notification1, + listTravelViewModel, + profileModelView, + notificationViewModel, + context, + eventViewModel) } composeTestRule.onNodeWithTag("notification_item_accept_button").assertIsDisplayed() @@ -150,7 +160,12 @@ class NotificationItemTest { composeTestRule.setContent { DeclineButton( - notification1, listTravelViewModel, profileModelView, notificationViewModel, context) + notification1, + listTravelViewModel, + profileModelView, + notificationViewModel, + context, + eventsViewModel = eventViewModel) } composeTestRule.onNodeWithTag("notification_item_decline_button").assertIsDisplayed() diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/travel/EditTravelScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/travel/EditTravelScreenTest.kt index 491a5a1e..8c5c540c 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/travel/EditTravelScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/travel/EditTravelScreenTest.kt @@ -176,7 +176,7 @@ class EditTravelSettingsScreenTest { composeTestRule.onNodeWithTag("travelDeleteButton").performScrollTo().assertIsDisplayed() composeTestRule.onNodeWithTag("travelDeleteButton").assertTextContains("Delete") composeTestRule.onNodeWithTag("travelSaveButton").performClick() - verify(travelRepository).updateTravel(any(), any(), anyOrNull(), any(), any()) + verify(travelRepository).updateTravel(any(), any(), anyOrNull(), any(), any(), anyOrNull()) } fun pressALotOfButtons() { @@ -227,7 +227,8 @@ class EditTravelSettingsScreenTest { composeTestRule.onNodeWithTag("travelDeleteButton").performScrollTo().assertIsDisplayed() composeTestRule.onNodeWithTag("travelDeleteButton").assertTextContains("Delete") composeTestRule.onNodeWithTag("travelSaveButton").performClick() - verify(travelRepository, atLeastOnce()).updateTravel(any(), any(), anyOrNull(), any(), any()) + verify(travelRepository, atLeastOnce()) + .updateTravel(any(), any(), anyOrNull(), any(), any(), anyOrNull()) } @Test diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/travel/ParticipantListScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/travel/ParticipantListScreenTest.kt index fc90b2fc..908ed73a 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/travel/ParticipantListScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/travel/ParticipantListScreenTest.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput +import com.github.se.travelpouch.model.events.EventRepository +import com.github.se.travelpouch.model.events.EventViewModel import com.github.se.travelpouch.model.notifications.NotificationRepository import com.github.se.travelpouch.model.notifications.NotificationViewModel import com.github.se.travelpouch.model.profile.Profile @@ -99,6 +101,8 @@ class ParticipantListScreenTest { private lateinit var notificationRepository: NotificationRepository private lateinit var profileModelView: ProfileModelView private lateinit var profileRepository: ProfileRepository + private lateinit var eventRepository: EventRepository + private lateinit var eventViewModel: EventViewModel @get:Rule val composeTestRule = createComposeRule() @@ -111,13 +115,19 @@ class ParticipantListScreenTest { notificationViewModel = NotificationViewModel(notificationRepository) profileRepository = mock(ProfileRepository::class.java) profileModelView = ProfileModelView(profileRepository) + eventRepository = mock(EventRepository::class.java) + eventViewModel = EventViewModel(eventRepository) } @Test fun testemptyView() { composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } // Check if all elements are displayed @@ -139,7 +149,11 @@ class ParticipantListScreenTest { fun testNonEmptyViewChangeRoleFailed() { composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } listTravelViewModel.selectTravel(container) @@ -203,7 +217,11 @@ class ParticipantListScreenTest { fun testNonEmptyViewChangeRoleToSameRole() { composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } listTravelViewModel.selectTravel(container) @@ -257,7 +275,11 @@ class ParticipantListScreenTest { fun testNonEmptyViewRemoveParticipant() { composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } listTravelViewModel.selectTravel(container) @@ -297,7 +319,11 @@ class ParticipantListScreenTest { fun testNonEmptyViewRemoveParticipantFail() { composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } listTravelViewModel.selectTravel(container) @@ -340,7 +366,11 @@ class ParticipantListScreenTest { listTravelViewModel.selectTravel(travelContainer) composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } // Open the Add User dialog @@ -380,7 +410,9 @@ class ParticipantListScreenTest { doAnswer { "abcdefghijklmnopqrst" }.`when`(notificationRepository).getNewUid() // Mock the repository.updateTravel method to do nothing - doNothing().`when`(travelRepository).updateTravel(any(), any(), anyOrNull(), any(), any()) + doNothing() + .`when`(travelRepository) + .updateTravel(any(), any(), anyOrNull(), any(), any(), anyOrNull()) // Click the Add User button composeTestRule.onNodeWithTag("addUserButton").performClick() @@ -396,7 +428,11 @@ class ParticipantListScreenTest { listTravelViewModel.selectTravel(travelContainer) composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } composeTestRule.onNodeWithTag("addUserFab").performClick() @@ -456,7 +492,11 @@ class ParticipantListScreenTest { listTravelViewModel.selectTravel(travelContainer) composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } composeTestRule.onNodeWithTag("addUserFab").performClick() @@ -500,7 +540,9 @@ class ParticipantListScreenTest { doAnswer { "abcdefghijklmnopqrst" }.`when`(notificationRepository).getNewUid() // Mock the repository.updateTravel method to do nothing - doNothing().`when`(travelRepository).updateTravel(any(), any(), anyOrNull(), any(), any()) + doNothing() + .`when`(travelRepository) + .updateTravel(any(), any(), anyOrNull(), any(), any(), anyOrNull()) doAnswer { "sigmasigmasigmasigm2" }.`when`(travelRepository).getNewUid() composeTestRule.onNodeWithTag("addUserButton").performClick() @@ -514,7 +556,11 @@ class ParticipantListScreenTest { listTravelViewModel.selectTravel(travelContainer) composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } composeTestRule.onNodeWithTag("addUserFab").performClick() @@ -558,7 +604,9 @@ class ParticipantListScreenTest { doAnswer { "abcdefghijklmnopqrst" }.`when`(notificationRepository).getNewUid() // Mock the repository.updateTravel method to do nothing - doNothing().`when`(travelRepository).updateTravel(any(), any(), anyOrNull(), any(), any()) + doNothing() + .`when`(travelRepository) + .updateTravel(any(), any(), anyOrNull(), any(), any(), anyOrNull()) composeTestRule.onNodeWithTag("addUserButton").performClick() verify(profileRepository).getFsUidByEmail(anyOrNull(), anyOrNull(), anyOrNull()) } @@ -569,7 +617,11 @@ class ParticipantListScreenTest { listTravelViewModel.selectTravel(travelContainer) composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } composeTestRule.onNodeWithTag("addUserFab").performClick() @@ -612,7 +664,9 @@ class ParticipantListScreenTest { doAnswer { "abcdefghijklmnopqrst" }.`when`(notificationRepository).getNewUid() // Mock the repository.updateTravel method to do nothing - doNothing().`when`(travelRepository).updateTravel(any(), any(), anyOrNull(), any(), any()) + doNothing() + .`when`(travelRepository) + .updateTravel(any(), any(), anyOrNull(), any(), any(), anyOrNull()) composeTestRule.onNodeWithTag("addUserButton").performClick() verify(profileRepository).getFsUidByEmail(anyOrNull(), anyOrNull(), anyOrNull()) verify(notificationRepository).addNotification(anyOrNull()) @@ -726,7 +780,11 @@ class ParticipantListScreenTest { fun addUserALotOfButton() { composeTestRule.setContent { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventViewModel) } // perform add user // Check that the dialog is displayed @@ -776,7 +834,9 @@ class ParticipantListScreenTest { .`when`(travelRepository) .checkParticipantExists(any(), any(), any()) // Mock the repository.updateTravel method to do nothing - doNothing().`when`(travelRepository).updateTravel(any(), any(), anyOrNull(), any(), any()) + doNothing() + .`when`(travelRepository) + .updateTravel(any(), any(), anyOrNull(), any(), any(), anyOrNull()) composeTestRule.onNodeWithTag("addUserButton").performClick() // Now this is a valid user that does exist @@ -797,7 +857,9 @@ class ParticipantListScreenTest { .`when`(travelRepository) .checkParticipantExists(any(), any(), any()) // Mock the repository.updateTravel method to do nothing - doNothing().`when`(travelRepository).updateTravel(any(), any(), anyOrNull(), any(), any()) + doNothing() + .`when`(travelRepository) + .updateTravel(any(), any(), anyOrNull(), any(), any(), anyOrNull()) composeTestRule.onNodeWithTag("addUserButton").performClick() } } diff --git a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt index 8a7be704..e94a874d 100644 --- a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt @@ -112,11 +112,16 @@ class MainActivity : ComponentActivity() { profileModelView) } - composable(Screen.ADD_ACTIVITY) { AddActivityScreen(navigationActions, activityModelView) } + composable(Screen.ADD_ACTIVITY) { + AddActivityScreen(navigationActions, activityModelView, eventViewModel = eventsViewModel) + } composable(Screen.EDIT_ACTIVITY) { EditActivity(navigationActions, activityModelView) } composable(Screen.ADD_TRAVEL) { AddTravelScreen( - listTravelViewModel, navigationActions, profileModelView = profileModelView) + listTravelViewModel, + navigationActions, + profileModelView = profileModelView, + eventViewModel = eventsViewModel) } composable(Screen.EDIT_TRAVEL_SETTINGS) { EditTravelSettingsScreen( @@ -133,7 +138,11 @@ class MainActivity : ComponentActivity() { composable(Screen.PARTICIPANT_LIST) { ParticipantListScreen( - listTravelViewModel, navigationActions, notificationViewModel, profileModelView) + listTravelViewModel, + navigationActions, + notificationViewModel, + profileModelView, + eventsViewModel) } composable(Screen.DOCUMENT_LIST) { DocumentListScreen( @@ -147,7 +156,7 @@ class MainActivity : ComponentActivity() { composable(Screen.DOCUMENT_PREVIEW) { DocumentPreview(documentViewModel, navigationActions) } - composable(Screen.TIMELINE) { TimelineScreen(eventsViewModel) } + composable(Screen.TIMELINE) { TimelineScreen(eventsViewModel, navigationActions) } composable(Screen.PROFILE) { ProfileScreen(navigationActions, profileModelView) } composable(Screen.EDIT_PROFILE) { diff --git a/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepository.kt b/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepository.kt index 07f9d1db..99581b3d 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepository.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepository.kt @@ -1,5 +1,7 @@ package com.github.se.travelpouch.model.activity +import com.google.firebase.firestore.DocumentReference + interface ActivityRepository { /** * This function allows us to retrieve all the activities from Firebase. @@ -26,8 +28,15 @@ interface ActivityRepository { * the database * @param onFailure ((Exception) -> Unit) : the function to call when an error occurs during the * adding of an activity to the database + * @param eventDocumentReference (DocumentReference) : The newly created event document reference + * to allow completion of the event at the creation of an activity */ - fun addActivity(activity: Activity, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) + fun addActivity( + activity: Activity, + onSuccess: () -> Unit, + onFailure: (Exception) -> Unit, + eventDocumentReference: DocumentReference + ) /** * The initialisation function of the interface diff --git a/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepositoryFirebase.kt b/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepositoryFirebase.kt index 91c8c1f3..f31f42c2 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepositoryFirebase.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityRepositoryFirebase.kt @@ -2,9 +2,12 @@ package com.github.se.travelpouch.model.activity import android.util.Log import com.github.se.travelpouch.model.FirebasePaths +import com.github.se.travelpouch.model.events.Event +import com.github.se.travelpouch.model.events.EventType import com.github.se.travelpouch.model.travels.Location import com.google.android.gms.tasks.Task import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FirebaseFirestore @@ -108,14 +111,33 @@ class ActivityRepositoryFirebase(private val db: FirebaseFirestore) : ActivityRe * the database * @param onFailure ((Exception) -> Unit) : the function to call when an error occurs during the * adding of an activity to the database + * @param eventDocumentReference (DocumentReference) : The newly created event document reference + * to allow completion of the event at the creation of an activity */ override fun addActivity( activity: Activity, onSuccess: () -> Unit, - onFailure: (Exception) -> Unit + onFailure: (Exception) -> Unit, + eventDocumentReference: DocumentReference ) { - performFirestoreOperation( - db.collection(collectionPath).document(activity.uid).set(activity), onSuccess, onFailure) + val activityDocumentReference = db.collection(collectionPath).document(activity.uid) + val event = + Event( + eventDocumentReference.id, + EventType.NEW_ACTIVITY, + Timestamp.now(), + activity.title, + "'${activity.title}' was added") + + db.runTransaction { + it.set(activityDocumentReference, activity) + it.set(eventDocumentReference, event) + } + .addOnSuccessListener { onSuccess() } + .addOnFailureListener { e -> + Log.e("ActivityRepositoryFirestore", "Error adding an activity", e) + onFailure(e) + } } /** diff --git a/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityViewModel.kt index 92d5a2ef..63a7dfdb 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/activity/ActivityViewModel.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import android.widget.Toast import androidx.lifecycle.ViewModel +import com.google.firebase.firestore.DocumentReference import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -42,8 +43,12 @@ class ActivityViewModel @Inject constructor(val activityRepositoryFirebase: Acti * This function adds an activity to the database * * @param activity (Activity) : the activity to add to the database + * @param context (Context) : The context of the UI to be able to display toast in case of success + * or failure of the function + * @param eventDocumentReference (DocumentReference) : The newly created event document reference + * to allow completion of the event at the creation of an activity */ - fun addActivity(activity: Activity, context: Context) { + fun addActivity(activity: Activity, context: Context, eventDocumentReference: DocumentReference) { activityRepositoryFirebase.addActivity( activity, onSuccess = { @@ -53,7 +58,8 @@ class ActivityViewModel @Inject constructor(val activityRepositoryFirebase: Acti onFailure = { Log.e(onFailureTag, "Failed to add an activity", it) Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show() - }) + }, + eventDocumentReference) } /** diff --git a/app/src/main/java/com/github/se/travelpouch/model/events/EventRepository.kt b/app/src/main/java/com/github/se/travelpouch/model/events/EventRepository.kt index a8cb1ce2..aa8f0974 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/events/EventRepository.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/events/EventRepository.kt @@ -2,7 +2,7 @@ package com.github.se.travelpouch.model.events import android.util.Log import com.github.se.travelpouch.model.FirebasePaths -import com.google.android.gms.tasks.Task +import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FirebaseFirestore @@ -18,12 +18,34 @@ class EventRepositoryFirebase(private val db: FirebaseFirestore) : EventReposito private var collectionPath = "" /** - * This function returns an unused unique identifier for a new event. + * This function returns an unused unique document reference for a new event when we don't know to + * which travel the event will be added. * - * @return (String) : an unused unique identifier + * @param travelId (String) : The travel id to which we have to link the event + * @return (DocumentReference) : The document reference to the new event */ - override fun getNewUid(): String { - return db.collection(collectionPath).document().id + override fun getNewDocumentReferenceForNewTravel(travelId: String): DocumentReference { + val newId = + db.collection(FirebasePaths.TravelsSuperCollection) + .document(travelId) + .collection(FirebasePaths.events) + .document() + .id + return db.collection(FirebasePaths.TravelsSuperCollection) + .document(travelId) + .collection(FirebasePaths.events) + .document(newId) + } + + /** + * This function returns an unused unique document reference for a new event when the travel id + * has being set. + * + * @return (DocumentReference) : The document reference to the new event + */ + override fun getNewDocumentReference(): DocumentReference { + val newId = db.collection(collectionPath).document().id + return db.collection(collectionPath).document(newId) } /** @@ -53,7 +75,7 @@ class EventRepositoryFirebase(private val db: FirebaseFirestore) : EventReposito .get() .addOnSuccessListener { result -> val events = result?.mapNotNull { documentToEvent(it) } ?: emptyList() - onSuccess(events) + onSuccess(events.sortedByDescending { it.date }) } .addOnFailureListener { e -> Log.e("EventRepository", "Error getting documents", e) @@ -61,44 +83,6 @@ class EventRepositoryFirebase(private val db: FirebaseFirestore) : EventReposito } } - /** - * This function adds an event to the collection of events in Firebase. - * - * @param event (Event) : the event we want to add on Firebase - * @param onSuccess (() -> Unit) : the function called when the event is correctly added to the - * database - * @param onFailure ((Exception) -> Unit) : the function called when an error occurs during the - * adding an event to the database - */ - override fun addEvent(event: Event, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) { - performFirestoreOperation( - db.collection(collectionPath).document(event.uid).set(event), onSuccess, onFailure) - } - - /** - * This function is a helper function that safely performs a Firebase operation. A task has - * listeners added to it. If the task is successful, we apply onSuccess. Otherwise we perform - * onFailure. - * - * @param task (Task) : a task to perform - * @param onSuccess (() -> Unit) : the function called when the event is correctly added to the - * database - * @param onFailure ((Exception) -> Unit) : the function called when an error occurs during the - * adding an event to the database - */ - private fun performFirestoreOperation( - task: Task, - onSuccess: () -> Unit, - onFailure: (Exception) -> Unit - ) { - task - .addOnSuccessListener { onSuccess() } - .addOnFailureListener { e -> - Log.e("EventRepositoryFirestore", "Error performing Firestore operation", e) - onFailure(e) - } - } - /** * This function converts a document got from Firebase to an event. It returns null if an error * occurs. @@ -113,19 +97,15 @@ class EventRepositoryFirebase(private val db: FirebaseFirestore) : EventReposito val title = document.getString("title") val description = document.getString("description") val date = document.getTimestamp("date") - val documents = document.get("listUploadedDocuments") as? Map val eventTypeString = document.getString("eventType") val eventType = EventType.valueOf(eventTypeString!!) - val uidParticipant = document.getString("uidParticipant") Event( uid = uid, title = title!!, description = description!!, date = date!!, - eventType = eventType, - uidParticipant = uidParticipant, - listUploadedDocuments = documents) + eventType = eventType) } catch (e: Exception) { Log.e("EventRepository", "Error converting document to Event", e) null diff --git a/app/src/main/java/com/github/se/travelpouch/model/events/EventViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/events/EventViewModel.kt index 4386e696..1d7a807f 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/events/EventViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/events/EventViewModel.kt @@ -1,6 +1,7 @@ package com.github.se.travelpouch.model.events import androidx.lifecycle.ViewModel +import com.google.firebase.firestore.DocumentReference import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -25,25 +26,28 @@ class EventViewModel @Inject constructor(private val repository: EventRepository } /** - * This function returns a new unused unique identifier. + * This function returns an unused unique document reference for a new event when we don't know to + * which travel the event will be added. * - * @return (String) : returns a new unused unique identifier + * @param travelId (String) : The travel id to which we have to link the event + * @return (DocumentReference) : The document reference to the new event */ - fun getNewUid(): String { - return repository.getNewUid() - } - - /** This function updates the list of events stored on firebase. */ - fun getEvents() { - repository.getEvents(onSuccess = { events_.value = it }, onFailure = {}) + fun getNewDocumentReferenceForNewTravel(travelId: String): DocumentReference { + return repository.getNewDocumentReferenceForNewTravel(travelId) } /** - * This function adds a new event in Firebase + * This function returns an unused unique document reference for a new event when the travel id + * has being set. * - * @param event (Event) : a new event to add in Firebase + * @return (DocumentReference) : The document reference to the new event */ - fun addEvent(event: Event) { - repository.addEvent(event = event, onSuccess = { getEvents() }, onFailure = {}) + fun getNewDocumentReference(): DocumentReference { + return repository.getNewDocumentReference() + } + + /** This function updates the list of events stored on firebase. */ + fun getEvents() { + repository.getEvents(onSuccess = { events_.value = it }, onFailure = {}) } } diff --git a/app/src/main/java/com/github/se/travelpouch/model/events/Events.kt b/app/src/main/java/com/github/se/travelpouch/model/events/Events.kt index 936a36b5..548db296 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/events/Events.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/events/Events.kt @@ -11,24 +11,19 @@ import com.google.firebase.Timestamp * @param date (Timestamp) : the date when the event occurred * @param title (String) : the title of the event * @param description (String) : the description of the event - * @param uidParticipant (String?) : the uid of the participant that triggered the event - * @param listUploadedDocuments (Map) : the list of all the documents uploaded during - * this event. We use a map because it is safer to store on firebase than an array. */ data class Event( val uid: String, val eventType: EventType, val date: Timestamp, val title: String, - val description: String, - val uidParticipant: String?, - val listUploadedDocuments: Map? + val description: String ) /** The enum class representing the type of event that can occur. */ enum class EventType { START_OF_JOURNEY, NEW_PARTICIPANT, - NEW_DOCUMENT, - OTHER_EVENT + PARTICIPANT_REMOVED, + NEW_ACTIVITY } diff --git a/app/src/main/java/com/github/se/travelpouch/model/events/Repository.kt b/app/src/main/java/com/github/se/travelpouch/model/events/Repository.kt index a4037db4..2a103ddd 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/events/Repository.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/events/Repository.kt @@ -1,5 +1,7 @@ package com.github.se.travelpouch.model.events +import com.google.firebase.firestore.DocumentReference + /** * An interface representing the event repository that is used to perform operations on Firebase. It * is used to retrieve events and store them. @@ -18,22 +20,21 @@ interface EventRepository { fun getEvents(onSuccess: (List) -> Unit, onFailure: (Exception) -> Unit) /** - * This function returns an unused unique identifier for a new event. + * This function returns an unused unique document reference for a new event when we don't know to + * which travel the event will be added. * - * @return (String) : an unused unique identifier + * @param travelId (String) : The travel id to which we have to link the event + * @return (DocumentReference) : The document reference to the new event */ - fun getNewUid(): String + fun getNewDocumentReferenceForNewTravel(travelId: String): DocumentReference /** - * This function adds an event to the collection of events in Firebase. + * This function returns an unused unique document reference for a new event when the travel id + * has being set. * - * @param event (Event) : the event we want to add on Firebase - * @param onSuccess (() -> Unit) : the function called when the event is correctly added to the - * database - * @param onFailure ((Exception) -> Unit) : the function called when an error occurs during the - * adding an event to the database + * @return (DocumentReference) : The document reference to the new event */ - fun addEvent(event: Event, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) + fun getNewDocumentReference(): DocumentReference /** * The initialisation function of the repository. diff --git a/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt b/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt index c9e7f776..fe4ef544 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/travels/ListTravelViewModel.kt @@ -4,6 +4,7 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.se.travelpouch.model.profile.Profile +import com.google.firebase.firestore.DocumentReference import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -127,8 +128,10 @@ open class ListTravelViewModel @Inject constructor(private val repository: Trave * Adds a Travel document. * * @param travel The Travel document to be added. + * @param eventDocumentReference (DocumentReference) : The newly created event document reference + * to allow completion of the event at the creation of a travel */ - fun addTravel(travel: TravelContainer) { + fun addTravel(travel: TravelContainer, eventDocumentReference: DocumentReference) { Log.d("ListTravelViewModel", "Adding travel") repository.addTravel( travel = travel, @@ -136,25 +139,35 @@ open class ListTravelViewModel @Inject constructor(private val repository: Trave Log.d("ListTravelViewModel", "Successfully added travel") getTravels() }, - onFailure = { Log.e("ListTravelViewModel", "Failed to add travel", it) }) + onFailure = { Log.e("ListTravelViewModel", "Failed to add travel", it) }, + eventDocumentReference) } /** * Updates a Travel document. * * @param travel The Travel document to be updated. + * @param modeOfUpdate (TravelRepository.UpdateMode) : The mode of update of the travel (only + * changing the fields, adding a participant or removing a participant) + * @param fsUidOfAddedParticipant (String?) The fsUid of the participant to be added or removed. + * It is null if we only update the fields of the travels + * @param eventDocumentReference (DocumentReference?) : The newly created event document reference + * to allow completion of the event at the update of a travel. It is null if we only update the + * fields of the travel */ fun updateTravel( travel: TravelContainer, modeOfUpdate: TravelRepository.UpdateMode, - fsUidOfAddedParticipant: String? + fsUidOfAddedParticipant: String?, + eventDocumentReference: DocumentReference? ) { repository.updateTravel( travel = travel, modeOfUpdate = modeOfUpdate, fsUidOfAddedParticipant = fsUidOfAddedParticipant, onSuccess = { getTravels() }, - onFailure = { Log.e("ListTravelViewModel", "Failed to update travel", it) }) + onFailure = { Log.e("ListTravelViewModel", "Failed to update travel", it) }, + eventDocumentReference) } /** @@ -228,12 +241,15 @@ open class ListTravelViewModel @Inject constructor(private val repository: Trave * @param onSuccess A callback function to be invoked with the updated travel document upon * successful addition. * @param onFailure A callback function to be invoked if the addition fails. + * @param eventDocumentReference (DocumentReference) : The newly created event document reference + * to allow completion of the event at the addition of a user to a travel */ fun addUserToTravel( email: String, selectedTravel: TravelContainer, onSuccess: (TravelContainer) -> Unit, - onFailure: () -> Unit + onFailure: () -> Unit, + eventDocumentReference: DocumentReference ) { checkParticipantExists( email, @@ -249,7 +265,11 @@ open class ListTravelViewModel @Inject constructor(private val repository: Trave selectedTravel.copy( allParticipants = newParticipantMap.toMap(), listParticipant = newParticipantList.toList()) - updateTravel(newTravel, TravelRepository.UpdateMode.ADD_PARTICIPANT, user.fsUid) + updateTravel( + newTravel, + TravelRepository.UpdateMode.ADD_PARTICIPANT, + user.fsUid, + eventDocumentReference) onSuccess(newTravel) }, onFailure = { onFailure() }) diff --git a/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepository.kt b/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepository.kt index 3cbf96bd..55f4ac68 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepository.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepository.kt @@ -1,6 +1,7 @@ package com.github.se.travelpouch.model.travels import com.github.se.travelpouch.model.profile.Profile +import com.google.firebase.firestore.DocumentReference interface TravelRepository { @@ -34,14 +35,20 @@ interface TravelRepository { onFailure: (Exception) -> Unit ) - fun addTravel(travel: TravelContainer, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) + fun addTravel( + travel: TravelContainer, + onSuccess: () -> Unit, + onFailure: (Exception) -> Unit, + eventDocumentReference: DocumentReference + ) fun updateTravel( travel: TravelContainer, modeOfUpdate: UpdateMode, fsUidOfAddedParticipant: String?, onSuccess: () -> Unit, - onFailure: (Exception) -> Unit + onFailure: (Exception) -> Unit, + eventDocumentReference: DocumentReference? ) fun deleteTravelById(id: String, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) diff --git a/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryFirestore.kt b/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryFirestore.kt index 32d1aa0e..81f1a22f 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryFirestore.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryFirestore.kt @@ -2,12 +2,15 @@ package com.github.se.travelpouch.model.travels import android.util.Log import com.github.se.travelpouch.model.FirebasePaths +import com.github.se.travelpouch.model.events.Event +import com.github.se.travelpouch.model.events.EventType import com.github.se.travelpouch.model.profile.Profile import com.github.se.travelpouch.model.profile.ProfileRepositoryConvert import com.google.android.gms.tasks.Task import com.google.firebase.Firebase import com.google.firebase.Timestamp import com.google.firebase.auth.auth +import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FirebaseFirestore @@ -118,11 +121,14 @@ class TravelRepositoryFirestore(private val db: FirebaseFirestore) : TravelRepos * @param travel The travel document to add. * @param onSuccess The callback to call if the operation is successful. * @param onFailure The callback to call if the operation fails. + * @param eventDocumentReference (DocumentReference) : The newly created event document reference + * to allow completion of the event at the creation of a travel */ override fun addTravel( travel: TravelContainer, onSuccess: () -> Unit, - onFailure: (Exception) -> Unit + onFailure: (Exception) -> Unit, + eventDocumentReference: DocumentReference ) { Log.d("TravelRepositoryFirestore", "addTravel") @@ -130,6 +136,14 @@ class TravelRepositoryFirestore(private val db: FirebaseFirestore) : TravelRepos db.collection(FirebasePaths.ProfilesSuperCollection).document(currentUserUid) val travelDocumentReference = db.collection(collectionPath).document(travel.fsUid) + val event = + Event( + eventDocumentReference.id, + EventType.START_OF_JOURNEY, + Timestamp.now(), + travel.title, + "Let's get started with ${travel.title}") + db.runTransaction { val profile = it.get(profileDocumentReference) val travelListProfile = profile.get("userTravelList") as? List ?: emptyList() @@ -137,6 +151,7 @@ class TravelRepositoryFirestore(private val db: FirebaseFirestore) : TravelRepos travelList.add(travel.fsUid) it.update(profileDocumentReference, "userTravelList", travelList.toList()) it.set(travelDocumentReference, travel.toMap()) + it.set(eventDocumentReference, event) } .addOnSuccessListener { onSuccess() } .addOnFailureListener { e -> @@ -151,13 +166,21 @@ class TravelRepositoryFirestore(private val db: FirebaseFirestore) : TravelRepos * @param travel The travel document to update. * @param onSuccess The callback to call if the operation is successful. * @param onFailure The callback to call if the operation fails. + * @param modeOfUpdate (TravelRepository.UpdateMode) : The mode of update of the travel (only + * changing the fields, adding a participant or removing a participant) + * @param fsUidOfAddedParticipant (String?) The fsUid of the participant to be added or removed. + * It is null if we only update the fields of the travels + * @param eventDocumentReference (DocumentReference?) : The newly created event document reference + * to allow completion of the event at the update of a travel. It is null if we only update the + * fields of the travel */ override fun updateTravel( travel: TravelContainer, modeOfUpdate: TravelRepository.UpdateMode, fsUidOfAddedParticipant: String?, onSuccess: () -> Unit, - onFailure: (Exception) -> Unit + onFailure: (Exception) -> Unit, + eventDocumentReference: DocumentReference? ) { Log.d("TravelRepositoryFirestore", "updateTravel") when (modeOfUpdate) { @@ -168,6 +191,7 @@ class TravelRepositoryFirestore(private val db: FirebaseFirestore) : TravelRepos onFailure) } TravelRepository.UpdateMode.ADD_PARTICIPANT -> { + val travelDocumentReference = db.collection(collectionPath).document(travel.fsUid) val addedUserDocumentReference = db.collection(userCollectionPath).document(fsUidOfAddedParticipant!!) @@ -176,11 +200,20 @@ class TravelRepositoryFirestore(private val db: FirebaseFirestore) : TravelRepos val currentAddedUserProfile = ProfileRepositoryConvert.documentToProfile(it.get(addedUserDocumentReference)) + val event = + Event( + eventDocumentReference!!.id, + EventType.NEW_PARTICIPANT, + Timestamp.now(), + "${currentAddedUserProfile.email} joined the travel.", + "${currentAddedUserProfile.email} joined the travel.") + val listTravelUpdated = currentAddedUserProfile.userTravelList.toMutableList() listTravelUpdated.add(travel.fsUid) it.set(travelDocumentReference, travel.toMap()) it.update(addedUserDocumentReference, "userTravelList", listTravelUpdated.toList()) + it.set(eventDocumentReference, event) } .addOnSuccessListener { onSuccess() } .addOnFailureListener { e -> @@ -197,11 +230,20 @@ class TravelRepositoryFirestore(private val db: FirebaseFirestore) : TravelRepos val currentAddedUserProfile = ProfileRepositoryConvert.documentToProfile(it.get(addedUserDocumentReference)) + val event = + Event( + eventDocumentReference!!.id, + EventType.PARTICIPANT_REMOVED, + Timestamp.now(), + "${currentAddedUserProfile.email} was removed from the travel.", + "${currentAddedUserProfile.email} was removed from the travel.") + val listTravelUpdated = currentAddedUserProfile.userTravelList.toMutableList() listTravelUpdated.remove(travel.fsUid) it.set(travelDocumentReference, travel.toMap()) it.update(addedUserDocumentReference, "userTravelList", listTravelUpdated.toList()) + it.set(eventDocumentReference, event) } .addOnSuccessListener { onSuccess() } .addOnFailureListener { e -> diff --git a/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryMock.kt b/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryMock.kt index 2d6abf0d..768ebf93 100644 --- a/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryMock.kt +++ b/app/src/main/java/com/github/se/travelpouch/model/travels/TravelRepositoryMock.kt @@ -2,6 +2,7 @@ package com.github.se.travelpouch.model.travels import com.github.se.travelpouch.di.travelCollection import com.github.se.travelpouch.model.profile.Profile +import com.google.firebase.firestore.DocumentReference class TravelRepositoryMock : TravelRepository { @@ -52,7 +53,8 @@ class TravelRepositoryMock : TravelRepository { override fun addTravel( travel: TravelContainer, onSuccess: () -> Unit, - onFailure: (Exception) -> Unit + onFailure: (Exception) -> Unit, + documentReference: DocumentReference ) { travelCollection[travel.fsUid] = travel onSuccess() @@ -63,7 +65,8 @@ class TravelRepositoryMock : TravelRepository { modeOfUpdate: TravelRepository.UpdateMode, fsUidOfAddedParticipant: String?, onSuccess: () -> Unit, - onFailure: (Exception) -> Unit + onFailure: (Exception) -> Unit, + eventDocumentReference: DocumentReference? ) { travelCollection[travel.fsUid] = travel onSuccess() diff --git a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/AddActivity.kt b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/AddActivity.kt index 41f7176a..1773d38d 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/AddActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/AddActivity.kt @@ -24,6 +24,7 @@ import androidx.core.text.isDigitsOnly import androidx.lifecycle.viewmodel.compose.viewModel import com.github.se.travelpouch.model.activity.Activity import com.github.se.travelpouch.model.activity.ActivityViewModel +import com.github.se.travelpouch.model.events.EventViewModel import com.github.se.travelpouch.model.location.LocationViewModel import com.github.se.travelpouch.model.travels.Location import com.github.se.travelpouch.ui.navigation.NavigationActions @@ -42,7 +43,8 @@ import java.util.* fun AddActivityScreen( navigationActions: NavigationActions, activityModelView: ActivityViewModel, - locationViewModel: LocationViewModel = viewModel(factory = LocationViewModel.Factory) + locationViewModel: LocationViewModel = viewModel(factory = LocationViewModel.Factory), + eventViewModel: EventViewModel ) { var title by remember { mutableStateOf("") } @@ -240,7 +242,8 @@ fun AddActivityScreen( finalDate, mapOf()) - activityModelView.addActivity(activity, context) + activityModelView.addActivity( + activity, context, eventViewModel.getNewDocumentReference()) navigationActions.navigateTo(Screen.SWIPER) } catch (e: java.text.ParseException) { diff --git a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Timeline.kt b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Timeline.kt index 13552a61..011a56e1 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Timeline.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Timeline.kt @@ -1,21 +1,27 @@ package com.github.se.travelpouch.ui.dashboard -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,8 +38,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.github.se.travelpouch.model.events.Event import com.github.se.travelpouch.model.events.EventType import com.github.se.travelpouch.model.events.EventViewModel -import java.util.Calendar -import java.util.GregorianCalendar +import com.github.se.travelpouch.ui.navigation.NavigationActions +import java.text.SimpleDateFormat +import java.util.Locale +import okhttp3.internal.format // credit to the website : // https://medium.com/proandroiddev/a-step-by-step-guide-to-building-a-timeline-component-with-jetpack-compose-358a596847cb @@ -44,6 +52,8 @@ enum class Paddings(val padding: Dp) { SPACER_MORE_RIGHT(96.dp) } +val format = SimpleDateFormat("dd/MM/yyyy 'at' hh:mm:ss a", Locale.getDefault()) + /** * A data class representing a circle * @@ -66,67 +76,87 @@ data class LineParameters(val strokeWidth: Dp, val brush: Brush) * * @param eventsViewModel (EventViewModel) : the view model used to manage the events */ -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun TimelineScreen(eventsViewModel: EventViewModel = hiltViewModel()) { +fun TimelineScreen( + eventsViewModel: EventViewModel = hiltViewModel(), + navigationActions: NavigationActions +) { + + LaunchedEffect(Unit) { eventsViewModel.getEvents() } var itemMoreRightOfScreen = false val events = eventsViewModel.events.collectAsState() Scaffold( modifier = Modifier.testTag("timelineScreen"), - ) { - if (events.value.isNotEmpty()) { - LazyColumn( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).testTag("timelineColumn"), - contentPadding = PaddingValues(vertical = 16.dp), - ) { - val size = events.value.size + topBar = { + TopAppBar( + title = { + Text( + "Your travel Milestone", + textAlign = TextAlign.Center, + modifier = Modifier.testTag("screenTitle")) + }, + navigationIcon = { + IconButton( + onClick = { navigationActions.goBack() }, + modifier = Modifier.testTag("goBackButton")) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back") + } + }) + }) { pd -> + if (events.value.isNotEmpty()) { + LazyColumn( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(pd) + .testTag("timelineColumn"), + contentPadding = PaddingValues(vertical = 16.dp), + ) { + val size = events.value.size + + items(size) { index -> + val color = mapEventTypeToColor(events.value[index].eventType) + val nextColor = + if (index < size - 1) mapEventTypeToColor(events.value[index + 1].eventType) + else null - item { + TimelineNode( + contentStartOffset = + if (itemMoreRightOfScreen) { + Paddings.SPACER_MORE_RIGHT.padding + } else { + Paddings.SPACER_LESS_RIGHT.padding + }, + spacerBetweenNodes = Paddings.SPACER_BETWEEN_NODES.padding, + circleParameters = + CircleParametersDefaults.circleParameters(backgroundColor = color), + lineParameters = + if (nextColor != null) + LineParametersDefaults.linearGradient( + startColor = color, endColor = nextColor) + else null) { modifier -> + TimelineItem(events.value[index], modifier) + } + + itemMoreRightOfScreen = !itemMoreRightOfScreen + } + } + } else { Box( modifier = Modifier.fillMaxSize().padding(20.dp), contentAlignment = Alignment.Center) { Text( - text = "Your travel Milestone", + text = "No events", textAlign = TextAlign.Center, - modifier = Modifier.testTag("screenTitle")) + modifier = Modifier.testTag("loadingText")) } } - items(size) { index -> - val color = mapEventTypeToColor(events.value[index].eventType) - val nextColor = - if (index < size - 1) mapEventTypeToColor(events.value[index + 1].eventType) else null - - TimelineNode( - contentStartOffset = - if (itemMoreRightOfScreen) { - Paddings.SPACER_MORE_RIGHT.padding - } else { - Paddings.SPACER_LESS_RIGHT.padding - }, - spacerBetweenNodes = Paddings.SPACER_BETWEEN_NODES.padding, - circleParameters = CircleParametersDefaults.circleParameters(backgroundColor = color), - lineParameters = - if (nextColor != null) - LineParametersDefaults.linearGradient( - startColor = color, endColor = nextColor) - else null) { modifier -> - TimelineItem(events.value[index], modifier) - } - - itemMoreRightOfScreen = !itemMoreRightOfScreen - } - } - } else { - Box(modifier = Modifier.fillMaxSize().padding(20.dp), contentAlignment = Alignment.Center) { - Text( - text = "Loading...", - textAlign = TextAlign.Center, - modifier = Modifier.testTag("loadingText")) } - } - } } /** @@ -138,21 +168,17 @@ fun TimelineScreen(eventsViewModel: EventViewModel = hiltViewModel Color.LightGray.copy(alpha = 0.3f) - EventType.NEW_DOCUMENT -> Color.Green.copy(alpha = 0.3f) EventType.START_OF_JOURNEY -> Color.Blue.copy(alpha = 0.3f) EventType.NEW_PARTICIPANT -> Color.Red.copy(alpha = 0.3f) + EventType.PARTICIPANT_REMOVED -> Color.Green.copy(alpha = 0.3f) + EventType.NEW_ACTIVITY -> Color.Yellow.copy(alpha = 0.3f) } } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/home/AddTravel.kt b/app/src/main/java/com/github/se/travelpouch/ui/home/AddTravel.kt index 49693086..101c1add 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/home/AddTravel.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/home/AddTravel.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.dp import androidx.compose.ui.window.PopupProperties import androidx.lifecycle.viewmodel.compose.viewModel +import com.github.se.travelpouch.model.events.EventViewModel import com.github.se.travelpouch.model.location.LocationViewModel import com.github.se.travelpouch.model.profile.ProfileModelView import com.github.se.travelpouch.model.travels.ListTravelViewModel @@ -61,6 +62,9 @@ import com.google.firebase.Timestamp * @param listTravelViewModel: The ViewModel that manages the list of travels in the app. * @param navigationActions: The navigation actions to handle navigation within the app. * @param locationViewModel: The ViewModel that manages the location search functionality. + * @param profileModelView (ProfileViewModel) : The ViewModel that manages the profile of the + * current user + * @param eventViewModel (EventViewModel) : The ViewModel that manages the events of the travel */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,7 +72,8 @@ fun AddTravelScreen( listTravelViewModel: ListTravelViewModel, navigationActions: NavigationActions, locationViewModel: LocationViewModel = viewModel(factory = LocationViewModel.Factory), - profileModelView: ProfileModelView + profileModelView: ProfileModelView, + eventViewModel: EventViewModel ) { var title by remember { mutableStateOf("") } var description by remember { mutableStateOf("") } @@ -292,7 +297,10 @@ fun AddTravelScreen( try { // Call the ViewModel method to add the travel data Log.d("AddTravelScreen", "Adding travel to ViewModel") - listTravelViewModel.addTravel(travelContainer) + listTravelViewModel.addTravel( + travelContainer, + eventViewModel.getNewDocumentReferenceForNewTravel( + travelContainer.fsUid)) Toast.makeText(context, "Travel added successfully!", Toast.LENGTH_SHORT) .show() diff --git a/app/src/main/java/com/github/se/travelpouch/ui/navigation/SwipePager.kt b/app/src/main/java/com/github/se/travelpouch/ui/navigation/SwipePager.kt index cb2da19e..866c5e2a 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/navigation/SwipePager.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/navigation/SwipePager.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack -import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.AccountBalance import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -99,7 +99,7 @@ fun SwipePager( IconButton( onClick = { navigationActions.navigateTo(Screen.TIMELINE) }, modifier = Modifier.testTag("eventTimelineButton")) { - Icon(imageVector = Icons.Default.DateRange, contentDescription = null) + Icon(imageVector = Icons.Default.AccountBalance, contentDescription = null) } }) }, diff --git a/app/src/main/java/com/github/se/travelpouch/ui/notifications/NotificationItem.kt b/app/src/main/java/com/github/se/travelpouch/ui/notifications/NotificationItem.kt index 0f5f99c4..ed03d12c 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/notifications/NotificationItem.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/notifications/NotificationItem.kt @@ -75,7 +75,8 @@ fun NotificationItem( listTravelViewModel, profileViewModel, notificationViewModel, - context) + context, + eventsViewModel) } } } @@ -106,15 +107,26 @@ fun InvitationButtons( listTravelViewModel: ListTravelViewModel, profileViewModel: ProfileModelView, notificationViewModel: NotificationViewModel, - context: android.content.Context + context: android.content.Context, + eventsViewModel: EventViewModel ) { Row( modifier = Modifier.fillMaxWidth().testTag("notification_item_buttons"), horizontalArrangement = Arrangement.Center) { AcceptButton( - notification, listTravelViewModel, profileViewModel, notificationViewModel, context) + notification, + listTravelViewModel, + profileViewModel, + notificationViewModel, + context, + eventsViewModel) DeclineButton( - notification, listTravelViewModel, profileViewModel, notificationViewModel, context) + notification, + listTravelViewModel, + profileViewModel, + notificationViewModel, + context, + eventsViewModel) } } @@ -124,7 +136,8 @@ fun AcceptButton( listTravelViewModel: ListTravelViewModel, profileViewModel: ProfileModelView, notificationViewModel: NotificationViewModel, - context: android.content.Context + context: android.content.Context, + eventsViewModel: EventViewModel ) { Button( onClick = { @@ -134,7 +147,8 @@ fun AcceptButton( profileViewModel, notificationViewModel, context, - isAccepted = true) + isAccepted = true, + eventsViewModel) }, modifier = Modifier.padding(end = 8.dp).testTag("notification_item_accept_button"), colors = @@ -150,7 +164,8 @@ fun DeclineButton( listTravelViewModel: ListTravelViewModel, profileViewModel: ProfileModelView, notificationViewModel: NotificationViewModel, - context: android.content.Context + context: android.content.Context, + eventsViewModel: EventViewModel ) { Button( onClick = { @@ -160,7 +175,8 @@ fun DeclineButton( profileViewModel, notificationViewModel, context, - isAccepted = false) + isAccepted = false, + eventsViewModel = eventsViewModel) }, colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent, contentColor = Color.Red), @@ -175,7 +191,8 @@ fun handleInvitationResponse( profileViewModel: ProfileModelView, notificationViewModel: NotificationViewModel, context: android.content.Context, - isAccepted: Boolean + isAccepted: Boolean, + eventsViewModel: EventViewModel ) { when (notification.sector) { NotificationSector.TRAVEL -> { @@ -206,7 +223,8 @@ fun handleInvitationResponse( listTravelViewModel.selectTravel(updatedContainer) Toast.makeText(context, "User added successfully!", Toast.LENGTH_SHORT).show() }, - { Toast.makeText(context, "Failed to add user", Toast.LENGTH_SHORT).show() }) + { Toast.makeText(context, "Failed to add user", Toast.LENGTH_SHORT).show() }, + eventsViewModel.getNewDocumentReferenceForNewTravel(travel.fsUid)) } Toast.makeText(context, responseMessage, Toast.LENGTH_SHORT).show() }, @@ -248,6 +266,7 @@ fun handleInvitationResponse( sector = notification.sector) notificationViewModel.sendNotification(invitationResponse) + Toast.makeText(context, "Request declined", Toast.LENGTH_LONG).show() } } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/travel/EditTravelSettings.kt b/app/src/main/java/com/github/se/travelpouch/ui/travel/EditTravelSettings.kt index b7f9e38e..8d7d1226 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/travel/EditTravelSettings.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/travel/EditTravelSettings.kt @@ -385,7 +385,7 @@ fun EditTravelSettingsScreen( allParticipants = selectedTravel!!.allParticipants, listParticipant = selectedTravel!!.listParticipant) listTravelViewModel.updateTravel( - newTravel, TravelRepository.UpdateMode.FIELDS_UPDATE, null) + newTravel, TravelRepository.UpdateMode.FIELDS_UPDATE, null, null) Toast.makeText(context, "Save clicked", Toast.LENGTH_SHORT).show() } catch (e: ParseException) { Toast.makeText(context, "Error: due date invalid", Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/github/se/travelpouch/ui/travel/ParticipantListScreen.kt b/app/src/main/java/com/github/se/travelpouch/ui/travel/ParticipantListScreen.kt index ca178502..3aefa49c 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/travel/ParticipantListScreen.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/travel/ParticipantListScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import com.github.se.travelpouch.model.events.EventViewModel import com.github.se.travelpouch.model.notifications.Notification import com.github.se.travelpouch.model.notifications.NotificationContent import com.github.se.travelpouch.model.notifications.NotificationSector @@ -71,7 +72,8 @@ fun ParticipantListScreen( listTravelViewModel: ListTravelViewModel, navigationActions: NavigationActions, notificationViewModel: NotificationViewModel, - profileViewModel: ProfileModelView + profileViewModel: ProfileModelView, + eventViewModel: EventViewModel ) { val context = LocalContext.current val selectedTravel by listTravelViewModel.selectedTravel.collectAsState() @@ -441,7 +443,7 @@ fun handleRoleChange( participantMap[Participant(participant.key)] = newRole val updatedContainer = selectedTravel.copy(allParticipants = participantMap.toMap()) listTravelViewModel.updateTravel( - updatedContainer, TravelRepository.UpdateMode.FIELDS_UPDATE, null) + updatedContainer, TravelRepository.UpdateMode.FIELDS_UPDATE, null, null) listTravelViewModel.selectTravel(updatedContainer) setExpandedRoleDialog(false) setExpanded(false) diff --git a/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityModelViewUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityModelViewUnitTest.kt index 4d2e6537..fda9712e 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityModelViewUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityModelViewUnitTest.kt @@ -3,6 +3,7 @@ package com.github.se.travelpouch.model.activity import android.content.Context import com.github.se.travelpouch.model.travels.Location import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentReference import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat import org.junit.Before @@ -18,6 +19,7 @@ class ActivityModelViewUnitTest { private lateinit var repository: ActivityRepository private lateinit var activityViewModel: ActivityViewModel private lateinit var mockContext: Context + private lateinit var mockDocumentReference: DocumentReference val activity = Activity( @@ -51,6 +53,7 @@ class ActivityModelViewUnitTest { repository = mock(ActivityRepository::class.java) activityViewModel = ActivityViewModel(repository) mockContext = mock(Context::class.java) + mockDocumentReference = mock() } @Test @@ -61,8 +64,8 @@ class ActivityModelViewUnitTest { @Test fun addActivitiesTest() { - activityViewModel.addActivity(activity, mockContext) - verify(repository).addActivity(anyOrNull(), anyOrNull(), anyOrNull()) + activityViewModel.addActivity(activity, mockContext, mockDocumentReference) + verify(repository).addActivity(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test diff --git a/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityRepositoryUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityRepositoryUnitTest.kt index b43dba7d..63bda520 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityRepositoryUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/activity/ActivityRepositoryUnitTest.kt @@ -3,6 +3,8 @@ package com.github.se.travelpouch.model.activity import android.os.Looper import androidx.test.core.app.ApplicationProvider import com.github.se.travelpouch.model.travels.Location +import com.google.android.gms.tasks.OnSuccessListener +import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.firebase.FirebaseApp import com.google.firebase.Timestamp @@ -12,6 +14,7 @@ import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.QuerySnapshot import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse import junit.framework.TestCase.fail import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat @@ -23,8 +26,11 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.timeout import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf @@ -113,16 +119,37 @@ class ActivityRepositoryUnitTest { @Test fun addActivity_shouldCallFirestoreCollection() { - `when`(mockDocumentReference.set(any())).thenReturn(Tasks.forResult(null)) // Simulate success - // This test verifies that when we add a new event, the Firestore `collection()` method is - // called. - activityRepositoryFirestore.addActivity(activity, onSuccess = {}, onFailure = {}) + val mockFirebaseFirestore: FirebaseFirestore = mock() + val mockCollectionReference: CollectionReference = mock() + val mockActivityDocumentReference: DocumentReference = mock() + val mockEventDocumentReference: DocumentReference = mock() - shadowOf(Looper.getMainLooper()).idle() + val mockVoidTask: Task = mock() - // Ensure Firestore collection method was called to reference the "events" collection - verify(mockDocumentReference).set(any()) + whenever(mockFirebaseFirestore.collection(anyOrNull())).thenReturn(mockCollectionReference) + whenever(mockCollectionReference.document(anyOrNull())) + .thenReturn(mockActivityDocumentReference) + whenever(mockEventDocumentReference.id).thenReturn("qwertzuiopasdfghjklm") + + whenever(mockFirebaseFirestore.runTransaction(anyOrNull())).thenReturn(mockVoidTask) + whenever(mockVoidTask.addOnSuccessListener(anyOrNull())).thenReturn(mockVoidTask) + whenever(mockVoidTask.addOnFailureListener(anyOrNull())).thenReturn(mockVoidTask) + whenever(mockVoidTask.isSuccessful).thenReturn(true) + + var succeeded = false + var failed = false + + val activityRepositoryFirebase = ActivityRepositoryFirebase(mockFirebaseFirestore) + activityRepositoryFirebase.addActivity( + activity, { succeeded = true }, { failed = true }, mockEventDocumentReference) + + val onSuccessListenerCaptor = argumentCaptor>() + verify(mockVoidTask).addOnSuccessListener(onSuccessListenerCaptor.capture()) + onSuccessListenerCaptor.firstValue.onSuccess(mockVoidTask.result) + + assert(succeeded) + assertFalse(failed) } @Test diff --git a/app/src/test/java/com/github/se/travelpouch/model/events/EventModelViewUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/events/EventModelViewUnitTest.kt index 095f42fa..8e9a95d3 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/events/EventModelViewUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/events/EventModelViewUnitTest.kt @@ -1,6 +1,7 @@ package com.github.se.travelpouch.model.events import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentReference import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat import org.junit.Before @@ -15,7 +16,7 @@ class EventModelViewUnitTest { private lateinit var repository: EventRepository private lateinit var eventViewModel: EventViewModel - val event = Event("1", EventType.NEW_DOCUMENT, Timestamp(0, 0), "it", "it", null, null) + val event = Event("1", EventType.NEW_ACTIVITY, Timestamp(0, 0), "it", "it") @Before fun setUp() { @@ -29,15 +30,14 @@ class EventModelViewUnitTest { verify(repository).getEvents(anyOrNull(), anyOrNull()) } - @Test - fun addEventTest() { - eventViewModel.addEvent(event) - verify(repository).addEvent(anyOrNull(), anyOrNull(), anyOrNull()) - } - @Test fun getNewUidTest() { - `when`(repository.getNewUid()).thenReturn("uid") - assertThat(eventViewModel.getNewUid(), `is`("uid")) + val documentReference: DocumentReference = mock() + + `when`(repository.getNewDocumentReferenceForNewTravel(anyOrNull())) + .thenReturn(documentReference) + assertThat( + eventViewModel.getNewDocumentReferenceForNewTravel("qwertzuiopasdfghjkly"), + `is`(documentReference)) } } diff --git a/app/src/test/java/com/github/se/travelpouch/model/events/EventRepositoryUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/events/EventRepositoryUnitTest.kt index bcfb8fd7..ac74e241 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/events/EventRepositoryUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/events/EventRepositoryUnitTest.kt @@ -1,6 +1,5 @@ package com.github.se.travelpouch.model.events -import android.os.Looper import androidx.test.core.app.ApplicationProvider import com.google.android.gms.tasks.Tasks import com.google.firebase.FirebaseApp @@ -22,10 +21,11 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.timeout import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf @RunWith(RobolectricTestRunner::class) class EventRepositoryUnitTest { @@ -34,18 +34,15 @@ class EventRepositoryUnitTest { @Mock private lateinit var mockCollectionReference: CollectionReference @Mock private lateinit var mockDocumentSnapshot: DocumentSnapshot @Mock private lateinit var mockToDoQuerySnapshot: QuerySnapshot + @Mock private lateinit var mockTravelDocumentCollectionReference: CollectionReference + @Mock private lateinit var mockTravelDocumentReference: DocumentReference + @Mock private lateinit var mockEventCollectionReference: CollectionReference + @Mock private lateinit var mockEventDocumentSnapshot: DocumentSnapshot + @Mock private lateinit var mockEventDocumentReference: DocumentReference private lateinit var eventRepositoryFirestore: EventRepositoryFirebase - val event = - Event( - "1", - EventType.NEW_DOCUMENT, - Timestamp(0, 0), - "eventTitle", - "eventDescription", - null, - null) + val event = Event("1", EventType.NEW_ACTIVITY, Timestamp(0, 0), "eventTitle", "eventDescription") @Before fun setUp() { @@ -60,16 +57,10 @@ class EventRepositoryUnitTest { mockDocumentSnapshot = mock(DocumentSnapshot::class.java) `when`(mockDocumentSnapshot.id).thenReturn("1") - `when`(mockDocumentSnapshot.getString("eventType")).thenReturn("NEW_DOCUMENT") + `when`(mockDocumentSnapshot.getString("eventType")).thenReturn("NEW_ACTIVITY") `when`(mockDocumentSnapshot.getString("title")).thenReturn("eventTitle") `when`(mockDocumentSnapshot.getString("description")).thenReturn("eventDescription") `when`(mockDocumentSnapshot.getTimestamp("date")).thenReturn(Timestamp(0, 0)) - `when`(mockDocumentSnapshot.get("uidParticipant")).thenReturn(null) - `when`(mockDocumentSnapshot.get("listUploadedDocuments")).thenReturn(null) - - `when`(mockFirestore.collection(any())).thenReturn(mockCollectionReference) - `when`(mockCollectionReference.document(any())).thenReturn(mockDocumentReference) - `when`(mockCollectionReference.document()).thenReturn(mockDocumentReference) } @Test @@ -81,13 +72,27 @@ class EventRepositoryUnitTest { @Test fun getNewUid() { - `when`(mockDocumentReference.id).thenReturn("1") - val uid = eventRepositoryFirestore.getNewUid() - assert(uid == "1") + whenever(mockFirestore.collection(anyOrNull())) + .thenReturn(mockTravelDocumentCollectionReference) + whenever(mockTravelDocumentCollectionReference.document(anyOrNull())) + .thenReturn(mockTravelDocumentReference) + whenever(mockTravelDocumentReference.collection(anyOrNull())) + .thenReturn(mockEventCollectionReference) + whenever(mockEventCollectionReference.document()).thenReturn(mockEventDocumentReference) + whenever(mockEventCollectionReference.document(any())).thenReturn(mockEventDocumentReference) + whenever(mockEventDocumentReference.id).thenReturn("qwertz") + + val reference = eventRepositoryFirestore.getNewDocumentReferenceForNewTravel("qwertz") + assert(reference == mockEventDocumentReference) } @Test fun getEvents_callsDocuments() { + + `when`(mockFirestore.collection(any())).thenReturn(mockCollectionReference) + `when`(mockCollectionReference.document(any())).thenReturn(mockDocumentReference) + `when`(mockCollectionReference.document()).thenReturn(mockDocumentReference) + // Ensure that mockToDoQuerySnapshot is properly initialized and mocked `when`(mockCollectionReference.get()).thenReturn(Tasks.forResult(mockToDoQuerySnapshot)) @@ -106,20 +111,6 @@ class EventRepositoryUnitTest { verify(timeout(100)) { (mockToDoQuerySnapshot).documents } } - @Test - fun addEvent_shouldCallFirestoreCollection() { - `when`(mockDocumentReference.set(any())).thenReturn(Tasks.forResult(null)) // Simulate success - - // This test verifies that when we add a new event, the Firestore `collection()` method is - // called. - eventRepositoryFirestore.addEvent(event, onSuccess = {}, onFailure = {}) - - shadowOf(Looper.getMainLooper()).idle() - - // Ensure Firestore collection method was called to reference the "events" collection - verify(mockDocumentReference).set(any()) - } - @Test fun documentToEvent() { val privateFunc = @@ -131,4 +122,14 @@ class EventRepositoryUnitTest { val result = privateFunc.invoke(eventRepositoryFirestore, *parameters) assertThat(result, `is`(event)) } + + @Test + fun getNewDocumentReference() { + `when`(mockFirestore.collection(any())).thenReturn(mockCollectionReference) + `when`(mockCollectionReference.document(any())).thenReturn(mockDocumentReference) + `when`(mockCollectionReference.document()).thenReturn(mockDocumentReference) + + whenever(mockDocumentReference.id).thenReturn("uid") + assert(mockDocumentReference == eventRepositoryFirestore.getNewDocumentReference()) + } } diff --git a/app/src/test/java/com/github/se/travelpouch/model/travel/ListTravelViewModelUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/travel/ListTravelViewModelUnitTest.kt index 88caa622..f466e495 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/travel/ListTravelViewModelUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/travel/ListTravelViewModelUnitTest.kt @@ -1,6 +1,8 @@ package com.github.se.travelpouch.model.travel import android.util.Log +import com.github.se.travelpouch.model.events.EventRepository +import com.github.se.travelpouch.model.events.EventViewModel import com.github.se.travelpouch.model.travels.ListTravelViewModel import com.github.se.travelpouch.model.travels.Location import com.github.se.travelpouch.model.travels.Participant @@ -8,6 +10,7 @@ import com.github.se.travelpouch.model.travels.Role import com.github.se.travelpouch.model.travels.TravelContainer import com.github.se.travelpouch.model.travels.TravelRepository import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentReference import kotlinx.coroutines.ExperimentalCoroutinesApi import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat @@ -31,6 +34,9 @@ class ListTravelViewModelTest { private lateinit var travelRepository: TravelRepository private lateinit var listTravelViewModel: ListTravelViewModel + private lateinit var eventViewModel: EventViewModel + private lateinit var eventRepository: EventRepository + private val travel = TravelContainer( "6NU2zp2oGdA34s1Q1q5h", @@ -48,10 +54,16 @@ class ListTravelViewModelTest { mapOf(Participant("SGzOL8yn0JmAVaTdvG9v12345678") to Role.OWNER), emptyList()) + private lateinit var eventDocumentReference: DocumentReference + @Before fun setUp() { travelRepository = mock(TravelRepository::class.java) listTravelViewModel = ListTravelViewModel(travelRepository) + eventDocumentReference = mock() + + eventRepository = mock() + eventViewModel = EventViewModel(eventRepository) } @Test @@ -115,7 +127,7 @@ class ListTravelViewModelTest { null } .whenever(travelRepository) - .addTravel(anyOrNull(), anyOrNull(), anyOrNull()) + .addTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) doAnswer { invocation -> val onSuccess = invocation.getArgument(0) as (List) -> Unit @@ -125,7 +137,7 @@ class ListTravelViewModelTest { .whenever(travelRepository) .getTravels(anyOrNull(), anyOrNull()) - listTravelViewModel.addTravel(travel) + listTravelViewModel.addTravel(travel, eventDocumentReference) assertThat(listTravelViewModel.travels.value, `is`(travelList)) } @@ -138,9 +150,9 @@ class ListTravelViewModelTest { null } .whenever(travelRepository) - .addTravel(anyOrNull(), anyOrNull(), anyOrNull()) + .addTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) - listTravelViewModel.addTravel(travel) + listTravelViewModel.addTravel(travel, eventDocumentReference) assertThat(listTravelViewModel.travels.value, `is`(emptyList())) } @@ -154,7 +166,7 @@ class ListTravelViewModelTest { null } .whenever(travelRepository) - .updateTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + .updateTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) doAnswer { invocation -> val onSuccess = invocation.getArgument(0) as (List) -> Unit @@ -164,7 +176,7 @@ class ListTravelViewModelTest { .whenever(travelRepository) .getTravels(anyOrNull(), anyOrNull()) - listTravelViewModel.updateTravel(travel, TravelRepository.UpdateMode.FIELDS_UPDATE, null) + listTravelViewModel.updateTravel(travel, TravelRepository.UpdateMode.FIELDS_UPDATE, null, null) assertThat(listTravelViewModel.travels.value, `is`(travelList)) } @@ -178,9 +190,9 @@ class ListTravelViewModelTest { null } .whenever(travelRepository) - .updateTravel(anyOrNull(), anyOrNull(), any(), anyOrNull(), anyOrNull()) + .updateTravel(anyOrNull(), anyOrNull(), any(), anyOrNull(), anyOrNull(), anyOrNull()) - listTravelViewModel.updateTravel(travel, TravelRepository.UpdateMode.FIELDS_UPDATE, null) + listTravelViewModel.updateTravel(travel, TravelRepository.UpdateMode.FIELDS_UPDATE, null, null) assertThat(listTravelViewModel.travels.value, `is`(initialTravels)) } @@ -261,14 +273,14 @@ class ListTravelViewModelTest { null } .whenever(travelRepository) - .addTravel(anyOrNull(), anyOrNull(), anyOrNull()) + .addTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) mockStatic(Log::class.java).use { logMock: MockedStatic -> logMock.`when` { Log.e(anyString(), anyString(), any()) }.thenReturn(0) - listTravelViewModel.addTravel(travel) + listTravelViewModel.addTravel(travel, eventDocumentReference) - verify(travelRepository).addTravel(anyOrNull(), anyOrNull(), anyOrNull()) + verify(travelRepository).addTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) logMock.verify { Log.e("ListTravelViewModel", errorMessage, exception) } } } @@ -283,15 +295,17 @@ class ListTravelViewModelTest { null } .whenever(travelRepository) - .updateTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + .updateTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) mockStatic(Log::class.java).use { logMock: MockedStatic -> logMock.`when` { Log.e(anyString(), anyString(), any()) }.thenReturn(0) - listTravelViewModel.updateTravel(travel, TravelRepository.UpdateMode.FIELDS_UPDATE, null) + listTravelViewModel.updateTravel( + travel, TravelRepository.UpdateMode.FIELDS_UPDATE, null, null) verify(travelRepository) - .updateTravel(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) + .updateTravel( + anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) logMock.verify { Log.e("ListTravelViewModel", errorMessage, exception) } } } diff --git a/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryFirestoreUnitTest.kt b/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryFirestoreUnitTest.kt index f580a678..171d24fe 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryFirestoreUnitTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryFirestoreUnitTest.kt @@ -44,6 +44,7 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @@ -172,6 +173,7 @@ class TravelRepositoryFirestoreUnitTest { val travelDocumentMock: DocumentReference = mock() val profileDocumentMock: DocumentReference = mock() + val eventDocumentReference: DocumentReference = mock() val transaction: Transaction = mock() val task: Task = mock() @@ -185,6 +187,7 @@ class TravelRepositoryFirestoreUnitTest { whenever(travelCollectionMock.document(anyOrNull())).thenReturn(travelDocumentMock) whenever(profileCollectionMock.document(anyOrNull())).thenReturn(profileDocumentMock) + whenever(eventDocumentReference.id).thenReturn("qwertzuiopasdfghjkly") whenever(firestoreMock.runTransaction(anyOrNull())).thenReturn(task) whenever(task.isSuccessful).thenReturn(true) @@ -194,7 +197,8 @@ class TravelRepositoryFirestoreUnitTest { var succeeded = false var failed = false - travelRepository.addTravel(travel, { succeeded = true }, { failed = true }) + travelRepository.addTravel( + travel, { succeeded = true }, { failed = true }, eventDocumentReference) val profileDocumentSnapShot: DocumentSnapshot = mock() whenever(transaction.get(anyOrNull())).thenReturn(profileDocumentSnapShot) @@ -206,7 +210,7 @@ class TravelRepositoryFirestoreUnitTest { verify(firestoreMock).runTransaction(transactionCaptor.capture()) transactionCaptor.firstValue.apply(transaction) - verify(transaction).set(anyOrNull(), anyOrNull()) + verify(transaction, times(2)).set(anyOrNull(), anyOrNull()) verify(transaction).update(anyOrNull(), eq("userTravelList"), anyOrNull()) verify(transaction).get(anyOrNull()) @@ -228,6 +232,7 @@ class TravelRepositoryFirestoreUnitTest { val travelDocumentMock: DocumentReference = mock() val profileDocumentMock: DocumentReference = mock() + val eventDocumentReference: DocumentReference = mock() val transaction: Transaction = mock() val task: Task = mock() @@ -240,6 +245,7 @@ class TravelRepositoryFirestoreUnitTest { whenever(travelCollectionMock.document(anyOrNull())).thenReturn(travelDocumentMock) whenever(profileCollectionMock.document(anyOrNull())).thenReturn(profileDocumentMock) + whenever(eventDocumentReference.id).thenReturn("qwertzuiopasdfghjkly") whenever(firestoreMock.runTransaction(anyOrNull())).thenReturn(task) whenever(task.isSuccessful).thenReturn(false) @@ -250,7 +256,8 @@ class TravelRepositoryFirestoreUnitTest { var succeeded = false var failed = false - travelRepository.addTravel(travel, { succeeded = true }, { failed = true }) + travelRepository.addTravel( + travel, { succeeded = true }, { failed = true }, eventDocumentReference) val onFailureListenerCaptor = argumentCaptor() verify(task).addOnFailureListener(onFailureListenerCaptor.capture()) @@ -273,7 +280,8 @@ class TravelRepositoryFirestoreUnitTest { TravelRepository.UpdateMode.FIELDS_UPDATE, null, { successCalled = true }, - { fail("Should not call onFailure") }) + { fail("Should not call onFailure") }, + null) // Simulate task completion val onCompleteListenerCaptor = argumentCaptor>() @@ -305,6 +313,9 @@ class TravelRepositoryFirestoreUnitTest { val travelDocumentReference: DocumentReference = mock() val profileDocumentReference: DocumentReference = mock() + val eventDocumentReference: DocumentReference = mock() + + whenever(eventDocumentReference.id).thenReturn("qwertzuiopasdfghjklb") whenever(firestoreMock.collection(eq(FirebasePaths.TravelsSuperCollection))) .thenReturn(travelCollectionReference) @@ -341,7 +352,8 @@ class TravelRepositoryFirestoreUnitTest { TravelRepository.UpdateMode.ADD_PARTICIPANT, friendProfile.fsUid, { succeeded = true }, - { failed = true }) + { failed = true }, + eventDocumentReference = eventDocumentReference) val onSuccessListenerCaptor = argumentCaptor>() verify(task).addOnSuccessListener(onSuccessListenerCaptor.capture()) @@ -352,7 +364,7 @@ class TravelRepositoryFirestoreUnitTest { transactionCaptor.firstValue.apply(transactionMock) verify(transactionMock).update(anyOrNull(), eq("userTravelList"), anyOrNull()) - verify(transactionMock).set(anyOrNull(), anyOrNull()) + verify(transactionMock, times(2)).set(anyOrNull(), anyOrNull()) verify(transactionMock).get(anyOrNull()) assertTrue(succeeded) @@ -363,14 +375,41 @@ class TravelRepositoryFirestoreUnitTest { fun updatesRemovingAUserSuccessfully() { val task: Task = mock() + val friendProfile = + Profile( + "qwertzuiopasdfghjklyxcvbnm16", + "usernameFriend", + "email@friend.ch", + emptyMap(), + "nameFriend", + emptyList()) + + val mockDocumentSnapshotFriend: DocumentSnapshot = mock() + + `when`(mockDocumentSnapshotFriend.id).thenReturn(friendProfile.fsUid) + `when`(mockDocumentSnapshotFriend.getString("email")).thenReturn(friendProfile.email) + `when`(mockDocumentSnapshotFriend.getString("name")).thenReturn(friendProfile.name) + `when`(mockDocumentSnapshotFriend.getString("username")).thenReturn(friendProfile.username) + `when`(mockDocumentSnapshotFriend.get("userTravelList")) + .thenReturn(friendProfile.userTravelList) + `when`(mockDocumentSnapshotFriend.get("friends")).thenReturn(friendProfile.friends) + val firestoreMock: FirebaseFirestore = mock() val travelRepository: TravelRepository = TravelRepositoryFirestore(firestoreMock) + val transactionTask: Task = mock() + val transaction: Transaction = mock() + + whenever(transaction.set(any(), any())).thenReturn(transaction) + whenever(transaction.update(any(), eq("userTravelList"), any())).thenReturn(transaction) + whenever(transaction.get(any())).thenReturn(mockDocumentSnapshotFriend) val travelCollectionReference: CollectionReference = mock() val profileCollectionReference: CollectionReference = mock() val travelDocumentReference: DocumentReference = mock() val profileDocumentReference: DocumentReference = mock() + val eventDocumentReference: DocumentReference = mock() + whenever(eventDocumentReference.id).thenReturn("qwertzuioplkjhgfdsax") whenever(firestoreMock.collection(eq(FirebasePaths.TravelsSuperCollection))) .thenReturn(travelCollectionReference) @@ -393,12 +432,21 @@ class TravelRepositoryFirestoreUnitTest { TravelRepository.UpdateMode.REMOVE_PARTICIPANT, "user", { succeeded = true }, - { failed = true }) + { failed = true }, + eventDocumentReference) val onSuccessListenerCaptor = argumentCaptor>() verify(task).addOnSuccessListener(onSuccessListenerCaptor.capture()) onSuccessListenerCaptor.firstValue.onSuccess(task.result) + val transactionCaptor = argumentCaptor>() + verify(firestoreMock).runTransaction(transactionCaptor.capture()) + transactionCaptor.firstValue.apply(transaction) + + verify(transaction, times(2)).set(any(), any()) + verify(transaction).update(any(), eq("userTravelList"), any()) + verify(transaction).get(any()) + assertTrue(succeeded) assertFalse(failed) } @@ -417,7 +465,8 @@ class TravelRepositoryFirestoreUnitTest { TravelRepository.UpdateMode.FIELDS_UPDATE, null, { fail("Should not call onSuccess") }, - { failureCalled = true }) + { failureCalled = true }, + null) // Simulate task completion val onCompleteListenerCaptor = argumentCaptor>() diff --git a/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryMockTest.kt b/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryMockTest.kt index ae3e234d..04784e7e 100644 --- a/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryMockTest.kt +++ b/app/src/test/java/com/github/se/travelpouch/model/travel/TravelRepositoryMockTest.kt @@ -9,8 +9,10 @@ import com.github.se.travelpouch.model.travels.TravelContainerMock import com.github.se.travelpouch.model.travels.TravelRepository import com.github.se.travelpouch.model.travels.TravelRepositoryMock import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentReference import junit.framework.TestCase.assertFalse import org.junit.Test +import org.mockito.Mockito.mock class TravelRepositoryMockTest { @@ -39,6 +41,7 @@ class TravelRepositoryMockTest { emptyList()) val travelMockRepository = TravelRepositoryMock() + val eventDocumentReference: DocumentReference = mock() @Test fun verifiesThatAddingWorks() { @@ -47,7 +50,8 @@ class TravelRepositoryMockTest { val noTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] assert(noTravel == null) - travelMockRepository.addTravel(travel, { succeeded = true }, { failed = true }) + travelMockRepository.addTravel( + travel, { succeeded = true }, { failed = true }, eventDocumentReference) assert(succeeded) assertFalse(failed) @@ -64,7 +68,7 @@ class TravelRepositoryMockTest { val noTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] assert(noTravel == null) - travelMockRepository.addTravel(travel, {}, {}) + travelMockRepository.addTravel(travel, {}, {}, eventDocumentReference) val travelAdded = com.github.se.travelpouch.di.travelCollection[travel.fsUid] assert(travelAdded == travel) travelMockRepository.updateTravel( @@ -72,7 +76,8 @@ class TravelRepositoryMockTest { TravelRepository.UpdateMode.FIELDS_UPDATE, null, { succeeded = true }, - { failed = true }) + { failed = true }, + null) assert(succeeded) assertFalse(failed) @@ -89,7 +94,7 @@ class TravelRepositoryMockTest { val noTravel = com.github.se.travelpouch.di.travelCollection[travel.fsUid] assert(noTravel == null) - travelMockRepository.addTravel(travel, {}, {}) + travelMockRepository.addTravel(travel, {}, {}, eventDocumentReference) val travelAdded = com.github.se.travelpouch.di.travelCollection[travel.fsUid] assert(travelAdded == travel) travelMockRepository.deleteTravelById(travel.fsUid, { succeeded = true }, { failed = true })