diff options
| author | Claude <noreply@anthropic.com> | 2026-04-23 23:10:11 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-04-23 23:10:11 +0000 |
| commit | e1db9200e9d44c10450361cc8984a45b2eda87b7 (patch) | |
| tree | 21bdfca02491e6ff4cde7a22673c821c0a03e108 /android-app/app/src/test | |
| parent | 55509f579b4d074f01237dd90791b6d25aaec3cd (diff) | |
| parent | eb78d317c722234a7ef2c501c68c9aa730ec2758 (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')
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 & "Fun" <test>")) + } +} 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)) + } +} |
