diff --git a/PLAN.md b/PLAN.md index 212dbc4..abd26b2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -23,6 +23,7 @@ This is the canonical working plan and progress log for the project. Use this fi - Backend analytics coverage now includes a representative key user flow batch. - iOS project scaffolding now exists as `mobile/ios-app/HermesApp.xcodeproj` with a shared scheme and XCTest target. - iOS sources and tests build and pass in Xcode on an iPhone simulator, and a standalone localization/error-mapping harness passes. +- Android local unit tests now cover localization, error mapping, and analytics batch conversion, and `./gradlew testDebugUnitTest` passes with the Android SDK installed. ### Still Open diff --git a/mobile/android-app/app/build.gradle.kts b/mobile/android-app/app/build.gradle.kts index 33a7296..dcad87a 100644 --- a/mobile/android-app/app/build.gradle.kts +++ b/mobile/android-app/app/build.gradle.kts @@ -24,6 +24,10 @@ android { buildConfig = true } + testOptions { + unitTests.isIncludeAndroidResources = true + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -67,4 +71,9 @@ dependencies { debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") + + testImplementation(kotlin("test")) + testImplementation("androidx.test:core:1.6.1") + testImplementation("junit:junit:4.13.2") + testImplementation("org.robolectric:robolectric:4.13") } diff --git a/mobile/android-app/app/src/test/java/com/hermes/app/core/analytics/HermesAnalyticsTrackerTest.kt b/mobile/android-app/app/src/test/java/com/hermes/app/core/analytics/HermesAnalyticsTrackerTest.kt new file mode 100644 index 0000000..9b9c32b --- /dev/null +++ b/mobile/android-app/app/src/test/java/com/hermes/app/core/analytics/HermesAnalyticsTrackerTest.kt @@ -0,0 +1,42 @@ +package com.hermes.app.core.analytics + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import java.util.UUID +import kotlinx.datetime.Instant +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class HermesAnalyticsTrackerTest { + @Test + fun convertsPendingEventsToBatchRequest() { + val tracker = HermesAnalyticsTracker() + val event = HermesTrackedEvent( + id = UUID.fromString("00000000-0000-0000-0000-000000000001"), + name = "screen_viewed", + attributes = mapOf("screen_name" to "feed"), + timestamp = Instant.fromEpochMilliseconds(1_000), + ) + + val batch = tracker.toBatchRequest(listOf(event)) + + assertEquals(1, batch.events.size) + assertEquals("screen_viewed", batch.events.first().eventName) + assertEquals("feed", batch.events.first().attributes!!.first().value) + } + + @Test + fun marksDeliveredEventsAsRemoved() { + val tracker = HermesAnalyticsTracker() + + tracker.track("screen_viewed", mapOf("screen_name" to "feed")) + val trackedId = tracker.pendingEventsSnapshot().first().id + tracker.markDelivered(setOf(trackedId)) + + assertTrue(tracker.pendingEventsSnapshot().isEmpty()) + } +} diff --git a/mobile/android-app/app/src/test/java/com/hermes/app/core/errors/HermesErrorMapperTest.kt b/mobile/android-app/app/src/test/java/com/hermes/app/core/errors/HermesErrorMapperTest.kt new file mode 100644 index 0000000..f9a76e1 --- /dev/null +++ b/mobile/android-app/app/src/test/java/com/hermes/app/core/errors/HermesErrorMapperTest.kt @@ -0,0 +1,40 @@ +package com.hermes.app.core.errors + +import androidx.test.core.app.ApplicationProvider +import com.hermes.app.R +import com.hermes.app.core.localization.HermesLocalizationStore +import java.io.IOException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class HermesErrorMapperTest { + private val context = ApplicationProvider.getApplicationContext() + private val store = HermesLocalizationStore(context) + + @Test + fun mapsNetworkErrorsToLocalizedCopy() { + assertEquals( + "Nätverksfel. Kontrollera anslutningen.", + mapUserFacingError(store, "sv", IOException("boom")), + ) + } + + @Test + fun mapsGenericErrorsToLocalizedCopy() { + assertEquals( + "Försök igen.", + mapUserFacingError(store, "sv", IllegalStateException("boom")), + ) + } + + @Test + fun ignoresCancellation() { + assertNull(mapUserFacingError(store, "en", kotlinx.coroutines.CancellationException())) + } +} diff --git a/mobile/android-app/app/src/test/java/com/hermes/app/core/localization/HermesLocalizationStoreTest.kt b/mobile/android-app/app/src/test/java/com/hermes/app/core/localization/HermesLocalizationStoreTest.kt new file mode 100644 index 0000000..d5af936 --- /dev/null +++ b/mobile/android-app/app/src/test/java/com/hermes/app/core/localization/HermesLocalizationStoreTest.kt @@ -0,0 +1,36 @@ +package com.hermes.app.core.localization + +import androidx.test.core.app.ApplicationProvider +import com.hermes.app.R +import kotlin.test.Test +import kotlin.test.assertEquals +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.junit.runner.RunWith + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) +class HermesLocalizationStoreTest { + private val context = ApplicationProvider.getApplicationContext() + private val store = HermesLocalizationStore(context) + + @Test + fun englishStringsLoad() { + assertEquals("Settings", store.string("en", R.string.settings_title)) + assertEquals("Enabled", store.string("en", R.string.settings_enabled)) + } + + @Test + fun swedishStringsLoad() { + assertEquals("Inställningar", store.string("sv", R.string.settings_title)) + assertEquals("Aktiverad", store.string("sv", R.string.settings_enabled)) + } + + @Test + fun unsupportedLocaleFallsBackToEnglish() { + store.setLocale("fr") + + assertEquals("en", store.localeCode.value) + assertEquals("English", store.localeName("en", "fr")) + } +}