Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +1 −1 Original line number Diff line number Diff line Loading @@ -612,8 +612,8 @@ private fun PlaceCardRoute( val placeJson = backStackEntry.arguments?.getString("place") val place = placeJson?.let { Gson().fromJson(it, Place::class.java) } place?.let { place -> viewModel.setPlace(place) LaunchedEffect(place) { viewModel.setPlace(place) // Clear any existing pins and add the new one to ensure only one pin is shown at a time state.mapPins.clear() state.mapPins.add(place) Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/PlaceCardScreen.kt +24 −5 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier Loading @@ -59,6 +60,7 @@ import earth.maps.cardinal.R.string import earth.maps.cardinal.data.AddressFormatter import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.format import kotlinx.coroutines.launch @Composable fun PlaceCardScreen( Loading Loading @@ -104,7 +106,12 @@ fun PlaceCardScreen( ) { PlaceHeader(displayedPlace) PlaceAddress(displayedPlace, addressFormatter) PlaceActions(displayedPlace, viewModel, place, onGetDirections) { showUnsaveConfirmationDialog = true } PlaceActions( displayedPlace, viewModel, place, onGetDirections ) { showUnsaveConfirmationDialog = true } // Inset horizontal divider HorizontalDivider( modifier = Modifier Loading @@ -121,7 +128,11 @@ fun PlaceCardScreen( }, onRouteClicked = {}) } UnsaveConfirmationDialog(displayedPlace, viewModel, showUnsaveConfirmationDialog) { showUnsaveConfirmationDialog = false } UnsaveConfirmationDialog( displayedPlace, viewModel, showUnsaveConfirmationDialog ) { showUnsaveConfirmationDialog = false } } } Loading Loading @@ -186,6 +197,8 @@ private fun PlaceActions( Text(stringResource(string.get_directions)) } val coroutineScope = rememberCoroutineScope() // Save/Unsave button Button( onClick = { Loading @@ -193,8 +206,10 @@ private fun PlaceActions( // Show confirmation dialog for unsaving onShowUnsaveDialog() } else { coroutineScope.launch { viewModel.savePlace(place) } } }, modifier = Modifier.padding(start = dimensionResource(dimen.padding_minor)) ) { Row( Loading Loading @@ -228,6 +243,8 @@ private fun UnsaveConfirmationDialog( show: Boolean, onDismiss: () -> Unit ) { val coroutineScope = rememberCoroutineScope() if (show) { AlertDialog( onDismissRequest = onDismiss, Loading @@ -242,8 +259,10 @@ private fun UnsaveConfirmationDialog( confirmButton = { TextButton( onClick = { coroutineScope.launch { viewModel.unsavePlace(displayedPlace) onDismiss() } }) { Text(stringResource(string.unsave_place)) } Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/PlaceCardViewModel.kt +12 −20 Original line number Diff line number Diff line Loading @@ -20,11 +20,9 @@ package earth.maps.cardinal.ui.place import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.room.SavedPlaceRepository import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel Loading @@ -35,33 +33,27 @@ class PlaceCardViewModel @Inject constructor( val isPlaceSaved = mutableStateOf(false) val place = mutableStateOf<Place?>(null) fun setPlace(place: Place) { suspend fun setPlace(place: Place) { this.place.value = place checkIfPlaceIsSaved(place) } fun checkIfPlaceIsSaved(place: Place) { viewModelScope.launch { suspend fun checkIfPlaceIsSaved(place: Place) { if (place.id != null) { val existingPlace = savedPlaceRepository.getPlaceById(place.id).getOrNull() isPlaceSaved.value = existingPlace != null } } } fun savePlace(place: Place) { viewModelScope.launch { suspend fun savePlace(place: Place) { savedPlaceRepository.savePlace(place) isPlaceSaved.value = true } } fun unsavePlace(place: Place) { viewModelScope.launch { suspend fun unsavePlace(place: Place) { place.id?.let { id -> savedPlaceRepository.deletePlace(placeId = id) } isPlaceSaved.value = false } } } cardinal-android/app/src/test/java/earth/maps/cardinal/ui/place/PlaceCardViewModelTest.kt 0 → 100644 +148 −0 Original line number Diff line number Diff line package earth.maps.cardinal.ui.place import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.room.SavedPlace import earth.maps.cardinal.data.room.SavedPlaceRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class PlaceCardViewModelTest { private lateinit var viewModel: PlaceCardViewModel private val mockSavedPlaceRepository = mockk<SavedPlaceRepository>() @Before fun setup() { viewModel = PlaceCardViewModel( savedPlaceRepository = mockSavedPlaceRepository ) } @Test fun `setPlace should update place and check if place is saved`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) val savedPlace = SavedPlace.fromPlace(testPlace) coEvery { mockSavedPlaceRepository.getPlaceById("1") } returns Result.success(savedPlace) viewModel.setPlace(testPlace) assertThat(viewModel.place.value).isEqualTo(testPlace) assertThat(viewModel.isPlaceSaved.value).isTrue() coVerify { mockSavedPlaceRepository.getPlaceById("1") } } @Test fun `checkIfPlaceIsSaved should set isPlaceSaved to true when place exists`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) val savedPlace = SavedPlace.fromPlace(testPlace) coEvery { mockSavedPlaceRepository.getPlaceById("1") } returns Result.success(savedPlace) viewModel.checkIfPlaceIsSaved(testPlace) assertThat(viewModel.isPlaceSaved.value).isTrue() } @Test fun `checkIfPlaceIsSaved should set isPlaceSaved to false when place does not exist`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) coEvery { mockSavedPlaceRepository.getPlaceById("1") } returns Result.success(null) viewModel.checkIfPlaceIsSaved(testPlace) assertThat(viewModel.isPlaceSaved.value).isFalse() } @Test fun `checkIfPlaceIsSaved should not check when place id is null`() = runTest { val testPlace = Place( id = null, name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) viewModel.checkIfPlaceIsSaved(testPlace) assertThat(viewModel.isPlaceSaved.value).isFalse() coVerify(exactly = 0) { mockSavedPlaceRepository.getPlaceById(any()) } } @Test fun `savePlace should save place and set isPlaceSaved to true`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) coEvery { mockSavedPlaceRepository.savePlace(testPlace) } returns Result.success("1") viewModel.savePlace(testPlace) coVerify { mockSavedPlaceRepository.savePlace(testPlace) } assertThat(viewModel.isPlaceSaved.value).isTrue() } @Test fun `unsavePlace should delete place and set isPlaceSaved to false`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) coEvery { mockSavedPlaceRepository.deletePlace("1") } returns Result.success(Unit) viewModel.unsavePlace(testPlace) coVerify { mockSavedPlaceRepository.deletePlace(placeId = "1") } assertThat(viewModel.isPlaceSaved.value).isFalse() } @Test fun `unsavePlace should not delete when place id is null`() = runTest { val testPlace = Place( id = null, name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) viewModel.unsavePlace(testPlace) coVerify(exactly = 0) { mockSavedPlaceRepository.deletePlace(any()) } assertThat(viewModel.isPlaceSaved.value).isFalse() } } cardinal-android/gradle/libs.versions.toml +1 −1 Original line number Diff line number Diff line Loading @@ -30,7 +30,7 @@ ferrostar = "0.41.0" okhttp3 = "5.1.0" material3 = "1.5.0-alpha04" detekt = "2.0.0-alpha.0" mockk = "1.13.16" mockk = "1.14.6" kotlinxCoroutinesTest = "1.10.2" hiltAndroidTesting = "2.57.1" Loading Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +1 −1 Original line number Diff line number Diff line Loading @@ -612,8 +612,8 @@ private fun PlaceCardRoute( val placeJson = backStackEntry.arguments?.getString("place") val place = placeJson?.let { Gson().fromJson(it, Place::class.java) } place?.let { place -> viewModel.setPlace(place) LaunchedEffect(place) { viewModel.setPlace(place) // Clear any existing pins and add the new one to ensure only one pin is shown at a time state.mapPins.clear() state.mapPins.add(place) Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/PlaceCardScreen.kt +24 −5 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier Loading @@ -59,6 +60,7 @@ import earth.maps.cardinal.R.string import earth.maps.cardinal.data.AddressFormatter import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.format import kotlinx.coroutines.launch @Composable fun PlaceCardScreen( Loading Loading @@ -104,7 +106,12 @@ fun PlaceCardScreen( ) { PlaceHeader(displayedPlace) PlaceAddress(displayedPlace, addressFormatter) PlaceActions(displayedPlace, viewModel, place, onGetDirections) { showUnsaveConfirmationDialog = true } PlaceActions( displayedPlace, viewModel, place, onGetDirections ) { showUnsaveConfirmationDialog = true } // Inset horizontal divider HorizontalDivider( modifier = Modifier Loading @@ -121,7 +128,11 @@ fun PlaceCardScreen( }, onRouteClicked = {}) } UnsaveConfirmationDialog(displayedPlace, viewModel, showUnsaveConfirmationDialog) { showUnsaveConfirmationDialog = false } UnsaveConfirmationDialog( displayedPlace, viewModel, showUnsaveConfirmationDialog ) { showUnsaveConfirmationDialog = false } } } Loading Loading @@ -186,6 +197,8 @@ private fun PlaceActions( Text(stringResource(string.get_directions)) } val coroutineScope = rememberCoroutineScope() // Save/Unsave button Button( onClick = { Loading @@ -193,8 +206,10 @@ private fun PlaceActions( // Show confirmation dialog for unsaving onShowUnsaveDialog() } else { coroutineScope.launch { viewModel.savePlace(place) } } }, modifier = Modifier.padding(start = dimensionResource(dimen.padding_minor)) ) { Row( Loading Loading @@ -228,6 +243,8 @@ private fun UnsaveConfirmationDialog( show: Boolean, onDismiss: () -> Unit ) { val coroutineScope = rememberCoroutineScope() if (show) { AlertDialog( onDismissRequest = onDismiss, Loading @@ -242,8 +259,10 @@ private fun UnsaveConfirmationDialog( confirmButton = { TextButton( onClick = { coroutineScope.launch { viewModel.unsavePlace(displayedPlace) onDismiss() } }) { Text(stringResource(string.unsave_place)) } Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/place/PlaceCardViewModel.kt +12 −20 Original line number Diff line number Diff line Loading @@ -20,11 +20,9 @@ package earth.maps.cardinal.ui.place import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.room.SavedPlaceRepository import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel Loading @@ -35,33 +33,27 @@ class PlaceCardViewModel @Inject constructor( val isPlaceSaved = mutableStateOf(false) val place = mutableStateOf<Place?>(null) fun setPlace(place: Place) { suspend fun setPlace(place: Place) { this.place.value = place checkIfPlaceIsSaved(place) } fun checkIfPlaceIsSaved(place: Place) { viewModelScope.launch { suspend fun checkIfPlaceIsSaved(place: Place) { if (place.id != null) { val existingPlace = savedPlaceRepository.getPlaceById(place.id).getOrNull() isPlaceSaved.value = existingPlace != null } } } fun savePlace(place: Place) { viewModelScope.launch { suspend fun savePlace(place: Place) { savedPlaceRepository.savePlace(place) isPlaceSaved.value = true } } fun unsavePlace(place: Place) { viewModelScope.launch { suspend fun unsavePlace(place: Place) { place.id?.let { id -> savedPlaceRepository.deletePlace(placeId = id) } isPlaceSaved.value = false } } }
cardinal-android/app/src/test/java/earth/maps/cardinal/ui/place/PlaceCardViewModelTest.kt 0 → 100644 +148 −0 Original line number Diff line number Diff line package earth.maps.cardinal.ui.place import earth.maps.cardinal.data.Place import earth.maps.cardinal.data.room.SavedPlace import earth.maps.cardinal.data.room.SavedPlaceRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class PlaceCardViewModelTest { private lateinit var viewModel: PlaceCardViewModel private val mockSavedPlaceRepository = mockk<SavedPlaceRepository>() @Before fun setup() { viewModel = PlaceCardViewModel( savedPlaceRepository = mockSavedPlaceRepository ) } @Test fun `setPlace should update place and check if place is saved`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) val savedPlace = SavedPlace.fromPlace(testPlace) coEvery { mockSavedPlaceRepository.getPlaceById("1") } returns Result.success(savedPlace) viewModel.setPlace(testPlace) assertThat(viewModel.place.value).isEqualTo(testPlace) assertThat(viewModel.isPlaceSaved.value).isTrue() coVerify { mockSavedPlaceRepository.getPlaceById("1") } } @Test fun `checkIfPlaceIsSaved should set isPlaceSaved to true when place exists`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) val savedPlace = SavedPlace.fromPlace(testPlace) coEvery { mockSavedPlaceRepository.getPlaceById("1") } returns Result.success(savedPlace) viewModel.checkIfPlaceIsSaved(testPlace) assertThat(viewModel.isPlaceSaved.value).isTrue() } @Test fun `checkIfPlaceIsSaved should set isPlaceSaved to false when place does not exist`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) coEvery { mockSavedPlaceRepository.getPlaceById("1") } returns Result.success(null) viewModel.checkIfPlaceIsSaved(testPlace) assertThat(viewModel.isPlaceSaved.value).isFalse() } @Test fun `checkIfPlaceIsSaved should not check when place id is null`() = runTest { val testPlace = Place( id = null, name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) viewModel.checkIfPlaceIsSaved(testPlace) assertThat(viewModel.isPlaceSaved.value).isFalse() coVerify(exactly = 0) { mockSavedPlaceRepository.getPlaceById(any()) } } @Test fun `savePlace should save place and set isPlaceSaved to true`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) coEvery { mockSavedPlaceRepository.savePlace(testPlace) } returns Result.success("1") viewModel.savePlace(testPlace) coVerify { mockSavedPlaceRepository.savePlace(testPlace) } assertThat(viewModel.isPlaceSaved.value).isTrue() } @Test fun `unsavePlace should delete place and set isPlaceSaved to false`() = runTest { val testPlace = Place( id = "1", name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) coEvery { mockSavedPlaceRepository.deletePlace("1") } returns Result.success(Unit) viewModel.unsavePlace(testPlace) coVerify { mockSavedPlaceRepository.deletePlace(placeId = "1") } assertThat(viewModel.isPlaceSaved.value).isFalse() } @Test fun `unsavePlace should not delete when place id is null`() = runTest { val testPlace = Place( id = null, name = "Test Place", latLng = earth.maps.cardinal.data.LatLng(0.0, 0.0), address = null ) viewModel.unsavePlace(testPlace) coVerify(exactly = 0) { mockSavedPlaceRepository.deletePlace(any()) } assertThat(viewModel.isPlaceSaved.value).isFalse() } }
cardinal-android/gradle/libs.versions.toml +1 −1 Original line number Diff line number Diff line Loading @@ -30,7 +30,7 @@ ferrostar = "0.41.0" okhttp3 = "5.1.0" material3 = "1.5.0-alpha04" detekt = "2.0.0-alpha.0" mockk = "1.13.16" mockk = "1.14.6" kotlinxCoroutinesTest = "1.10.2" hiltAndroidTesting = "2.57.1" Loading