From e65f9bbec803430393bd8d641a85fdba91fe568f Mon Sep 17 00:00:00 2001 From: Brandon Ruschill Date: Mon, 16 Mar 2026 13:54:51 -0500 Subject: [PATCH] feat/persist-node-list-filters - move test file to correct location - get test working --- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../NodeFilterParametersTests.swift | 571 ++++++++++-------- 2 files changed, 330 insertions(+), 245 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 8d829786..412dc622 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 05DCA1AF2F646B3B00D0724C /* NodeFilterParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05DCA1AE2F646B3B00D0724C /* NodeFilterParametersTests.swift */; }; + 0516B3FE2F68892000D0FC40 /* 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 */; }; @@ -1661,6 +1661,7 @@ buildActionMask = 2147483647; files = ( 25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */, + 0516B3FE2F68892000D0FC40 /* NodeFilterParametersTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1884,7 +1885,6 @@ 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 */, diff --git a/MeshtasticTests/NodeFilterParametersTests.swift b/MeshtasticTests/NodeFilterParametersTests.swift index eb6add21..bd4593f7 100644 --- a/MeshtasticTests/NodeFilterParametersTests.swift +++ b/MeshtasticTests/NodeFilterParametersTests.swift @@ -1,332 +1,417 @@ // // NodeFilterParametersTests.swift -// MeshtasticTests +// Meshtastic // -// Created on 3/13/26. +// Created on 3/16/26. // +import Foundation import XCTest +@testable import Meshtastic + @MainActor -final class NodeFilterParametersTests: XCTestCase { +class NodeFilterParametersTests: XCTestCase { - var sut: NodeFilterParameters! - let testSuiteName = "NodeFilterParametersTests" + // MARK: - Initialization Tests - override func setUp() async throws { - try await super.setUp() + func testDefaultInitialization() async throws { + // Clean up UserDefaults before test + clearAllFilterDefaults() - // Clear UserDefaults before each test to ensure clean state - clearUserDefaults() + let filters = NodeFilterParameters() - // Create a fresh instance - sut = NodeFilterParameters() + XCTAssertEqual(filters.searchText, "") + XCTAssertEqual(filters.isOnline, false) + XCTAssertEqual(filters.isPkiEncrypted, false) + XCTAssertEqual(filters.isFavorite, false) + XCTAssertEqual(filters.isIgnored, false) + XCTAssertEqual(filters.isEnvironment, false) + XCTAssertEqual(filters.distanceFilter, false) + XCTAssertEqual(filters.maxDistance, 800_000) + XCTAssertEqual(filters.hopsAway, -1.0) + XCTAssertEqual(filters.roleFilter, false) + XCTAssertTrue(filters.deviceRoles.isEmpty) + XCTAssertEqual(filters.viaLora, true) + XCTAssertEqual(filters.viaMqtt, true) } - override func tearDown() async throws { - sut = nil - clearUserDefaults() - try await super.tearDown() - } + func testInitializationWithPersistedDeviceRoles() async throws { + clearAllFilterDefaults() - 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) } - } + // Store device roles in UserDefaults + let expectedRoles = [1, 2, 3, 5, 8] + UserDefaults.standard.set(expectedRoles, forKey: "nodeFilter.deviceRoles") - 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") - } + let filters = NodeFilterParameters() - func testSearchTextPersistence() { - sut.searchText = "Test Node" - - // Create new instance to test persistence - let newInstance = NodeFilterParameters() - XCTAssertEqual(newInstance.searchText, "Test Node", "Search text should persist") + XCTAssertEqual(filters.deviceRoles, Set(expectedRoles)) } - func testBooleanFiltersPersistence() { - sut.isOnline = true - sut.isPkiEncrypted = true - sut.isFavorite = true - sut.isIgnored = true - sut.isEnvironment = true + // MARK: - @AppStorage Persistence Tests + + func testSearchTextPersistence() async throws { + clearAllFilterDefaults() - 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") + let filters1 = NodeFilterParameters() + filters1.searchText = "Test Node" + + let filters2 = NodeFilterParameters() + XCTAssertEqual(filters2.searchText, "Test Node") } - func testDistanceFilterPersistence() { - sut.distanceFilter = true - sut.maxDistance = 50_000 + func testBooleanFiltersPersistence() async throws { + clearAllFilterDefaults() - let newInstance = NodeFilterParameters() - XCTAssertTrue(newInstance.distanceFilter, "distanceFilter should persist") - XCTAssertEqual(newInstance.maxDistance, 50_000, "maxDistance should persist") + let filters1 = NodeFilterParameters() + filters1.isOnline = true + filters1.isPkiEncrypted = true + filters1.isFavorite = true + filters1.isIgnored = true + filters1.isEnvironment = true + filters1.distanceFilter = true + filters1.roleFilter = true + + let filters2 = NodeFilterParameters() + XCTAssertEqual(filters2.isOnline, true) + XCTAssertEqual(filters2.isPkiEncrypted, true) + XCTAssertEqual(filters2.isFavorite, true) + XCTAssertEqual(filters2.isIgnored, true) + XCTAssertEqual(filters2.isEnvironment, true) + XCTAssertEqual(filters2.distanceFilter, true) + XCTAssertEqual(filters2.roleFilter, true) } - func testHopsAwayPersistence() { - sut.hopsAway = 3.0 + func testNumericFiltersPersistence() async throws { + clearAllFilterDefaults() - let newInstance = NodeFilterParameters() - XCTAssertEqual(newInstance.hopsAway, 3.0, "hopsAway should persist") + let filters1 = NodeFilterParameters() + filters1.maxDistance = 500_000 + filters1.hopsAway = 3.0 + + let filters2 = NodeFilterParameters() + XCTAssertEqual(filters2.maxDistance, 500_000) + XCTAssertEqual(filters2.hopsAway, 3.0) } - func testRoleFilterPersistence() { - sut.roleFilter = true + // MARK: - Device Roles Tests + + func testDeviceRolesPersistence() async throws { + clearAllFilterDefaults() - let newInstance = NodeFilterParameters() - XCTAssertTrue(newInstance.roleFilter, "roleFilter should persist") - } + let filters1 = NodeFilterParameters() + filters1.deviceRoles = [1, 3, 5, 7] - func testDeviceRolesInitiallyEmpty() { - XCTAssertTrue(sut.deviceRoles.isEmpty, "deviceRoles should be empty initially") + // Verify it's stored in UserDefaults + let storedRoles = UserDefaults.standard.array(forKey: "nodeFilter.deviceRoles") as? [Int] + XCTAssertNotNil(storedRoles) + XCTAssertEqual(Set(storedRoles!), Set([1, 3, 5, 7])) + + // Verify it persists to new instance + let filters2 = NodeFilterParameters() + XCTAssertEqual(filters2.deviceRoles, Set([1, 3, 5, 7])) } - func testDeviceRolesPersistence() { - // Add some roles - sut.deviceRoles = [0, 1, 2] + func testAddingDeviceRoles() async throws { + clearAllFilterDefaults() - // 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") + let filters = NodeFilterParameters() + filters.deviceRoles.insert(2) + filters.deviceRoles.insert(4) + filters.deviceRoles.insert(6) - // Create new instance to test persistence - let newInstance = NodeFilterParameters() - XCTAssertEqual(newInstance.deviceRoles, [0, 1, 2], "deviceRoles should persist") + let newFilters = NodeFilterParameters() + XCTAssertTrue(newFilters.deviceRoles.contains(2)) + XCTAssertTrue(newFilters.deviceRoles.contains(4)) + XCTAssertTrue(newFilters.deviceRoles.contains(6)) + XCTAssertEqual(newFilters.deviceRoles.count, 3) } - func testDeviceRolesAddAndRemove() { - XCTAssertTrue(sut.deviceRoles.isEmpty, "Should start empty") + func testRemovingDeviceRoles() async throws { + clearAllFilterDefaults() - // Add roles - sut.deviceRoles.insert(1) - sut.deviceRoles.insert(3) - sut.deviceRoles.insert(5) - XCTAssertEqual(sut.deviceRoles.count, 3, "Should have 3 roles") + let filters1 = NodeFilterParameters() + filters1.deviceRoles = [1, 2, 3, 4, 5] - // 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") + filters1.deviceRoles.remove(2) + filters1.deviceRoles.remove(4) - // 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") + let filters2 = NodeFilterParameters() + XCTAssertEqual(filters2.deviceRoles, Set([1, 3, 5])) + XCTAssertFalse(filters2.deviceRoles.contains(2)) + XCTAssertFalse(filters2.deviceRoles.contains(4)) } - func testCanSetViaLoraToFalseWhenMqttIsTrue() { - sut.viaLora = false + func testEmptyDeviceRolesPersistence() async throws { + clearAllFilterDefaults() - XCTAssertFalse(sut.viaLora, "viaLora should be false") - XCTAssertTrue(sut.viaMqtt, "viaMqtt should remain true") + let filters1 = NodeFilterParameters() + filters1.deviceRoles = [1, 2, 3] + + // Clear the set + filters1.deviceRoles = [] + + let filters2 = NodeFilterParameters() + XCTAssertTrue(filters2.deviceRoles.isEmpty) } - func testCanSetViaMqttToFalseWhenLoraIsTrue() { - sut.viaMqtt = false + // MARK: - Via Lora/MQTT Enforcement Tests + + func testViaLoraEnforcesViaMqtt() async throws { + clearAllFilterDefaults() - XCTAssertFalse(sut.viaMqtt, "viaMqtt should be false") - XCTAssertTrue(sut.viaLora, "viaLora should remain true") + let filters = NodeFilterParameters() + + // Start with both true + XCTAssertEqual(filters.viaLora, true) + XCTAssertEqual(filters.viaMqtt, true) + + // Set viaLora to false + filters.viaLora = false + + // viaMqtt should remain true + XCTAssertEqual(filters.viaLora, false) + XCTAssertEqual(filters.viaMqtt, true) + + // Try to set viaMqtt to false - it should enforce viaLora to true + filters.viaMqtt = false + + XCTAssertEqual(filters.viaLora, true) + XCTAssertEqual(filters.viaMqtt, false) } - 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") + func testViaMqttEnforcesViaLora() async throws { + clearAllFilterDefaults() - // Try to set LoRa to false - should enforce MQTT back to true - sut.viaLora = false + let filters = NodeFilterParameters() - XCTAssertFalse(sut.viaLora, "viaLora should be false") - XCTAssertTrue(sut.viaMqtt, "viaMqtt should be enforced to true") + // Start with both true + XCTAssertEqual(filters.viaLora, true) + XCTAssertEqual(filters.viaMqtt, true) + + // Set viaMqtt to false + filters.viaMqtt = false + + // viaLora should remain true + XCTAssertEqual(filters.viaLora, true) + XCTAssertEqual(filters.viaMqtt, false) + + // Try to set viaLora to false - it should enforce viaMqtt to true + filters.viaLora = false + + XCTAssertEqual(filters.viaLora, false) + XCTAssertEqual(filters.viaMqtt, 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") + func testBothViaTrue() async throws { + clearAllFilterDefaults() - // Try to set MQTT to false - should enforce LoRa back to true - sut.viaMqtt = false + let filters = NodeFilterParameters() + filters.viaLora = true + filters.viaMqtt = true - XCTAssertFalse(sut.viaMqtt, "viaMqtt should be false") - XCTAssertTrue(sut.viaLora, "viaLora should be enforced to true") + XCTAssertEqual(filters.viaLora, true) + XCTAssertEqual(filters.viaMqtt, true) } - func testViaLoraAndMqttPersistence() { - sut.viaLora = false - sut.viaMqtt = true + func testViaSettingsPersistence() async throws { + clearAllFilterDefaults() - let newInstance = NodeFilterParameters() - XCTAssertFalse(newInstance.viaLora, "viaLora state should persist") - XCTAssertTrue(newInstance.viaMqtt, "viaMqtt state should persist") + let filters1 = NodeFilterParameters() + filters1.viaLora = false + // viaMqtt should be enforced to true + + let filters2 = NodeFilterParameters() + XCTAssertEqual(filters2.viaLora, false) + XCTAssertEqual(filters2.viaMqtt, true) + } + + func testCannotHaveBothViaFalse() async throws { + clearAllFilterDefaults() + + let filters = NodeFilterParameters() + + // Set viaLora to false first + filters.viaLora = false + XCTAssertEqual(filters.viaLora, false) + XCTAssertEqual(filters.viaMqtt, true) + + // Try to set viaMqtt to false + filters.viaMqtt = false + + // viaLora should be enforced back to true + XCTAssertEqual(filters.viaLora, true) + XCTAssertEqual(filters.viaMqtt, false) } // MARK: - ObservableObject Tests - func testObjectWillChangeTriggeredOnViaLoraChange() { - let expectation = XCTestExpectation(description: "objectWillChange should trigger") + func testObjectWillChange() async throws { + clearAllFilterDefaults() - let cancellable = sut.objectWillChange.sink { - expectation.fulfill() + let filters = NodeFilterParameters() + var changeCount = 0 + + let cancellable = filters.objectWillChange.sink { + changeCount += 1 } - sut.viaLora = false + // Modify various properties + filters.searchText = "Test" + filters.isOnline = true + filters.deviceRoles.insert(1) + filters.viaLora = false + + // Should have triggered changes + XCTAssertGreaterThan(changeCount, 0) - wait(for: [expectation], timeout: 0.1) cancellable.cancel() } - func testObjectWillChangeTriggeredOnViaMqttChange() { - let expectation = XCTestExpectation(description: "objectWillChange should trigger") + // MARK: - Edge Cases + + func testLargeMaxDistance() async throws { + clearAllFilterDefaults() - let cancellable = sut.objectWillChange.sink { - expectation.fulfill() - } + let filters = NodeFilterParameters() + filters.maxDistance = 10_000_000 - sut.viaMqtt = false - - wait(for: [expectation], timeout: 0.1) - cancellable.cancel() + let newFilters = NodeFilterParameters() + XCTAssertEqual(newFilters.maxDistance, 10_000_000) } - func testObjectWillChangeTriggeredOnDeviceRolesChange() { - let expectation = XCTestExpectation(description: "objectWillChange should trigger") - expectation.expectedFulfillmentCount = 1 + func testNegativeHopsAway() async throws { + clearAllFilterDefaults() - let cancellable = sut.objectWillChange.sink { - expectation.fulfill() - } + let filters = NodeFilterParameters() + filters.hopsAway = -1.0 - 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") + let newFilters = NodeFilterParameters() + XCTAssertEqual(newFilters.hopsAway, -1.0) } - func testHopsAwayBoundaryValues() { - sut.hopsAway = -1.0 - XCTAssertEqual(sut.hopsAway, -1.0, "Should handle -1 (all hops)") + func testZeroHopsAway() async throws { + clearAllFilterDefaults() - sut.hopsAway = 0.0 - XCTAssertEqual(sut.hopsAway, 0.0, "Should handle 0 (direct)") + let filters = NodeFilterParameters() + filters.hopsAway = 0.0 - sut.hopsAway = 7.0 - XCTAssertEqual(sut.hopsAway, 7.0, "Should handle maximum hops") + let newFilters = NodeFilterParameters() + XCTAssertEqual(newFilters.hopsAway, 0.0) } - func testSearchTextWithSpecialCharacters() { - let specialStrings = [ - "Test Node #1", - "Node@123", - "Node with spaces", - "Node_with_underscores", - "Node-with-dashes", - "Node.with.dots", - "๐ŸŽฏ Node with emoji" - ] + func testSpecialCharactersInSearchText() async throws { + clearAllFilterDefaults() - for testString in specialStrings { - sut.searchText = testString - XCTAssertEqual(sut.searchText, testString, "Should handle: \(testString)") - } + let filters = NodeFilterParameters() + filters.searchText = "Test!@#$%^&*()_+-=[]{}|;':\",./<>?" + + let newFilters = NodeFilterParameters() + XCTAssertEqual(newFilters.searchText, "Test!@#$%^&*()_+-=[]{}|;':\",./<>?") } - func testDeviceRolesWithDuplicates() { - sut.deviceRoles = [1, 2, 3] - sut.deviceRoles.insert(2) // Try to insert duplicate + func testUnicodeInSearchText() async throws { + clearAllFilterDefaults() - 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") + let filters = NodeFilterParameters() + filters.searchText = "ๆต‹่ฏ• ะขะตัั‚ ๐ŸŽ‰๐Ÿš€" + + let newFilters = NodeFilterParameters() + XCTAssertEqual(newFilters.searchText, "ๆต‹่ฏ• ะขะตัั‚ ๐ŸŽ‰๐Ÿš€") } - 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 + func testLongSearchText() async throws { + clearAllFilterDefaults() - // 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) + let filters = NodeFilterParameters() + let longText = String(repeating: "A", count: 1000) + filters.searchText = longText - // 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) + let newFilters = NodeFilterParameters() + XCTAssertEqual(newFilters.searchText, longText) + } + + func testLargeDeviceRolesSet() async throws { + clearAllFilterDefaults() + + let filters = NodeFilterParameters() + filters.deviceRoles = Set(0...100) + + let newFilters = NodeFilterParameters() + XCTAssertEqual(newFilters.deviceRoles, Set(0...100)) + XCTAssertEqual(newFilters.deviceRoles.count, 101) + } + + // MARK: - Reset/Clear Tests + + func testResetAllFilters() async throws { + clearAllFilterDefaults() + + let filters = NodeFilterParameters() + + // Set all values to non-default + filters.searchText = "Test" + filters.isOnline = true + filters.isPkiEncrypted = true + filters.isFavorite = true + filters.isIgnored = true + filters.isEnvironment = true + filters.distanceFilter = true + filters.maxDistance = 500_000 + filters.hopsAway = 5.0 + filters.roleFilter = true + filters.deviceRoles = [1, 2, 3] + filters.viaLora = false + + // Reset to defaults + filters.searchText = "" + filters.isOnline = false + filters.isPkiEncrypted = false + filters.isFavorite = false + filters.isIgnored = false + filters.isEnvironment = false + filters.distanceFilter = false + filters.maxDistance = 800_000 + filters.hopsAway = -1.0 + filters.roleFilter = false + filters.deviceRoles = [] + filters.viaLora = true + filters.viaMqtt = true + + // Verify all are back to defaults + let newFilters = NodeFilterParameters() + XCTAssertEqual(newFilters.searchText, "") + XCTAssertEqual(newFilters.isOnline, false) + XCTAssertEqual(newFilters.isPkiEncrypted, false) + XCTAssertEqual(newFilters.isFavorite, false) + XCTAssertEqual(newFilters.isIgnored, false) + XCTAssertEqual(newFilters.isEnvironment, false) + XCTAssertEqual(newFilters.distanceFilter, false) + XCTAssertEqual(newFilters.maxDistance, 800_000) + XCTAssertEqual(newFilters.hopsAway, -1.0) + XCTAssertEqual(newFilters.roleFilter, false) + XCTAssertTrue(newFilters.deviceRoles.isEmpty) + XCTAssertEqual(newFilters.viaLora, true) + XCTAssertEqual(newFilters.viaMqtt, true) + } + + // MARK: - Helper Functions + + /// Clears all NodeFilter-related UserDefaults to ensure clean test state + private func clearAllFilterDefaults() { + let defaults = UserDefaults.standard + defaults.removeObject(forKey: "nodeFilter.searchText") + defaults.removeObject(forKey: "nodeFilter.isOnline") + defaults.removeObject(forKey: "nodeFilter.isPkiEncrypted") + defaults.removeObject(forKey: "nodeFilter.isFavorite") + defaults.removeObject(forKey: "nodeFilter.isIgnored") + defaults.removeObject(forKey: "nodeFilter.isEnvironment") + defaults.removeObject(forKey: "nodeFilter.distanceFilter") + defaults.removeObject(forKey: "nodeFilter.maxDistance") + defaults.removeObject(forKey: "nodeFilter.hopsAway") + defaults.removeObject(forKey: "nodeFilter.roleFilter") + defaults.removeObject(forKey: "nodeFilter.deviceRoles") + defaults.removeObject(forKey: "nodeFilter.viaLora") + defaults.removeObject(forKey: "nodeFilter.viaMqtt") } }