diff --git a/app/build.gradle.kts b/app/build.gradle.kts index af39bd6ba..2157bd233 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -221,8 +221,8 @@ dependencies { /// Mockito for android testing androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.mockito.android) androidTestImplementation(libs.mockito.kotlin) + androidTestImplementation(libs.dexmaker.mockito.inline) // Mockito for unit testing testImplementation(libs.mockito.kotlin) diff --git a/app/src/androidTest/java/com/android/periodpals/ui/alert/AlertScreenTest.kt b/app/src/androidTest/java/com/android/periodpals/ui/alert/AlertScreenTest.kt index c4a1aa710..f9728a36c 100644 --- a/app/src/androidTest/java/com/android/periodpals/ui/alert/AlertScreenTest.kt +++ b/app/src/androidTest/java/com/android/periodpals/ui/alert/AlertScreenTest.kt @@ -1,32 +1,54 @@ package com.android.periodpals.ui.alert -import androidx.compose.material3.MaterialTheme import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput -import androidx.navigation.compose.rememberNavController +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.periodpals.ui.navigation.NavigationActions +import com.android.periodpals.ui.navigation.Route +import com.android.periodpals.ui.navigation.Screen +import com.android.periodpals.ui.navigation.TopLevelDestination +import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +@RunWith(AndroidJUnit4::class) class AlertScreenTest { + private lateinit var navigationActions: NavigationActions @get:Rule val composeTestRule = createComposeRule() + @Before + fun setUp() { + navigationActions = mock(NavigationActions::class.java) + + `when`(navigationActions.currentRoute()).thenReturn(Route.ALERT) + + composeTestRule.setContent { AlertScreen(navigationActions) } + } + @Test - fun displayAllComponents() { - composeTestRule.setContent { - MaterialTheme { AlertScreen(NavigationActions(rememberNavController())) } - } + fun allComponentsAreDisplayed() { composeTestRule.onNodeWithTag("alertInstruction").assertIsDisplayed() composeTestRule.onNodeWithTag("alertProduct").assertIsDisplayed() composeTestRule.onNodeWithTag("alertUrgency").assertIsDisplayed() composeTestRule.onNodeWithTag("alertLocation").assertIsDisplayed() composeTestRule.onNodeWithTag("alertMessage").assertIsDisplayed() + composeTestRule.onNodeWithTag("bottomNavigationMenu").assertIsDisplayed() + composeTestRule.onNodeWithTag("topBar").assertIsDisplayed() + composeTestRule.onNodeWithTag("goBackButton").assertIsNotDisplayed() composeTestRule .onNodeWithTag("alertSubmit") .assertIsDisplayed() @@ -34,22 +56,70 @@ class AlertScreenTest { } @Test - fun interactWithComponents() { - composeTestRule.setContent { - MaterialTheme { AlertScreen(NavigationActions(rememberNavController())) } - } + fun createValidAlert() { composeTestRule.onNodeWithTag("alertProduct").performClick() - composeTestRule.onNodeWithTag("Pads").performClick() - // composeTestRule.onNodeWithTag("alertProduct").assertTextEquals("Pads") + composeTestRule.onNodeWithText("Pads").performClick() + composeTestRule.onNodeWithTag("alertUrgency").performClick() - composeTestRule.onNodeWithTag("!! Medium").performClick() - // composeTestRule.onNodeWithTag("alertUrgency").assertTextEquals("!! Medium") + composeTestRule.onNodeWithText("!! Medium").performClick() composeTestRule.onNodeWithTag("alertLocation").performTextInput("Rolex") composeTestRule.onNodeWithTag("alertMessage").performTextInput("I need help finding a tampon") - // Cannot test navigation actions - // composeTestRule.onNodeWithTag("alertSubmit").performClick() + composeTestRule.onNodeWithTag("alertSubmit").performClick() + verify(navigationActions).navigateTo(Screen.ALERT_LIST) + } + + @Test + fun createInvalidAlertNoProduct() { + + composeTestRule.onNodeWithTag("alertUrgency").performClick() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText("!! Medium").performClick() + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithTag("alertLocation").performTextInput("Rolex") + composeTestRule.onNodeWithTag("alertMessage").performTextInput("I need help finding a tampon") + + composeTestRule.onNodeWithTag("alertSubmit").performClick() + verify(navigationActions, never()).navigateTo(any()) + verify(navigationActions, never()).navigateTo(any()) + } + + @Test + fun createInvalidAlertNoUrgencyLevel() { + + composeTestRule.onNodeWithTag("alertProduct").performClick() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText("Pads").performClick() + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithTag("alertLocation").performTextInput("Rolex") + composeTestRule.onNodeWithTag("alertMessage").performTextInput("I need help finding a tampon") + + composeTestRule.onNodeWithTag("alertSubmit").performClick() + verify(navigationActions, never()).navigateTo(any()) + verify(navigationActions, never()).navigateTo(any()) + } + + @Test + fun createInvalidAlertNoLocation() { + + composeTestRule.onNodeWithTag("alertProduct").performClick() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText("Pads").performClick() + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithTag("alertUrgency").performClick() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText("!! Medium").performClick() + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithTag("alertMessage").performTextInput("I need help finding a tampon") + + composeTestRule.onNodeWithTag("alertSubmit").performClick() + verify(navigationActions, never()).navigateTo(any()) + verify(navigationActions, never()).navigateTo(any()) } } diff --git a/app/src/main/java/com/android/periodpals/ui/alert/AlertScreen.kt b/app/src/main/java/com/android/periodpals/ui/alert/AlertScreen.kt index a4c9bdf84..90e9a0589 100644 --- a/app/src/main/java/com/android/periodpals/ui/alert/AlertScreen.kt +++ b/app/src/main/java/com/android/periodpals/ui/alert/AlertScreen.kt @@ -1,5 +1,6 @@ package com.android.periodpals.ui.alert +import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -24,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -35,74 +37,102 @@ import com.android.periodpals.ui.navigation.TopAppBar @Composable fun AlertScreen(navigationActions: NavigationActions) { + val context = LocalContext.current var location by remember { mutableStateOf("") } var message by remember { mutableStateOf("") } + val (productIsSelected, setProductIsSelected) = remember { mutableStateOf(false) } + val (urgencyIsSelected, setUrgencyIsSelected) = remember { mutableStateOf(false) } - // TODO("TOP APP BAR and BOTTOM NAVIGATION") Scaffold( modifier = Modifier.testTag("alertScreen"), bottomBar = { BottomNavigationMenu( onTabSelect = { route -> navigationActions.navigateTo(route) }, tabList = LIST_TOP_LEVEL_DESTINATION, - selectedItem = navigationActions.currentRoute()) - }, - topBar = { - TopAppBar( - title = "Create Alert", + selectedItem = navigationActions.currentRoute(), ) }, + topBar = { TopAppBar(title = "Create Alert") }, content = { paddingValues -> Column( modifier = Modifier.fillMaxSize().padding(30.dp).padding(paddingValues), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly) { - // Text Instruction - Text( - "Push a notification to users near you! If they are available and have the products you need, they'll be able to help you!", - modifier = Modifier.testTag("alertInstruction"), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleSmall) + verticalArrangement = Arrangement.SpaceEvenly, + ) { + // Text Instruction + Text( + "Push a notification to users near you! If they are available and have the products you need, they'll be able to help you!", + modifier = Modifier.testTag("alertInstruction"), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleSmall, + ) - // Product - ExposedDropdownMenuSample( - listOf("Tampons", "Pads", "No Preference"), "Product Needed", "alertProduct") + // Product + ExposedDropdownMenuSample( + listOf("Tampons", "Pads", "No Preference"), + "Product Needed", + "alertProduct", + setProductIsSelected, + ) - // Urgency - ExposedDropdownMenuSample( - listOf("!!! High", "!! Medium", "! Low"), "Urgency level", "alertUrgency") + // Urgency + ExposedDropdownMenuSample( + listOf("!!! High", "!! Medium", "! Low"), + "Urgency level", + "alertUrgency", + setUrgencyIsSelected, + ) - // Location - OutlinedTextField( - value = location, - onValueChange = { location = it }, - label = { Text("Location") }, - placeholder = { Text("Enter your location") }, - modifier = Modifier.fillMaxWidth().testTag("alertLocation")) + // Location + OutlinedTextField( + value = location, + onValueChange = { location = it }, + label = { Text("Location") }, + placeholder = { Text("Enter your location") }, + modifier = Modifier.fillMaxWidth().testTag("alertLocation"), + ) - // Message - OutlinedTextField( - value = message, - onValueChange = { message = it }, - label = { Text("Message") }, - placeholder = { Text("Write a message for the other users") }, - modifier = Modifier.fillMaxWidth().height(150.dp).testTag("alertMessage")) + // Message + OutlinedTextField( + value = message, + onValueChange = { message = it }, + label = { Text("Message") }, + placeholder = { Text("Write a message for the other users") }, + modifier = Modifier.fillMaxWidth().height(150.dp).testTag("alertMessage"), + ) - // Submit Button - Button( - onClick = { navigationActions.navigateTo(Screen.ALERT_LIST) }, - modifier = - Modifier.width(300.dp).height(100.dp).testTag("alertSubmit").padding(16.dp), - ) { - Text("Ask for Help", style = MaterialTheme.typography.headlineMedium) - } - } - }) + // Submit Button + Button( + onClick = { + if (location.isEmpty()) { + Toast.makeText(context, "Please enter a location", Toast.LENGTH_SHORT).show() + } else if (!productIsSelected) { + Toast.makeText(context, "Please select a product", Toast.LENGTH_SHORT).show() + } else if (!urgencyIsSelected) { + Toast.makeText(context, "Please select an urgency level", Toast.LENGTH_SHORT) + .show() + } else { + navigationActions.navigateTo(Screen.ALERT_LIST) + } + }, + modifier = + Modifier.width(300.dp).height(100.dp).testTag("alertSubmit").padding(16.dp), + ) { + Text("Ask for Help", style = MaterialTheme.typography.headlineMedium) + } + } + }, + ) } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ExposedDropdownMenuSample(list: List, label: String, testTag: String) { +fun ExposedDropdownMenuSample( + list: List, + label: String, + testTag: String, + setIsSelected: (Boolean) -> Unit, +) { var options = list var expanded by remember { mutableStateOf(false) } var text by remember { mutableStateOf("Please choose one option") } @@ -116,23 +146,21 @@ fun ExposedDropdownMenuSample(list: List, label: String, testTag: String modifier = Modifier.menuAnchor(), value = text, onValueChange = {}, - readOnly = true, singleLine = true, + readOnly = true, label = { Text(label) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.textFieldColors(), ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { options.forEach { option -> DropdownMenuItem( - modifier = Modifier.testTag(option), + modifier = Modifier, text = { Text(option) }, onClick = { text = option expanded = false + setIsSelected(true) }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index edbc4f9dd..4f9445c9e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ agp = "8.6.1" androidxNavigationCompose = "2.5.3" bomVersion = "3.0.0" compose = "1.0.0-beta01" +dexmakerMockitoInline = "2.28.4" imagepicker = "2.1" json = "20240303" kotlin = "2.0.0" @@ -101,6 +102,7 @@ androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "ui" } compose = { module = "com.github.bumptech.glide:compose", version.ref = "compose" } core-ktx = { module = "com.google.android.play:core-ktx", version.ref = "coreKtxVersion" } +dexmaker-mockito-inline = { module = "com.linkedin.dexmaker:dexmaker-mockito-inline", version.ref = "dexmakerMockitoInline" } github-postgrest-kt = { module = "io.github.jan-tennert.supabase:postgrest-kt" } imagepicker = { module = "com.github.dhaval2404:imagepicker", version.ref = "imagepicker" } json = { module = "org.json:json", version.ref = "json" }