From c3b731a0102c8fb82c4fa2a72bbaa4f4332d8360 Mon Sep 17 00:00:00 2001 From: Stefan <46031448+StefanPetersTM@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:21:28 +0400 Subject: [PATCH] Feature/calendar implement navigation to & from the screen (#108) * feat(calendar): add CalendarScreen Added a calendar and it's view for activity tracking * fix(firestore): CalendarScreen now updates The calendar now takes data from firestore and displays activities * fix(firestore): CalendarScreen now updates The calendar now takes data from firestore and displays activities * test: add UI tests * test: add UI tests * test(coverage): improve coverage * test(coverage): improve coverage * chore(refactor): separate composables in files * chore(refactor): make activities val again Made ActivityViewModel.activities back into a val, as opposed to a var * chore(refactor): refactor LaunchedEffect Moved the LaunchedEffect in the CalendarScreen to a separate file * chore(formatting): moved files to UI * feat(navigation): implement navigation * feat(navigation): implement navigation * tests(navigation): refactor tests for navigation * tests(navigation): refactor tests for navigation * tests(coverage): improve coverage --- app/build.gradle.kts | 1 + .../travelpouch/ui/home/CalendarScreenTest.kt | 38 +++++++++++- .../ui/home/TravelListScreenTest.kt | 61 +++++++++++++++++++ .../com/github/se/travelpouch/MainActivity.kt | 2 +- .../se/travelpouch/ui/dashboard/Calendar.kt | 38 ++++++++++-- .../se/travelpouch/ui/home/TravelList.kt | 31 ++++++++++ gradle/libs.versions.toml | 2 + 7 files changed, 166 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f644a37..46823954 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -163,6 +163,7 @@ dependencies { implementation(libs.firebase.common.ktx) implementation(libs.firebase.auth.ktx) implementation(libs.androidx.runtime.livedata) + implementation(libs.androidx.navigation.testing) testImplementation(libs.junit) globalTestImplementation(libs.androidx.junit) globalTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/home/CalendarScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/home/CalendarScreenTest.kt index 3d422660..3d336ff4 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/home/CalendarScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/home/CalendarScreenTest.kt @@ -10,6 +10,7 @@ import com.github.se.travelpouch.model.activity.ActivityRepository import com.github.se.travelpouch.model.activity.ActivityViewModel import com.github.se.travelpouch.model.dashboard.CalendarViewModel import com.github.se.travelpouch.ui.dashboard.CalendarScreen +import com.github.se.travelpouch.ui.navigation.NavigationActions import com.google.firebase.Timestamp import java.util.Calendar import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -28,10 +29,13 @@ class CalendarScreenTest { private lateinit var mockActivityRepositoryFirebase: ActivityRepository private lateinit var mockActivityViewModel: ActivityViewModel + private lateinit var navigationActions: NavigationActions + @get:Rule val composeTestRule = createComposeRule() @Before fun setUp() { + navigationActions = mock(NavigationActions::class.java) mockActivityRepositoryFirebase = mock(ActivityRepository::class.java) mockActivityViewModel = ActivityViewModel(mockActivityRepositoryFirebase) calendarViewModel = CalendarViewModel(activityViewModel = mockActivityViewModel) @@ -40,7 +44,9 @@ class CalendarScreenTest { @Test fun hasRequiredComponents() { // Act - composeTestRule.setContent { CalendarScreen(calendarViewModel = calendarViewModel) } + composeTestRule.setContent { + CalendarScreen(calendarViewModel = calendarViewModel, navigationActions) + } composeTestRule.waitForIdle() // Assert @@ -81,7 +87,9 @@ class CalendarScreenTest { mockActivityViewModel.getAllActivities() // Act - composeTestRule.setContent { CalendarScreen(calendarViewModel = calendarViewModel) } + composeTestRule.setContent { + CalendarScreen(calendarViewModel = calendarViewModel, navigationActions = navigationActions) + } composeTestRule.waitForIdle() // Assert activity for today is displayed @@ -93,4 +101,30 @@ class CalendarScreenTest { // Click on the back icon to test navigation composeTestRule.onNodeWithTag("goBackIcon").performClick() } + + @Test + fun testNavigationGoBack() { + // Act + composeTestRule.setContent { + CalendarScreen(calendarViewModel = calendarViewModel, navigationActions) + } + composeTestRule.waitForIdle() + + // Click on the back icon to test navigation + composeTestRule.onNodeWithTag("goBackIcon").performClick() + composeTestRule.waitForIdle() + } + + @Test + fun testNavigationBottomBar() { + // Act + composeTestRule.setContent { + CalendarScreen(calendarViewModel = calendarViewModel, navigationActions) + } + composeTestRule.waitForIdle() + + // Click on the Map icon to test navigation + composeTestRule.onNodeWithTag("navigationBarItemMap").performClick() + composeTestRule.waitForIdle() + } } diff --git a/app/src/androidTest/java/com/github/se/travelpouch/ui/home/TravelListScreenTest.kt b/app/src/androidTest/java/com/github/se/travelpouch/ui/home/TravelListScreenTest.kt index 5af8f66a..58892575 100644 --- a/app/src/androidTest/java/com/github/se/travelpouch/ui/home/TravelListScreenTest.kt +++ b/app/src/androidTest/java/com/github/se/travelpouch/ui/home/TravelListScreenTest.kt @@ -2,6 +2,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import com.github.se.travelpouch.model.ListTravelViewModel import com.github.se.travelpouch.model.Location import com.github.se.travelpouch.model.Participant @@ -12,6 +13,7 @@ import com.github.se.travelpouch.model.TravelRepository import com.github.se.travelpouch.ui.home.MapScreen import com.github.se.travelpouch.ui.home.TravelListScreen import com.github.se.travelpouch.ui.navigation.NavigationActions +import com.github.se.travelpouch.ui.navigation.Screen import com.google.firebase.Timestamp import java.util.Date import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,6 +21,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mockito.mock +import org.mockito.Mockito.verify import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.whenever @@ -146,4 +149,62 @@ class TravelListScreenTest { // Assert composeTestRule.onNodeWithTag("mapScreen").assertIsDisplayed() } + + @Test + fun testNavigationBottomBar() { + // Act + composeTestRule.setContent { + TravelListScreen( + navigationActions = navigationActions, listTravelViewModel = listTravelViewModel) + } + composeTestRule.waitForIdle() + + // Click on the "Activities" navigation item + composeTestRule.onNodeWithTag("navigationBarItemActivities").performClick() + composeTestRule.waitForIdle() + + // Verify that the navigation action was called for "Activities" + verify(navigationActions).navigateTo(Screen.TRAVEL_ACTIVITIES) + + // Click on the "Calendar" navigation item + composeTestRule.onNodeWithTag("navigationBarItemCalendar").performClick() + composeTestRule.waitForIdle() + + // Verify that the navigation action was called for "Calendar" + verify(navigationActions).navigateTo(Screen.CALENDAR) + } + + @Test + fun testTravelItemClickNavigatesToTravelActivities() { + // Act + composeTestRule.setContent { + TravelListScreen( + navigationActions = navigationActions, listTravelViewModel = listTravelViewModel) + } + composeTestRule.waitForIdle() + + // Find and click on a travel list item + composeTestRule.onNodeWithTag("travelListItem").performClick() + composeTestRule.waitForIdle() + + // Verify that the navigation action was called for TRAVEL_ACTIVITIES + verify(navigationActions).navigateTo(Screen.TRAVEL_ACTIVITIES) + } + + @Test + fun testCreateTravelFabClickNavigatesToAddTravel() { + // Act + composeTestRule.setContent { + TravelListScreen( + navigationActions = navigationActions, listTravelViewModel = listTravelViewModel) + } + composeTestRule.waitForIdle() + + // Find and click on the create travel FAB + composeTestRule.onNodeWithTag("createTravelFab").performClick() + composeTestRule.waitForIdle() + + // Verify that the navigation action was called for ADD_TRAVEL + verify(navigationActions).navigateTo(Screen.ADD_TRAVEL) + } } 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 2d5222a6..59906d42 100644 --- a/app/src/main/java/com/github/se/travelpouch/MainActivity.kt +++ b/app/src/main/java/com/github/se/travelpouch/MainActivity.kt @@ -94,7 +94,7 @@ fun TravelPouchApp() { } composable(Screen.DOCUMENT_PREVIEW) { DocumentPreview(documentViewModel, navigationActions) } composable(Screen.TIMELINE) { TimelineScreen(eventsViewModel) } - composable(Screen.CALENDAR) { CalendarScreen(calendarViewModel) } + composable(Screen.CALENDAR) { CalendarScreen(calendarViewModel, navigationActions) } } } } diff --git a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Calendar.kt b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Calendar.kt index bac8d68d..da0970d9 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Calendar.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/dashboard/Calendar.kt @@ -9,9 +9,13 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Place import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -24,6 +28,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import com.github.se.travelpouch.model.dashboard.CalendarViewModel +import com.github.se.travelpouch.ui.navigation.NavigationActions +import com.github.se.travelpouch.ui.navigation.Screen import java.text.SimpleDateFormat import java.util.Locale @@ -34,15 +40,19 @@ import java.util.Locale */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CalendarScreen( - calendarViewModel: CalendarViewModel, -) { +fun CalendarScreen(calendarViewModel: CalendarViewModel, navigationActions: NavigationActions) { // Observe the state of activities from the ViewModel val calendarState by calendarViewModel.calendarState.collectAsState(initial = emptyList()) // Initial Setup CalendarScreenLaunchedEffect(calendarViewModel = calendarViewModel) + // List of destinations for the bottom navigation bar + val listOfDestinations = + listOf( + BottomNavigationItem("Activities", Icons.Default.Home), + BottomNavigationItem("Map", Icons.Default.Place)) + // Application Scaffold( topBar = { @@ -52,7 +62,7 @@ fun CalendarScreen( navigationIcon = { IconButton( modifier = Modifier.testTag("goBackIcon"), - onClick = { /* TODO: Implement go back navigation logic */}) { + onClick = { navigationActions.goBack() }) { // Back icon for navigation Icon( Icons.AutoMirrored.Filled.ArrowBack, @@ -64,6 +74,26 @@ fun CalendarScreen( TopAppBarDefaults.topAppBarColors( containerColor = Color(0xFF6200EE), titleContentColor = Color.White), modifier = Modifier.testTag("calendarTopAppBar")) + }, + bottomBar = { + NavigationBar(modifier = Modifier.testTag("navigationBarCalendar")) { + listOfDestinations.forEach { destination -> + NavigationBarItem( + onClick = { + when (destination.title) { + "Activities" -> navigationActions.navigateTo(Screen.TRAVEL_ACTIVITIES) + "Map" -> navigationActions.navigateTo(Screen.TRAVEL_LIST) + } + }, + icon = { Icon(destination.icon, contentDescription = null) }, + selected = false, + label = { Text(destination.title) }, + modifier = + Modifier.testTag( + if (destination.title == "Map") "navigationBarItemMap" + else "navigationBarItem")) + } + } }) { innerPadding -> Column( modifier = Modifier.fillMaxSize().padding(innerPadding).testTag("calendarScreenColumn"), diff --git a/app/src/main/java/com/github/se/travelpouch/ui/home/TravelList.kt b/app/src/main/java/com/github/se/travelpouch/ui/home/TravelList.kt index 3b341496..0ab5aa6c 100644 --- a/app/src/main/java/com/github/se/travelpouch/ui/home/TravelList.kt +++ b/app/src/main/java/com/github/se/travelpouch/ui/home/TravelList.kt @@ -15,10 +15,14 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.Home import androidx.compose.material3.Card import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -32,6 +36,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.github.se.travelpouch.model.ListTravelViewModel import com.github.se.travelpouch.model.TravelContainer +import com.github.se.travelpouch.ui.dashboard.BottomNavigationItem import com.github.se.travelpouch.ui.navigation.NavigationActions import com.github.se.travelpouch.ui.navigation.Screen import java.util.Locale @@ -56,6 +61,11 @@ fun TravelListScreen( // travelContainers.getTravels() val travelList = listTravelViewModel.travels.collectAsState().value + val listOfDestinations = + listOf( + BottomNavigationItem("Activities", Icons.Default.Home), + BottomNavigationItem("Calendar", Icons.Default.DateRange)) + Scaffold( modifier = Modifier.testTag("TravelListScreen"), floatingActionButton = { @@ -65,6 +75,27 @@ fun TravelListScreen( Icon(imageVector = Icons.Default.Add, contentDescription = "Add") } }, + bottomBar = { + NavigationBar(modifier = Modifier.testTag("navigationBarTravelList")) { + listOfDestinations.forEach { destination -> + NavigationBarItem( + onClick = { + when (destination.title) { + "Activities" -> navigationActions.navigateTo(Screen.TRAVEL_ACTIVITIES) + "Calendar" -> navigationActions.navigateTo(Screen.CALENDAR) + } + }, + icon = { Icon(destination.icon, contentDescription = null) }, + selected = false, + label = { Text(destination.title) }, + modifier = + Modifier.testTag( + if (destination.title == "Activities") "navigationBarItemActivities" + else "navigationBarItemCalendar")) + // modifier = Modifier.testTag("navigationBarItem")) + } + } + }, content = { pd -> Column { // Add the map to display the travels diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b1c8955..41c3e6d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,6 +54,7 @@ robolectric = "4.11.1" sonar = "4.4.1.3373" uiTestJunit4 = "1.7.3" runtimeLivedata = "1.7.4" +navigationTesting = "2.8.3" [libraries] @@ -116,6 +117,7 @@ androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents" # Networking okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } +androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "navigationTesting" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }