summaryrefslogtreecommitdiff
path: root/android-app/app/src/test/kotlin
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-04-23 23:10:11 +0000
committerClaude <noreply@anthropic.com>2026-04-23 23:10:11 +0000
commite1db9200e9d44c10450361cc8984a45b2eda87b7 (patch)
tree21bdfca02491e6ff4cde7a22673c821c0a03e108 /android-app/app/src/test/kotlin
parent55509f579b4d074f01237dd90791b6d25aaec3cd (diff)
parenteb78d317c722234a7ef2c501c68c9aa730ec2758 (diff)
Merge origin/main — unite all work onto one branch
Brings in dev log (NavLogger), UnitPrefs, MapLayerManager, HUD views, conditions throttling, track save/load pipeline, improved ParticleWindView (antimeridian-aware, dynamic particle count), Snackbar error surfacing, and all other main-branch work from the prior session. Combined with master's hardware source flags, vessel registry, crew management, thermal alarm, CPA collision alerts, and track stats. Also documents primary branch policy in CLAUDE.md and .agent/config.md: all work merges to main, never master. https://claude.ai/code/session_011h2dXbgXg3PesQMmQUNTCW
Diffstat (limited to 'android-app/app/src/test/kotlin')
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/data/repository/WeatherRepositoryTest.kt31
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/track/GpxRoundTripTest.kt83
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/track/TrackSummaryTest.kt56
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/ui/DirectionArrowViewTest.kt25
-rw-r--r--android-app/app/src/test/kotlin/org/terst/nav/ui/InstrumentHandlerTest.kt39
5 files changed, 228 insertions, 6 deletions
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/data/repository/WeatherRepositoryTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/data/repository/WeatherRepositoryTest.kt
index 749630f..c455085 100644
--- a/android-app/app/src/test/kotlin/org/terst/nav/data/repository/WeatherRepositoryTest.kt
+++ b/android-app/app/src/test/kotlin/org/terst/nav/data/repository/WeatherRepositoryTest.kt
@@ -3,8 +3,7 @@ package org.terst.nav.data.repository
import org.terst.nav.data.api.MarineApiService
import org.terst.nav.data.api.WeatherApiService
import org.terst.nav.data.model.*
-import io.mockk.coEvery
-import io.mockk.mockk
+import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Before
@@ -36,6 +35,9 @@ class WeatherRepositoryTest {
time = listOf("2026-03-13T00:00", "2026-03-13T01:00"),
waveHeight = listOf(1.2, 1.1),
waveDirection = listOf(250.0, 255.0),
+ swellWaveHeight = emptyList(),
+ swellWaveDirection = emptyList(),
+ swellWavePeriod = emptyList(),
oceanCurrentVelocity = listOf(0.3, 0.4),
oceanCurrentDirection = listOf(180.0, 185.0)
)
@@ -48,7 +50,7 @@ class WeatherRepositoryTest {
@Test
fun `fetchForecastItems maps weather response to ForecastItem list`() = runTest {
- coEvery { weatherApi.getWeatherForecast(any(), any()) } returns weatherResponse
+ coEvery { weatherApi.getWeatherForecast(any(), any(), any(), any(), any()) } returns weatherResponse
coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
val result = repo.fetchForecastItems(37.5, -122.3)
@@ -65,8 +67,25 @@ class WeatherRepositoryTest {
}
@Test
+ fun `fetchCurrentConditions maps responses to MarineConditions`() = runTest {
+ coEvery { weatherApi.getWeatherForecast(any(), any(), any(), eq(1), any()) } returns weatherResponse
+ coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
+
+ val result = repo.fetchCurrentConditions(37.5, -122.3)
+
+ if (result.isFailure) {
+ fail("fetchCurrentConditions failed with: ${result.exceptionOrNull()}")
+ }
+ val cond = result.getOrThrow()
+ assertEquals(15.0, cond.windSpeedKt!!, 0.001)
+ assertEquals(1.2, cond.waveHeightM!!, 0.001)
+ assertEquals(0.3 * 1.94384, cond.currentSpeedKt!!, 0.001)
+ assertEquals(180.0, cond.currentDirDeg!!, 0.001)
+ }
+
+ @Test
fun `fetchWindArrow returns WindArrow for first (current) hour`() = runTest {
- coEvery { weatherApi.getWeatherForecast(any(), any()) } returns weatherResponse
+ coEvery { weatherApi.getWeatherForecast(any(), any(), any(), eq(1), any()) } returns weatherResponse
coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
val result = repo.fetchWindArrow(37.5, -122.3)
@@ -81,7 +100,7 @@ class WeatherRepositoryTest {
@Test
fun `fetchForecastItems returns failure when weather API throws`() = runTest {
- coEvery { weatherApi.getWeatherForecast(any(), any()) } throws RuntimeException("Network error")
+ coEvery { weatherApi.getWeatherForecast(any(), any(), any(), any(), any()) } throws RuntimeException("Network error")
coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
val result = repo.fetchForecastItems(37.5, -122.3)
@@ -91,7 +110,7 @@ class WeatherRepositoryTest {
@Test
fun `fetchWindArrow returns failure when API throws`() = runTest {
- coEvery { weatherApi.getWeatherForecast(any(), any()) } throws RuntimeException("Timeout")
+ coEvery { weatherApi.getWeatherForecast(any(), any(), any(), eq(1), any()) } throws RuntimeException("Timeout")
coEvery { marineApi.getMarineForecast(any(), any()) } returns marineResponse
val result = repo.fetchWindArrow(37.5, -122.3)
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/track/GpxRoundTripTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/track/GpxRoundTripTest.kt
new file mode 100644
index 0000000..7ed7ec7
--- /dev/null
+++ b/android-app/app/src/test/kotlin/org/terst/nav/track/GpxRoundTripTest.kt
@@ -0,0 +1,83 @@
+package org.terst.nav.track
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class GpxRoundTripTest {
+
+ private fun roundTrip(points: List<TrackPoint>): List<TrackPoint> {
+ val gpx = GpxSerializer.serialize(points, "Test Track")
+ return GpxParser.parse(gpx.byteInputStream())
+ }
+
+ @Test
+ fun `round-trip preserves lat and lon`() {
+ val pt = TrackPoint(lat = 37.8044, lon = -122.2712, sogKnots = 6.1, cogDeg = 247.0)
+ val result = roundTrip(listOf(pt)).first()
+ assertEquals(37.8044, result.lat, 0.00001)
+ assertEquals(-122.2712, result.lon, 0.00001)
+ }
+
+ @Test
+ fun `round-trip preserves sog and cog`() {
+ val pt = TrackPoint(lat = 0.0, lon = 0.0, sogKnots = 5.3, cogDeg = 183.0)
+ val result = roundTrip(listOf(pt)).first()
+ assertEquals(5.3, result.sogKnots, 0.001)
+ assertEquals(183.0, result.cogDeg, 0.001)
+ }
+
+ @Test
+ fun `round-trip preserves optional nav fields`() {
+ val pt = TrackPoint(
+ lat = 1.0, lon = 2.0, sogKnots = 4.0, cogDeg = 90.0,
+ depthMeters = 12.5, baroHpa = 1013.2, windSpeedKnots = 14.0,
+ windAngleDeg = 45.0, isTrueWind = true, waveHeightM = 1.2
+ )
+ val result = roundTrip(listOf(pt)).first()
+ assertEquals(12.5, result.depthMeters!!, 0.001)
+ assertEquals(1013.2, result.baroHpa!!, 0.001)
+ assertEquals(14.0, result.windSpeedKnots!!, 0.001)
+ assertEquals(45.0, result.windAngleDeg!!, 0.001)
+ assertTrue(result.isTrueWind)
+ assertEquals(1.2, result.waveHeightM!!, 0.001)
+ }
+
+ @Test
+ fun `round-trip with null optional fields leaves them null`() {
+ val pt = TrackPoint(lat = 0.0, lon = 0.0, sogKnots = 0.0, cogDeg = 0.0)
+ val result = roundTrip(listOf(pt)).first()
+ assertNull(result.depthMeters)
+ assertNull(result.baroHpa)
+ assertNull(result.windSpeedKnots)
+ }
+
+ @Test
+ fun `round-trip preserves multiple points in order`() {
+ val points = listOf(
+ TrackPoint(lat = 1.0, lon = 1.0, sogKnots = 1.0, cogDeg = 0.0, timestampMs = 1000L),
+ TrackPoint(lat = 2.0, lon = 2.0, sogKnots = 2.0, cogDeg = 90.0, timestampMs = 2000L),
+ TrackPoint(lat = 3.0, lon = 3.0, sogKnots = 3.0, cogDeg = 180.0, timestampMs = 3000L),
+ )
+ val result = roundTrip(points)
+ assertEquals(3, result.size)
+ assertEquals(1.0, result[0].lat, 0.00001)
+ assertEquals(2.0, result[1].lat, 0.00001)
+ assertEquals(3.0, result[2].lat, 0.00001)
+ }
+
+ @Test
+ fun `round-trip preserves timestamp`() {
+ val ts = 1712345678000L
+ val pt = TrackPoint(lat = 0.0, lon = 0.0, sogKnots = 0.0, cogDeg = 0.0, timestampMs = ts)
+ val result = roundTrip(listOf(pt)).first()
+ assertEquals(ts, result.timestampMs)
+ }
+
+ @Test
+ fun `track name with special chars is escaped`() {
+ val gpx = GpxSerializer.serialize(emptyList(), "Track & \"Fun\" <test>")
+ assertTrue(gpx.contains("Track &amp; &quot;Fun&quot; &lt;test&gt;"))
+ }
+}
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/track/TrackSummaryTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/track/TrackSummaryTest.kt
new file mode 100644
index 0000000..2daaf45
--- /dev/null
+++ b/android-app/app/src/test/kotlin/org/terst/nav/track/TrackSummaryTest.kt
@@ -0,0 +1,56 @@
+package org.terst.nav.track
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class TrackSummaryTest {
+
+ private fun pt(lat: Double, lon: Double, sog: Double = 5.0, ts: Long = 0L) =
+ TrackPoint(lat = lat, lon = lon, sogKnots = sog, cogDeg = 0.0, timestampMs = ts)
+
+ @Test
+ fun `distance between two points one nm apart`() {
+ // 1 nautical mile north along the prime meridian ≈ 0.01667° latitude
+ val a = pt(0.0, 0.0, ts = 0L)
+ val b = pt(0.016667, 0.0, ts = 60_000L)
+ val s = summarise(listOf(a, b))
+ assertEquals(1.0, s.distanceNm, 0.01)
+ }
+
+ @Test
+ fun `duration is last minus first timestamp`() {
+ val points = listOf(pt(0.0, 0.0, ts = 1_000L), pt(0.0, 0.0, ts = 61_000L))
+ assertEquals(60_000L, summarise(points).durationMs)
+ }
+
+ @Test
+ fun `max sog picks highest value`() {
+ val points = listOf(pt(0.0, 0.0, sog = 4.0), pt(0.0, 0.0, sog = 9.2), pt(0.0, 0.0, sog = 6.0))
+ assertEquals(9.2, summarise(points).maxSogKt, 0.001)
+ }
+
+ @Test
+ fun `avg wind is null when no wind data`() {
+ val s = summarise(listOf(pt(0.0, 0.0)))
+ assertNull(s.avgWindKt)
+ }
+
+ @Test
+ fun `avg wind averages available readings`() {
+ val points = listOf(
+ TrackPoint(0.0, 0.0, 5.0, 0.0, windSpeedKnots = 10.0),
+ TrackPoint(0.0, 0.0, 5.0, 0.0, windSpeedKnots = 20.0),
+ )
+ assertEquals(15.0, summarise(points).avgWindKt!!, 0.001)
+ }
+
+ @Test
+ fun `avg wave height averages available readings`() {
+ val points = listOf(
+ TrackPoint(0.0, 0.0, 5.0, 0.0, waveHeightM = 1.0),
+ TrackPoint(0.0, 0.0, 5.0, 0.0, waveHeightM = 3.0),
+ )
+ assertEquals(2.0, summarise(points).avgWaveHeightM!!, 0.001)
+ }
+}
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/ui/DirectionArrowViewTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/ui/DirectionArrowViewTest.kt
new file mode 100644
index 0000000..e26b67d
--- /dev/null
+++ b/android-app/app/src/test/kotlin/org/terst/nav/ui/DirectionArrowViewTest.kt
@@ -0,0 +1,25 @@
+package org.terst.nav.ui
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class DirectionArrowViewTest {
+
+ @Test
+ fun `bearing is normalised — values over 360 wrap`() {
+ assertEquals(10f, normalizeBearing(370f), 0.001f)
+ }
+
+ @Test
+ fun `bearing is normalised — negative values wrap`() {
+ assertEquals(350f, normalizeBearing(-10f), 0.001f)
+ }
+
+ @Test
+ fun `bearing is normalised — exactly 360 becomes 0`() {
+ assertEquals(0f, normalizeBearing(360f), 0.001f)
+ }
+}
+
+// Top-level helper extracted from DirectionArrowView for testability
+fun normalizeBearing(deg: Float): Float = ((deg % 360f) + 360f) % 360f
diff --git a/android-app/app/src/test/kotlin/org/terst/nav/ui/InstrumentHandlerTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/ui/InstrumentHandlerTest.kt
new file mode 100644
index 0000000..a749ba2
--- /dev/null
+++ b/android-app/app/src/test/kotlin/org/terst/nav/ui/InstrumentHandlerTest.kt
@@ -0,0 +1,39 @@
+package org.terst.nav.ui
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.util.Locale
+
+class InstrumentHandlerTest {
+
+ @Test
+ fun `metresToFeet converts correctly`() {
+ assertEquals(3.28f, metresToFeet(1.0).toFloat(), 0.01f)
+ }
+
+ @Test
+ fun `metresToFeet zero returns zero`() {
+ assertEquals(0f, metresToFeet(0.0).toFloat(), 0.001f)
+ }
+
+ @Test
+ fun `formatFt formats to one decimal`() {
+ val result = formatFt(3.28084, Locale.US)
+ assertEquals("3.3", result)
+ }
+
+ @Test
+ fun `formatBearing appends degree symbol`() {
+ assertEquals("275°", formatBearing(275.0, Locale.US))
+ }
+
+ @Test
+ fun `formatBearing rounds to zero decimals`() {
+ assertEquals("123°", formatBearing(123.4, Locale.US))
+ }
+
+ @Test
+ fun `formatPeriod appends s`() {
+ assertEquals("· 14s", formatPeriod(14.0, Locale.US))
+ }
+}