mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-13 18:40:18 -04:00
test(navigation): add tests for NavigationConfig, DeepLinkRouter, and… (#5052)
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.navigation
|
||||
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class DeepLinkRouterTest {
|
||||
|
||||
private fun route(path: String): List<*>? {
|
||||
val uri = CommonUri.parse("$DEEP_LINK_BASE_URI$path")
|
||||
return DeepLinkRouter.route(uri)
|
||||
}
|
||||
|
||||
// region empty / unrecognized
|
||||
|
||||
@Test
|
||||
fun `empty path returns null`() {
|
||||
assertNull(route(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unrecognized segment returns null`() {
|
||||
assertNull(route("/unknown-page"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region contacts / messages
|
||||
|
||||
@Test
|
||||
fun `share with message`() {
|
||||
assertEquals(
|
||||
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share("hello world")),
|
||||
route("/share?message=hello%20world"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `share without message defaults to empty string`() {
|
||||
assertEquals(listOf(ContactsRoute.ContactsGraph, ContactsRoute.Share("")), route("/share"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `quickchat routes to QuickChat`() {
|
||||
assertEquals(listOf(ContactsRoute.ContactsGraph, ContactsRoute.QuickChat), route("/quickchat"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `messages with contactKey path segment`() {
|
||||
assertEquals(
|
||||
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "abc123", message = "")),
|
||||
route("/messages/abc123"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `messages with contactKey query param`() {
|
||||
assertEquals(
|
||||
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "contact1", message = "")),
|
||||
route("/messages?contactKey=contact1"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `messages with contactKey and message`() {
|
||||
assertEquals(
|
||||
listOf(ContactsRoute.ContactsGraph, ContactsRoute.Messages(contactKey = "contact1", message = "hi")),
|
||||
route("/messages/contact1?message=hi"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `messages without contactKey returns graph only`() {
|
||||
assertEquals(listOf(ContactsRoute.ContactsGraph), route("/messages"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region connections
|
||||
|
||||
@Test
|
||||
fun `connections routes to ConnectionsGraph`() {
|
||||
assertEquals(listOf(ConnectionsRoute.ConnectionsGraph), route("/connections"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region map
|
||||
|
||||
@Test
|
||||
fun `map without waypointId`() {
|
||||
assertEquals(listOf(MapRoute.Map(waypointId = null)), route("/map"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map with waypointId path segment`() {
|
||||
assertEquals(listOf(MapRoute.Map(waypointId = 42)), route("/map/42"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map with waypointId query param`() {
|
||||
assertEquals(listOf(MapRoute.Map(waypointId = 99)), route("/map?waypointId=99"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map with invalid waypointId falls back to null`() {
|
||||
assertEquals(listOf(MapRoute.Map(waypointId = null)), route("/map/not-a-number"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region nodes
|
||||
|
||||
@Test
|
||||
fun `nodes root returns NodesGraph`() {
|
||||
assertEquals(listOf(NodesRoute.NodesGraph), route("/nodes"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum returns NodeDetail`() {
|
||||
assertEquals(listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 1234)), route("/nodes/1234"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and device-metrics sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 1234),
|
||||
NodeDetailRoute.DeviceMetrics(destNum = 1234),
|
||||
),
|
||||
route("/nodes/1234/device-metrics"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and map sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 5678),
|
||||
NodeDetailRoute.NodeMap(destNum = 5678),
|
||||
),
|
||||
route("/nodes/5678/map"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and position sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 100),
|
||||
NodeDetailRoute.PositionLog(destNum = 100),
|
||||
),
|
||||
route("/nodes/100/position"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and environment sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 100),
|
||||
NodeDetailRoute.EnvironmentMetrics(destNum = 100),
|
||||
),
|
||||
route("/nodes/100/environment"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and signal sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 100),
|
||||
NodeDetailRoute.SignalMetrics(destNum = 100),
|
||||
),
|
||||
route("/nodes/100/signal"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and power sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 100),
|
||||
NodeDetailRoute.PowerMetrics(destNum = 100),
|
||||
),
|
||||
route("/nodes/100/power"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and traceroute sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 100),
|
||||
NodeDetailRoute.TracerouteLog(destNum = 100),
|
||||
),
|
||||
route("/nodes/100/traceroute"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and host-metrics sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 100),
|
||||
NodeDetailRoute.HostMetricsLog(destNum = 100),
|
||||
),
|
||||
route("/nodes/100/host-metrics"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and pax sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 100),
|
||||
NodeDetailRoute.PaxMetrics(destNum = 100),
|
||||
),
|
||||
route("/nodes/100/pax"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and neighbors sub-route`() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.NodeDetailGraph(destNum = 100),
|
||||
NodeDetailRoute.NeighborInfoLog(destNum = 100),
|
||||
),
|
||||
route("/nodes/100/neighbors"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum and unknown sub-route falls back to NodeDetail`() {
|
||||
assertEquals(
|
||||
listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 1234)),
|
||||
route("/nodes/1234/unknown-sub"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with non-numeric destNum returns NodesGraph only`() {
|
||||
assertEquals(listOf(NodesRoute.NodesGraph), route("/nodes/not-a-number"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodes with destNum query param`() {
|
||||
assertEquals(listOf(NodesRoute.NodesGraph, NodesRoute.NodeDetail(destNum = 9999)), route("/nodes?destNum=9999"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region settings
|
||||
|
||||
@Test
|
||||
fun `settings root returns SettingsGraph`() {
|
||||
assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null)), route("/settings"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `settings with destNum`() {
|
||||
assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = 1234)), route("/settings/1234"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `settings with destNum and sub-route`() {
|
||||
assertEquals(
|
||||
listOf(SettingsRoute.SettingsGraph(destNum = 1234), SettingsRoute.About),
|
||||
route("/settings/1234/about"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `settings with sub-route without destNum`() {
|
||||
assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null), SettingsRoute.LoRa), route("/settings/lora"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `settings with unknown sub-route returns SettingsGraph only`() {
|
||||
assertEquals(listOf(SettingsRoute.SettingsGraph(destNum = null)), route("/settings/nonexistent-page"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `settings all known sub-routes resolve correctly`() {
|
||||
val expectedSubRoutes =
|
||||
mapOf(
|
||||
"device-config" to SettingsRoute.DeviceConfiguration,
|
||||
"module-config" to SettingsRoute.ModuleConfiguration,
|
||||
"admin" to SettingsRoute.Administration,
|
||||
"user" to SettingsRoute.User,
|
||||
"channel" to SettingsRoute.ChannelConfig,
|
||||
"device" to SettingsRoute.Device,
|
||||
"position" to SettingsRoute.Position,
|
||||
"power" to SettingsRoute.Power,
|
||||
"network" to SettingsRoute.Network,
|
||||
"display" to SettingsRoute.Display,
|
||||
"lora" to SettingsRoute.LoRa,
|
||||
"bluetooth" to SettingsRoute.Bluetooth,
|
||||
"security" to SettingsRoute.Security,
|
||||
"mqtt" to SettingsRoute.MQTT,
|
||||
"serial" to SettingsRoute.Serial,
|
||||
"ext-notification" to SettingsRoute.ExtNotification,
|
||||
"store-forward" to SettingsRoute.StoreForward,
|
||||
"range-test" to SettingsRoute.RangeTest,
|
||||
"telemetry" to SettingsRoute.Telemetry,
|
||||
"canned-message" to SettingsRoute.CannedMessage,
|
||||
"audio" to SettingsRoute.Audio,
|
||||
"remote-hardware" to SettingsRoute.RemoteHardware,
|
||||
"neighbor-info" to SettingsRoute.NeighborInfo,
|
||||
"ambient-lighting" to SettingsRoute.AmbientLighting,
|
||||
"detection-sensor" to SettingsRoute.DetectionSensor,
|
||||
"paxcounter" to SettingsRoute.Paxcounter,
|
||||
"status-message" to SettingsRoute.StatusMessage,
|
||||
"traffic-management" to SettingsRoute.TrafficManagement,
|
||||
"tak" to SettingsRoute.TAK,
|
||||
"clean-node-db" to SettingsRoute.CleanNodeDb,
|
||||
"debug-panel" to SettingsRoute.DebugPanel,
|
||||
"about" to SettingsRoute.About,
|
||||
"filter-settings" to SettingsRoute.FilterSettings,
|
||||
)
|
||||
|
||||
expectedSubRoutes.forEach { (slug, expectedRoute) ->
|
||||
assertEquals(
|
||||
listOf(SettingsRoute.SettingsGraph(destNum = null), expectedRoute),
|
||||
route("/settings/$slug"),
|
||||
"Settings sub-route '$slug' did not resolve to $expectedRoute",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region channels
|
||||
|
||||
@Test
|
||||
fun `channels routes to ChannelsGraph`() {
|
||||
assertEquals(listOf(ChannelsRoute.ChannelsGraph), route("/channels"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region firmware
|
||||
|
||||
@Test
|
||||
fun `firmware root returns FirmwareGraph`() {
|
||||
assertEquals(listOf(FirmwareRoute.FirmwareGraph), route("/firmware"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firmware update returns FirmwareGraph and FirmwareUpdate`() {
|
||||
assertEquals(listOf(FirmwareRoute.FirmwareGraph, FirmwareRoute.FirmwareUpdate), route("/firmware/update"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region wifi-provision
|
||||
|
||||
@Test
|
||||
fun `wifi-provision without address`() {
|
||||
assertEquals(listOf(WifiProvisionRoute.WifiProvision(address = null)), route("/wifi-provision"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wifi-provision with address query param`() {
|
||||
assertEquals(
|
||||
listOf(WifiProvisionRoute.WifiProvision(address = "AA:BB:CC:DD:EE:FF")),
|
||||
route("/wifi-provision?address=AA:BB:CC:DD:EE:FF"),
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region case insensitivity
|
||||
|
||||
@Test
|
||||
fun `route segments are case insensitive`() {
|
||||
assertEquals(listOf(NodesRoute.NodesGraph), route("/Nodes"))
|
||||
assertEquals(listOf(ConnectionsRoute.ConnectionsGraph), route("/CONNECTIONS"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class NavBackStackExtTest {
|
||||
|
||||
// region replaceLast
|
||||
|
||||
@Test
|
||||
fun `replaceLast on non-empty list replaces the last element`() {
|
||||
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
|
||||
stack.replaceLast(NodesRoute.NodeDetail(destNum = 42))
|
||||
|
||||
assertEquals(2, stack.size)
|
||||
assertEquals(NodesRoute.NodesGraph, stack[0])
|
||||
assertEquals(NodesRoute.NodeDetail(destNum = 42), stack[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceLast on single-element list replaces that element`() {
|
||||
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph)
|
||||
stack.replaceLast(SettingsRoute.SettingsGraph())
|
||||
|
||||
assertEquals(1, stack.size)
|
||||
assertEquals(SettingsRoute.SettingsGraph(), stack[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceLast on empty list adds the element`() {
|
||||
val stack = mutableListOf<NavKey>()
|
||||
stack.replaceLast(NodesRoute.Nodes)
|
||||
|
||||
assertEquals(1, stack.size)
|
||||
assertEquals(NodesRoute.Nodes, stack[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceLast with same element does not mutate`() {
|
||||
val route = NodesRoute.Nodes
|
||||
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, route)
|
||||
stack.replaceLast(route)
|
||||
|
||||
assertEquals(2, stack.size)
|
||||
assertEquals(route, stack[1])
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region replaceAll
|
||||
|
||||
@Test
|
||||
fun `replaceAll replaces entire stack with new routes`() {
|
||||
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
|
||||
val newRoutes = listOf<NavKey>(SettingsRoute.SettingsGraph(), SettingsRoute.About)
|
||||
|
||||
stack.replaceAll(newRoutes)
|
||||
|
||||
assertEquals(newRoutes, stack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceAll with shorter list trims excess elements`() {
|
||||
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 42))
|
||||
val newRoutes = listOf<NavKey>(SettingsRoute.SettingsGraph())
|
||||
|
||||
stack.replaceAll(newRoutes)
|
||||
|
||||
assertEquals(1, stack.size)
|
||||
assertEquals(SettingsRoute.SettingsGraph(), stack[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceAll with longer list appends new elements`() {
|
||||
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph)
|
||||
val newRoutes = listOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 99))
|
||||
|
||||
stack.replaceAll(newRoutes)
|
||||
|
||||
assertEquals(newRoutes, stack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceAll with empty list clears the stack`() {
|
||||
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
|
||||
|
||||
stack.replaceAll(emptyList())
|
||||
|
||||
assertEquals(0, stack.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceAll on empty stack with new routes populates it`() {
|
||||
val stack = mutableListOf<NavKey>()
|
||||
val newRoutes = listOf<NavKey>(ContactsRoute.ContactsGraph, ContactsRoute.Contacts)
|
||||
|
||||
stack.replaceAll(newRoutes)
|
||||
|
||||
assertEquals(newRoutes, stack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceAll with identical routes does not mutate entries`() {
|
||||
val routes = listOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes)
|
||||
val stack = routes.toMutableList()
|
||||
|
||||
stack.replaceAll(routes)
|
||||
|
||||
assertEquals(routes, stack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `replaceAll with partial overlap only changes differing elements`() {
|
||||
val stack = mutableListOf<NavKey>(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1))
|
||||
val newRoutes =
|
||||
listOf<NavKey>(
|
||||
NodesRoute.NodesGraph, // same
|
||||
SettingsRoute.About, // different
|
||||
)
|
||||
|
||||
stack.replaceAll(newRoutes)
|
||||
|
||||
assertEquals(2, stack.size)
|
||||
assertEquals(NodesRoute.NodesGraph, stack[0])
|
||||
assertEquals(SettingsRoute.About, stack[1])
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.navigation
|
||||
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.savedstate.serialization.decodeFromSavedState
|
||||
import androidx.savedstate.serialization.encodeToSavedState
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Verifies that all route subclasses registered in [MeshtasticNavSavedStateConfig] can round-trip through SavedState
|
||||
* serialization. This catches:
|
||||
* - Missing `@Serializable` annotations on new route subclasses
|
||||
* - Sealed interfaces not registered in [NavigationConfig.kt]
|
||||
* - Breaking changes in the `subclassesOfSealed` experimental API
|
||||
*/
|
||||
class NavigationConfigTest {
|
||||
|
||||
/**
|
||||
* Every concrete route instance that can appear in a backstack. When adding a new route, add a representative
|
||||
* instance here — the test will fail if serialization is misconfigured.
|
||||
*/
|
||||
private val allRouteInstances: List<NavKey> =
|
||||
listOf(
|
||||
// ChannelsRoute
|
||||
ChannelsRoute.ChannelsGraph,
|
||||
ChannelsRoute.Channels,
|
||||
// ConnectionsRoute
|
||||
ConnectionsRoute.ConnectionsGraph,
|
||||
ConnectionsRoute.Connections,
|
||||
// ContactsRoute
|
||||
ContactsRoute.ContactsGraph,
|
||||
ContactsRoute.Contacts,
|
||||
ContactsRoute.Messages(contactKey = "test-contact", message = "hello"),
|
||||
ContactsRoute.Messages(contactKey = "test-contact"),
|
||||
ContactsRoute.Share(message = "share-text"),
|
||||
ContactsRoute.QuickChat,
|
||||
// MapRoute
|
||||
MapRoute.Map(),
|
||||
MapRoute.Map(waypointId = 42),
|
||||
// NodesRoute
|
||||
NodesRoute.NodesGraph,
|
||||
NodesRoute.Nodes,
|
||||
NodesRoute.NodeDetailGraph(destNum = 1234),
|
||||
NodesRoute.NodeDetailGraph(),
|
||||
NodesRoute.NodeDetail(destNum = 5678),
|
||||
NodesRoute.NodeDetail(),
|
||||
// NodeDetailRoute
|
||||
NodeDetailRoute.DeviceMetrics(destNum = 100),
|
||||
NodeDetailRoute.NodeMap(destNum = 100),
|
||||
NodeDetailRoute.PositionLog(destNum = 100),
|
||||
NodeDetailRoute.EnvironmentMetrics(destNum = 100),
|
||||
NodeDetailRoute.SignalMetrics(destNum = 100),
|
||||
NodeDetailRoute.PowerMetrics(destNum = 100),
|
||||
NodeDetailRoute.TracerouteLog(destNum = 100),
|
||||
NodeDetailRoute.TracerouteMap(destNum = 100, requestId = 200, logUuid = "uuid-123"),
|
||||
NodeDetailRoute.TracerouteMap(destNum = 100, requestId = 200),
|
||||
NodeDetailRoute.HostMetricsLog(destNum = 100),
|
||||
NodeDetailRoute.PaxMetrics(destNum = 100),
|
||||
NodeDetailRoute.NeighborInfoLog(destNum = 100),
|
||||
// SettingsRoute
|
||||
SettingsRoute.SettingsGraph(),
|
||||
SettingsRoute.SettingsGraph(destNum = 999),
|
||||
SettingsRoute.Settings(),
|
||||
SettingsRoute.Settings(destNum = 999),
|
||||
SettingsRoute.DeviceConfiguration,
|
||||
SettingsRoute.ModuleConfiguration,
|
||||
SettingsRoute.Administration,
|
||||
SettingsRoute.User,
|
||||
SettingsRoute.ChannelConfig,
|
||||
SettingsRoute.Device,
|
||||
SettingsRoute.Position,
|
||||
SettingsRoute.Power,
|
||||
SettingsRoute.Network,
|
||||
SettingsRoute.Display,
|
||||
SettingsRoute.LoRa,
|
||||
SettingsRoute.Bluetooth,
|
||||
SettingsRoute.Security,
|
||||
SettingsRoute.MQTT,
|
||||
SettingsRoute.Serial,
|
||||
SettingsRoute.ExtNotification,
|
||||
SettingsRoute.StoreForward,
|
||||
SettingsRoute.RangeTest,
|
||||
SettingsRoute.Telemetry,
|
||||
SettingsRoute.CannedMessage,
|
||||
SettingsRoute.Audio,
|
||||
SettingsRoute.RemoteHardware,
|
||||
SettingsRoute.NeighborInfo,
|
||||
SettingsRoute.AmbientLighting,
|
||||
SettingsRoute.DetectionSensor,
|
||||
SettingsRoute.Paxcounter,
|
||||
SettingsRoute.StatusMessage,
|
||||
SettingsRoute.TrafficManagement,
|
||||
SettingsRoute.TAK,
|
||||
SettingsRoute.CleanNodeDb,
|
||||
SettingsRoute.DebugPanel,
|
||||
SettingsRoute.About,
|
||||
SettingsRoute.FilterSettings,
|
||||
// FirmwareRoute
|
||||
FirmwareRoute.FirmwareGraph,
|
||||
FirmwareRoute.FirmwareUpdate,
|
||||
// WifiProvisionRoute
|
||||
WifiProvisionRoute.WifiProvisionGraph,
|
||||
WifiProvisionRoute.WifiProvision(address = "AA:BB:CC:DD:EE:FF"),
|
||||
WifiProvisionRoute.WifiProvision(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `all route instances round-trip through SavedState serialization`() {
|
||||
allRouteInstances.forEach { route ->
|
||||
val savedState = encodeToSavedState(route, MeshtasticNavSavedStateConfig)
|
||||
val decoded = decodeFromSavedState<NavKey>(savedState, MeshtasticNavSavedStateConfig)
|
||||
assertEquals(
|
||||
route,
|
||||
decoded,
|
||||
"Round-trip failed for ${route::class.simpleName}: encoded $route but decoded $decoded",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all sealed route interfaces are represented in the route instances list`() {
|
||||
// Verify we have at least one instance from each sealed route interface.
|
||||
// This catches the case where a new sealed interface is added to Routes.kt
|
||||
// but no instances are added to allRouteInstances above.
|
||||
val representedInterfaces =
|
||||
allRouteInstances
|
||||
.map { route ->
|
||||
when (route) {
|
||||
is ChannelsRoute -> "ChannelsRoute"
|
||||
is ConnectionsRoute -> "ConnectionsRoute"
|
||||
is ContactsRoute -> "ContactsRoute"
|
||||
is MapRoute -> "MapRoute"
|
||||
is NodesRoute -> "NodesRoute"
|
||||
is NodeDetailRoute -> "NodeDetailRoute"
|
||||
is SettingsRoute -> "SettingsRoute"
|
||||
is FirmwareRoute -> "FirmwareRoute"
|
||||
is WifiProvisionRoute -> "WifiProvisionRoute"
|
||||
else -> "Unknown(${route::class.simpleName})"
|
||||
}
|
||||
}
|
||||
.toSet()
|
||||
|
||||
val expectedInterfaces =
|
||||
setOf(
|
||||
"ChannelsRoute",
|
||||
"ConnectionsRoute",
|
||||
"ContactsRoute",
|
||||
"MapRoute",
|
||||
"NodesRoute",
|
||||
"NodeDetailRoute",
|
||||
"SettingsRoute",
|
||||
"FirmwareRoute",
|
||||
"WifiProvisionRoute",
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expectedInterfaces,
|
||||
representedInterfaces,
|
||||
"Missing sealed route interfaces in test coverage. " +
|
||||
"Missing: ${expectedInterfaces - representedInterfaces}",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `route instances with default parameters serialize correctly`() {
|
||||
// Specifically test routes with nullable/default params to catch
|
||||
// serialization issues with optional fields.
|
||||
val routesWithDefaults: List<Pair<NavKey, NavKey>> =
|
||||
listOf(
|
||||
MapRoute.Map() to MapRoute.Map(waypointId = null),
|
||||
NodesRoute.NodeDetailGraph() to NodesRoute.NodeDetailGraph(destNum = null),
|
||||
NodesRoute.NodeDetail() to NodesRoute.NodeDetail(destNum = null),
|
||||
SettingsRoute.SettingsGraph() to SettingsRoute.SettingsGraph(destNum = null),
|
||||
SettingsRoute.Settings() to SettingsRoute.Settings(destNum = null),
|
||||
WifiProvisionRoute.WifiProvision() to WifiProvisionRoute.WifiProvision(address = null),
|
||||
)
|
||||
|
||||
routesWithDefaults.forEach { (defaultInstance, explicitNullInstance) ->
|
||||
assertEquals(
|
||||
defaultInstance,
|
||||
explicitNullInstance,
|
||||
"Default and explicit null should be equal for ${defaultInstance::class.simpleName}",
|
||||
)
|
||||
|
||||
val savedDefault = encodeToSavedState(defaultInstance, MeshtasticNavSavedStateConfig)
|
||||
val savedExplicit = encodeToSavedState(explicitNullInstance, MeshtasticNavSavedStateConfig)
|
||||
|
||||
val decodedDefault = decodeFromSavedState<NavKey>(savedDefault, MeshtasticNavSavedStateConfig)
|
||||
val decodedExplicit = decodeFromSavedState<NavKey>(savedExplicit, MeshtasticNavSavedStateConfig)
|
||||
|
||||
assertEquals(decodedDefault, decodedExplicit)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user