diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt new file mode 100644 index 000000000..a6ead2605 --- /dev/null +++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt @@ -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 . + */ +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 +} diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavBackStackExtTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavBackStackExtTest.kt new file mode 100644 index 000000000..2f013a39c --- /dev/null +++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavBackStackExtTest.kt @@ -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 . + */ +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(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(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() + 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(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(NodesRoute.NodesGraph, NodesRoute.Nodes) + val newRoutes = listOf(SettingsRoute.SettingsGraph(), SettingsRoute.About) + + stack.replaceAll(newRoutes) + + assertEquals(newRoutes, stack) + } + + @Test + fun `replaceAll with shorter list trims excess elements`() { + val stack = mutableListOf(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 42)) + val newRoutes = listOf(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(NodesRoute.NodesGraph) + val newRoutes = listOf(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(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() + val newRoutes = listOf(ContactsRoute.ContactsGraph, ContactsRoute.Contacts) + + stack.replaceAll(newRoutes) + + assertEquals(newRoutes, stack) + } + + @Test + fun `replaceAll with identical routes does not mutate entries`() { + val routes = listOf(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(NodesRoute.NodesGraph, NodesRoute.Nodes, NodesRoute.NodeDetail(destNum = 1)) + val newRoutes = + listOf( + NodesRoute.NodesGraph, // same + SettingsRoute.About, // different + ) + + stack.replaceAll(newRoutes) + + assertEquals(2, stack.size) + assertEquals(NodesRoute.NodesGraph, stack[0]) + assertEquals(SettingsRoute.About, stack[1]) + } + + // endregion +} diff --git a/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavigationConfigTest.kt b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavigationConfigTest.kt new file mode 100644 index 000000000..e89879613 --- /dev/null +++ b/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/NavigationConfigTest.kt @@ -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 . + */ +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 = + 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(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> = + 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(savedDefault, MeshtasticNavSavedStateConfig) + val decodedExplicit = decodeFromSavedState(savedExplicit, MeshtasticNavSavedStateConfig) + + assertEquals(decodedDefault, decodedExplicit) + } + } +}