summaryrefslogtreecommitdiff
path: root/android-app/app/src/test/kotlin/org
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/test/kotlin/org')
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/ui_orig/LocationPermissionHandlerTest.kt110
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/ui_orig/MainViewModelTest.kt105
2 files changed, 215 insertions, 0 deletions
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/LocationPermissionHandlerTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/LocationPermissionHandlerTest.kt
new file mode 100644
index 0000000..9caa5a0
--- /dev/null
+++ b/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/LocationPermissionHandlerTest.kt
@@ -0,0 +1,110 @@
+package org.terst.nav.ui
+
+import org.junit.Assert.*
+import org.junit.Test
+
+class LocationPermissionHandlerTest {
+
+ // Convenience factory — callers override only the lambdas they care about.
+ private fun makeHandler(
+ checkGranted: () -> Boolean = { false },
+ onGranted: () -> Unit = {},
+ onDenied: () -> Unit = {},
+ requestPermissions: () -> Unit = {}
+ ) = LocationPermissionHandler(checkGranted, onGranted, onDenied, requestPermissions)
+
+ // ── start() ──────────────────────────────────────────────────────────────
+
+ @Test
+ fun `start - permission already granted - calls onGranted without requesting`() {
+ var onGrantedCalled = false
+ var requestCalled = false
+ makeHandler(
+ checkGranted = { true },
+ onGranted = { onGrantedCalled = true },
+ requestPermissions = { requestCalled = true }
+ ).start()
+
+ assertTrue("onGranted should be called", onGrantedCalled)
+ assertFalse("requestPermissions should NOT be called", requestCalled)
+ }
+
+ @Test
+ fun `start - permission not granted - calls requestPermissions without calling onGranted`() {
+ var onGrantedCalled = false
+ var requestCalled = false
+ makeHandler(
+ checkGranted = { false },
+ onGranted = { onGrantedCalled = true },
+ requestPermissions = { requestCalled = true }
+ ).start()
+
+ assertFalse("onGranted should NOT be called", onGrantedCalled)
+ assertTrue("requestPermissions should be called", requestCalled)
+ }
+
+ // ── onResult() ───────────────────────────────────────────────────────────
+
+ @Test
+ fun `onResult - fine location granted - calls onGranted`() {
+ var onGrantedCalled = false
+ makeHandler(onGranted = { onGrantedCalled = true }).onResult(
+ mapOf(
+ "android.permission.ACCESS_FINE_LOCATION" to true,
+ "android.permission.ACCESS_COARSE_LOCATION" to false
+ )
+ )
+ assertTrue("onGranted should be called when fine location is granted", onGrantedCalled)
+ }
+
+ @Test
+ fun `onResult - coarse location granted - calls onGranted`() {
+ var onGrantedCalled = false
+ makeHandler(onGranted = { onGrantedCalled = true }).onResult(
+ mapOf(
+ "android.permission.ACCESS_FINE_LOCATION" to false,
+ "android.permission.ACCESS_COARSE_LOCATION" to true
+ )
+ )
+ assertTrue("onGranted should be called when coarse location is granted", onGrantedCalled)
+ }
+
+ @Test
+ fun `onResult - both permissions granted - calls onGranted`() {
+ var onGrantedCalled = false
+ makeHandler(onGranted = { onGrantedCalled = true }).onResult(
+ mapOf(
+ "android.permission.ACCESS_FINE_LOCATION" to true,
+ "android.permission.ACCESS_COARSE_LOCATION" to true
+ )
+ )
+ assertTrue(onGrantedCalled)
+ }
+
+ @Test
+ fun `onResult - all permissions denied - calls onDenied not onGranted`() {
+ var onGrantedCalled = false
+ var onDeniedCalled = false
+ makeHandler(
+ onGranted = { onGrantedCalled = true },
+ onDenied = { onDeniedCalled = true }
+ ).onResult(
+ mapOf(
+ "android.permission.ACCESS_FINE_LOCATION" to false,
+ "android.permission.ACCESS_COARSE_LOCATION" to false
+ )
+ )
+ assertFalse("onGranted should NOT be called", onGrantedCalled)
+ assertTrue("onDenied should be called", onDeniedCalled)
+ }
+
+ @Test
+ fun `onResult - empty grants (never ask again scenario) - calls onDenied`() {
+ var onDeniedCalled = false
+ makeHandler(onDenied = { onDeniedCalled = true }).onResult(emptyMap())
+ assertTrue(
+ "onDenied should be called for empty grants (never-ask-again)",
+ onDeniedCalled
+ )
+ }
+}
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/MainViewModelTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/MainViewModelTest.kt
new file mode 100644
index 0000000..edecdd5
--- /dev/null
+++ b/android-app/app/src/test/kotlin/org/terst/nav/ui_orig/MainViewModelTest.kt
@@ -0,0 +1,105 @@
+package org.terst.nav.ui
+
+import app.cash.turbine.test
+import org.terst.nav.data.model.ForecastItem
+import org.terst.nav.data.model.WindArrow
+import org.terst.nav.data.repository.WeatherRepository
+import io.mockk.coEvery
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.*
+import org.junit.After
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class MainViewModelTest {
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val repo = mockk<WeatherRepository>()
+ private lateinit var vm: MainViewModel
+
+ private val sampleArrow = WindArrow(37.5, -122.3, 15.0, 270.0)
+ private val sampleForecast = listOf(
+ ForecastItem("2026-03-13T00:00", 15.0, 270.0, 18.5, 20, 1)
+ )
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ private fun makeVm() = MainViewModel(repo)
+
+ @Test
+ fun `initial uiState is Loading`() {
+ coEvery { repo.fetchWindArrow(any(), any()) } coAnswers { Result.success(sampleArrow) }
+ coEvery { repo.fetchForecastItems(any(), any()) } coAnswers { Result.success(sampleForecast) }
+ vm = makeVm()
+ // Before loadWeather() is called the state is Loading
+ assertEquals(UiState.Loading, vm.uiState.value)
+ }
+
+ @Test
+ fun `loadWeather success transitions to Success state`() = runTest {
+ coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
+ coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
+ vm = makeVm()
+
+ vm.uiState.test {
+ assertEquals(UiState.Loading, awaitItem())
+ vm.loadWeather(37.5, -122.3)
+ assertEquals(UiState.Success, awaitItem())
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `loadWeather populates windArrow and forecast`() = runTest {
+ coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
+ coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
+ vm = makeVm()
+ vm.loadWeather(37.5, -122.3)
+
+ assertEquals(sampleArrow, vm.windArrow.value)
+ assertEquals(sampleForecast, vm.forecast.value)
+ }
+
+ @Test
+ fun `loadWeather arrow failure transitions to Error state`() = runTest {
+ coEvery { repo.fetchWindArrow(any(), any()) } returns Result.failure(RuntimeException("Net error"))
+ coEvery { repo.fetchForecastItems(any(), any()) } returns Result.success(sampleForecast)
+ vm = makeVm()
+
+ vm.uiState.test {
+ awaitItem() // Loading
+ vm.loadWeather(37.5, -122.3)
+ val state = awaitItem()
+ assertTrue(state is UiState.Error)
+ assertTrue((state as UiState.Error).message.contains("Net error"))
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `loadWeather forecast failure transitions to Error state`() = runTest {
+ coEvery { repo.fetchWindArrow(any(), any()) } returns Result.success(sampleArrow)
+ coEvery { repo.fetchForecastItems(any(), any()) } returns Result.failure(RuntimeException("Timeout"))
+ vm = makeVm()
+
+ vm.uiState.test {
+ awaitItem() // Loading
+ vm.loadWeather(37.5, -122.3)
+ val state = awaitItem()
+ assertTrue(state is UiState.Error)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+}