feat: persist node list filters

- add tests
This commit is contained in:
Brandon Ruschill 2026-03-13 11:04:16 -05:00
parent 48f782ff49
commit 322073ed1f
2 changed files with 336 additions and 0 deletions

View file

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
05DCA1AF2F646B3B00D0724C /* NodeFilterParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05DCA1AE2F646B3B00D0724C /* NodeFilterParametersTests.swift */; };
102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAA2E172F41003D191E /* DatadogCore */; };
102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAC2E172F41003D191E /* DatadogCrashReporting */; };
102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAE2E172F41003D191E /* DatadogLogs */; };
@ -346,6 +347,7 @@
/* Begin PBXFileReference section */
01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = "<group>"; };
05DCA1AE2F646B3B00D0724C /* NodeFilterParametersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeFilterParametersTests.swift; sourceTree = "<group>"; };
0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = "<group>"; };
09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = "<group>"; };
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
@ -1400,6 +1402,7 @@
231A53772E69ADB900216B99 /* NodeFilterParameters.swift */,
251926882C3BAF2E00249DF5 /* Actions */,
BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */,
05DCA1AE2F646B3B00D0724C /* NodeFilterParametersTests.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1881,6 +1884,7 @@
DD6F65742C6CB80A0053C113 /* View.swift in Sources */,
DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */,
DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */,
05DCA1AF2F646B3B00D0724C /* NodeFilterParametersTests.swift in Sources */,
DD6D5A332CA1178300ED3032 /* TraceRoute.swift in Sources */,
DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */,
BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */,

View file

@ -0,0 +1,332 @@
//
// NodeFilterParametersTests.swift
// MeshtasticTests
//
// Created on 3/13/26.
//
import XCTest
@MainActor
final class NodeFilterParametersTests: XCTestCase {
var sut: NodeFilterParameters!
let testSuiteName = "NodeFilterParametersTests"
override func setUp() async throws {
try await super.setUp()
// Clear UserDefaults before each test to ensure clean state
clearUserDefaults()
// Create a fresh instance
sut = NodeFilterParameters()
}
override func tearDown() async throws {
sut = nil
clearUserDefaults()
try await super.tearDown()
}
private func clearUserDefaults() {
let defaults = UserDefaults.standard
let keys = [
"nodeFilter.searchText",
"nodeFilter.isOnline",
"nodeFilter.isPkiEncrypted",
"nodeFilter.isFavorite",
"nodeFilter.isIgnored",
"nodeFilter.isEnvironment",
"nodeFilter.distanceFilter",
"nodeFilter.maxDistance",
"nodeFilter.hopsAway",
"nodeFilter.roleFilter",
"nodeFilter.deviceRoles",
"nodeFilter.viaLora",
"nodeFilter.viaMqtt"
]
keys.forEach { defaults.removeObject(forKey: $0) }
}
func testDefaultValues() {
XCTAssertEqual(sut.searchText, "", "Search text should default to empty string")
XCTAssertFalse(sut.isOnline, "isOnline should default to false")
XCTAssertFalse(sut.isPkiEncrypted, "isPkiEncrypted should default to false")
XCTAssertFalse(sut.isFavorite, "isFavorite should default to false")
XCTAssertFalse(sut.isIgnored, "isIgnored should default to false")
XCTAssertFalse(sut.isEnvironment, "isEnvironment should default to false")
XCTAssertFalse(sut.distanceFilter, "distanceFilter should default to false")
XCTAssertEqual(sut.maxDistance, 800_000, "maxDistance should default to 800,000")
XCTAssertEqual(sut.hopsAway, -1.0, "hopsAway should default to -1.0")
XCTAssertFalse(sut.roleFilter, "roleFilter should default to false")
XCTAssertTrue(sut.deviceRoles.isEmpty, "deviceRoles should default to empty set")
XCTAssertTrue(sut.viaLora, "viaLora should default to true")
XCTAssertTrue(sut.viaMqtt, "viaMqtt should default to true")
}
func testSearchTextPersistence() {
sut.searchText = "Test Node"
// Create new instance to test persistence
let newInstance = NodeFilterParameters()
XCTAssertEqual(newInstance.searchText, "Test Node", "Search text should persist")
}
func testBooleanFiltersPersistence() {
sut.isOnline = true
sut.isPkiEncrypted = true
sut.isFavorite = true
sut.isIgnored = true
sut.isEnvironment = true
let newInstance = NodeFilterParameters()
XCTAssertTrue(newInstance.isOnline, "isOnline should persist")
XCTAssertTrue(newInstance.isPkiEncrypted, "isPkiEncrypted should persist")
XCTAssertTrue(newInstance.isFavorite, "isFavorite should persist")
XCTAssertTrue(newInstance.isIgnored, "isIgnored should persist")
XCTAssertTrue(newInstance.isEnvironment, "isEnvironment should persist")
}
func testDistanceFilterPersistence() {
sut.distanceFilter = true
sut.maxDistance = 50_000
let newInstance = NodeFilterParameters()
XCTAssertTrue(newInstance.distanceFilter, "distanceFilter should persist")
XCTAssertEqual(newInstance.maxDistance, 50_000, "maxDistance should persist")
}
func testHopsAwayPersistence() {
sut.hopsAway = 3.0
let newInstance = NodeFilterParameters()
XCTAssertEqual(newInstance.hopsAway, 3.0, "hopsAway should persist")
}
func testRoleFilterPersistence() {
sut.roleFilter = true
let newInstance = NodeFilterParameters()
XCTAssertTrue(newInstance.roleFilter, "roleFilter should persist")
}
func testDeviceRolesInitiallyEmpty() {
XCTAssertTrue(sut.deviceRoles.isEmpty, "deviceRoles should be empty initially")
}
func testDeviceRolesPersistence() {
// Add some roles
sut.deviceRoles = [0, 1, 2]
// Verify they're stored in UserDefaults
let stored = UserDefaults.standard.array(forKey: "nodeFilter.deviceRoles") as? [Int]
XCTAssertNotNil(stored, "deviceRoles should be stored in UserDefaults")
XCTAssertEqual(Set(stored ?? []), Set([0, 1, 2]), "Stored roles should match")
// Create new instance to test persistence
let newInstance = NodeFilterParameters()
XCTAssertEqual(newInstance.deviceRoles, [0, 1, 2], "deviceRoles should persist")
}
func testDeviceRolesAddAndRemove() {
XCTAssertTrue(sut.deviceRoles.isEmpty, "Should start empty")
// Add roles
sut.deviceRoles.insert(1)
sut.deviceRoles.insert(3)
sut.deviceRoles.insert(5)
XCTAssertEqual(sut.deviceRoles.count, 3, "Should have 3 roles")
// Remove a role
sut.deviceRoles.remove(3)
XCTAssertEqual(sut.deviceRoles.count, 2, "Should have 2 roles after removal")
XCTAssertTrue(sut.deviceRoles.contains(1), "Should still contain role 1")
XCTAssertTrue(sut.deviceRoles.contains(5), "Should still contain role 5")
XCTAssertFalse(sut.deviceRoles.contains(3), "Should not contain removed role 3")
// Verify persistence after changes
let newInstance = NodeFilterParameters()
XCTAssertEqual(newInstance.deviceRoles, sut.deviceRoles, "Changes should persist")
}
func testViaLoraAndMqttBothTrueByDefault() {
XCTAssertTrue(sut.viaLora, "viaLora should default to true")
XCTAssertTrue(sut.viaMqtt, "viaMqtt should default to true")
}
func testCanSetViaLoraToFalseWhenMqttIsTrue() {
sut.viaLora = false
XCTAssertFalse(sut.viaLora, "viaLora should be false")
XCTAssertTrue(sut.viaMqtt, "viaMqtt should remain true")
}
func testCanSetViaMqttToFalseWhenLoraIsTrue() {
sut.viaMqtt = false
XCTAssertFalse(sut.viaMqtt, "viaMqtt should be false")
XCTAssertTrue(sut.viaLora, "viaLora should remain true")
}
func testEnforcesAtLeastOneViaLoraOrMqtt_WhenSettingLoraFalse() {
// First set MQTT to false
sut.viaMqtt = false
XCTAssertFalse(sut.viaMqtt, "viaMqtt should be false")
XCTAssertTrue(sut.viaLora, "viaLora should be true")
// Try to set LoRa to false - should enforce MQTT back to true
sut.viaLora = false
XCTAssertFalse(sut.viaLora, "viaLora should be false")
XCTAssertTrue(sut.viaMqtt, "viaMqtt should be enforced to true")
}
func testEnforcesAtLeastOneViaLoraOrMqtt_WhenSettingMqttFalse() {
// First set LoRa to false
sut.viaLora = false
XCTAssertFalse(sut.viaLora, "viaLora should be false")
XCTAssertTrue(sut.viaMqtt, "viaMqtt should be true")
// Try to set MQTT to false - should enforce LoRa back to true
sut.viaMqtt = false
XCTAssertFalse(sut.viaMqtt, "viaMqtt should be false")
XCTAssertTrue(sut.viaLora, "viaLora should be enforced to true")
}
func testViaLoraAndMqttPersistence() {
sut.viaLora = false
sut.viaMqtt = true
let newInstance = NodeFilterParameters()
XCTAssertFalse(newInstance.viaLora, "viaLora state should persist")
XCTAssertTrue(newInstance.viaMqtt, "viaMqtt state should persist")
}
// MARK: - ObservableObject Tests
func testObjectWillChangeTriggeredOnViaLoraChange() {
let expectation = XCTestExpectation(description: "objectWillChange should trigger")
let cancellable = sut.objectWillChange.sink {
expectation.fulfill()
}
sut.viaLora = false
wait(for: [expectation], timeout: 0.1)
cancellable.cancel()
}
func testObjectWillChangeTriggeredOnViaMqttChange() {
let expectation = XCTestExpectation(description: "objectWillChange should trigger")
let cancellable = sut.objectWillChange.sink {
expectation.fulfill()
}
sut.viaMqtt = false
wait(for: [expectation], timeout: 0.1)
cancellable.cancel()
}
func testObjectWillChangeTriggeredOnDeviceRolesChange() {
let expectation = XCTestExpectation(description: "objectWillChange should trigger")
expectation.expectedFulfillmentCount = 1
let cancellable = sut.objectWillChange.sink {
expectation.fulfill()
}
sut.deviceRoles = [1, 2, 3]
wait(for: [expectation], timeout: 0.1)
cancellable.cancel()
}
func testMaxDistanceBoundaryValues() {
sut.maxDistance = 0
XCTAssertEqual(sut.maxDistance, 0, "Should handle zero distance")
sut.maxDistance = Double.greatestFiniteMagnitude
XCTAssertEqual(sut.maxDistance, Double.greatestFiniteMagnitude, "Should handle large distances")
}
func testHopsAwayBoundaryValues() {
sut.hopsAway = -1.0
XCTAssertEqual(sut.hopsAway, -1.0, "Should handle -1 (all hops)")
sut.hopsAway = 0.0
XCTAssertEqual(sut.hopsAway, 0.0, "Should handle 0 (direct)")
sut.hopsAway = 7.0
XCTAssertEqual(sut.hopsAway, 7.0, "Should handle maximum hops")
}
func testSearchTextWithSpecialCharacters() {
let specialStrings = [
"Test Node #1",
"Node@123",
"Node with spaces",
"Node_with_underscores",
"Node-with-dashes",
"Node.with.dots",
"🎯 Node with emoji"
]
for testString in specialStrings {
sut.searchText = testString
XCTAssertEqual(sut.searchText, testString, "Should handle: \(testString)")
}
}
func testDeviceRolesWithDuplicates() {
sut.deviceRoles = [1, 2, 3]
sut.deviceRoles.insert(2) // Try to insert duplicate
XCTAssertEqual(sut.deviceRoles.count, 3, "Set should not contain duplicates")
XCTAssertTrue(sut.deviceRoles.contains(1), "Should contain 1")
XCTAssertTrue(sut.deviceRoles.contains(2), "Should contain 2")
XCTAssertTrue(sut.deviceRoles.contains(3), "Should contain 3")
}
func testMultipleFiltersActive() {
sut.searchText = "Test"
sut.isOnline = true
sut.isFavorite = true
sut.distanceFilter = true
sut.maxDistance = 100_000
sut.hopsAway = 2.0
sut.roleFilter = true
sut.deviceRoles = [0, 1]
sut.viaLora = true
sut.viaMqtt = false
// Verify all settings
XCTAssertEqual(sut.searchText, "Test")
XCTAssertTrue(sut.isOnline)
XCTAssertTrue(sut.isFavorite)
XCTAssertTrue(sut.distanceFilter)
XCTAssertEqual(sut.maxDistance, 100_000)
XCTAssertEqual(sut.hopsAway, 2.0)
XCTAssertTrue(sut.roleFilter)
XCTAssertEqual(sut.deviceRoles, [0, 1])
XCTAssertTrue(sut.viaLora)
XCTAssertFalse(sut.viaMqtt)
// Verify persistence
let newInstance = NodeFilterParameters()
XCTAssertEqual(newInstance.searchText, "Test")
XCTAssertTrue(newInstance.isOnline)
XCTAssertTrue(newInstance.isFavorite)
XCTAssertTrue(newInstance.distanceFilter)
XCTAssertEqual(newInstance.maxDistance, 100_000)
XCTAssertEqual(newInstance.hopsAway, 2.0)
XCTAssertTrue(newInstance.roleFilter)
XCTAssertEqual(newInstance.deviceRoles, [0, 1])
XCTAssertTrue(newInstance.viaLora)
XCTAssertFalse(newInstance.viaMqtt)
}
}