mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
* Delete Messages fix * Bump version to 2.7.9 * Bump widgets version * TAK Server channel index picker Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion. * Changed capitalization from 'environment' to 'Environment' for section header. (#1591) * Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612) * Initial plan * Add Danish (da) translations from PR #1609 Resolves merge conflicts from PR #1609 by adding Danish translations to the Localizable.xcstrings file. The PR adds Danish translation strings throughout the app while preserving all existing translations for other languages. Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Migrate test project to Swift Testing and add connect view and router tests (#1643) * Migrate to Swift Testing and add connect view tests - Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require) - Create ConnectViewTests.swift with tests for connect view child types: - Device struct (creation, signal strength, RSSI, description, codable) - TransportType enum (cases, raw values, codable) - ConnectionState enum (equality, codable) - BLESignalStrength enum (raw values, init) - TransportStatus enum (equality) - NavigationState (defaults, tabs, sub-states) - InvalidVersion view (creation with versions) - ConnectedDevice view (connected/disconnected/MQTT states) - CircleText view (default/custom sizes, emoji) - BatteryCompact view (levels, nil, charging, plugged in) - SignalStrengthIndicator view (dimensions, strength levels) - Update Xcode project to include new test file Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Fix signal strength test boundary conditions The getSignalStrength() method uses NSNumber.compare(.orderedDescending), which is a strict greater-than check. Fix the boundary test cases: - RSSI -65 is .normal (not .strong), since -65 is not > -65 - RSSI -85 is .weak (not .normal), since -85 is not > -85 - Add -64 → .strong and -84 → .normal as adjacent boundary tests Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Improve and complete router tests with comprehensive coverage Added tests for: - Custom initial state - Invalid scheme / unknown path handling (state unchanged) - navigateToNodeDetail public method - Messages edge cases: channelId only, userNum only, messageId only, non-numeric params - Nodes with non-numeric nodenum - Map with both nodenum+waypointId (nodeId priority), non-numeric params - Parameterized settings test covering all 31 SettingsNavigationState cases - State transitions: consecutive routes, invalid scheme preserves existing state Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * Localizable update * Merge translations file --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> * Fix merge conflicts in PR #1614 (Spanish translations) (#1644) * 20% of strings translated to spanish * add more translations * add rest of translations * small fixes --------- Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> * fix typo in hop limit option description (#1631) O hop -> 0 hop --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com> Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com> Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com> Co-authored-by: axunes <axunes@axunes.net>
This commit is contained in:
parent
6ee6579cdb
commit
54ff386c03
11 changed files with 13949 additions and 185 deletions
13248
Localizable.xcstrings
13248
Localizable.xcstrings
File diff suppressed because it is too large
Load diff
|
|
@ -82,6 +82,7 @@
|
|||
25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5BF2C3F6DA6008036E3 /* Router.swift */; };
|
||||
25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5C12C3F6E4B008036E3 /* AppState.swift */; };
|
||||
25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5D02C4375DF008036E3 /* RouterTests.swift */; };
|
||||
AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; };
|
||||
2849A5E4CE9FDC1DB33DFA34 /* TAKConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01028778B8BFD81F7A039593 /* TAKConnection.swift */; };
|
||||
300424F80C4A445A0FBAE82D /* TAKMeshtasticBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D006C85B250291D5925F30 /* TAKMeshtasticBridge.swift */; };
|
||||
3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */; };
|
||||
|
|
@ -411,6 +412,7 @@
|
|||
25F5D5C12C3F6E4B008036E3 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||
25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
25F5D5D02C4375DF008036E3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = "<group>"; };
|
||||
AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = "<group>"; };
|
||||
2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerManager.swift; sourceTree = "<group>"; };
|
||||
3D0A8ABAEF1E587683970927 /* EXICodec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EXICodec.swift; sourceTree = "<group>"; };
|
||||
3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -900,6 +902,7 @@
|
|||
25F5D5C82C4375A8008036E3 /* MeshtasticTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AA00010022E2730EC0060000 /* ConnectViewTests.swift */,
|
||||
25F5D5D02C4375DF008036E3 /* RouterTests.swift */,
|
||||
);
|
||||
path = MeshtasticTests;
|
||||
|
|
@ -1657,6 +1660,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */,
|
||||
25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -2170,7 +2174,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 2.7.8;
|
||||
MARKETING_VERSION = 2.7.9;
|
||||
OTHER_LDFLAGS = (
|
||||
"-weak_framework",
|
||||
SwiftUI,
|
||||
|
|
@ -2209,7 +2213,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 2.7.8;
|
||||
MARKETING_VERSION = 2.7.9;
|
||||
OTHER_LDFLAGS = (
|
||||
"-weak_framework",
|
||||
SwiftUI,
|
||||
|
|
@ -2245,7 +2249,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 2.7.8;
|
||||
MARKETING_VERSION = 2.7.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -2278,7 +2282,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 2.7.8;
|
||||
MARKETING_VERSION = 2.7.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
|
|
@ -157,6 +157,8 @@ final class TAKMeshtasticBridge {
|
|||
return
|
||||
}
|
||||
|
||||
let channel = UInt32(TAKServerManager.shared.channel)
|
||||
|
||||
// Determine send method based on CoT type
|
||||
let sendMethod = GenericCoTHandler.shared.classifySendMethod(for: cotMessage)
|
||||
|
||||
|
|
@ -169,7 +171,7 @@ final class TAKMeshtasticBridge {
|
|||
}
|
||||
|
||||
do {
|
||||
try await accessoryManager.sendTAKPacket(takPacket)
|
||||
try await accessoryManager.sendTAKPacket(takPacket, channel: channel)
|
||||
Logger.tak.info("Sent TAKPacket to mesh: \(cotMessage.type)")
|
||||
} catch {
|
||||
Logger.tak.error("Failed to send TAKPacket to mesh: \(error.localizedDescription)")
|
||||
|
|
@ -179,7 +181,7 @@ final class TAKMeshtasticBridge {
|
|||
// Use EXI compression on ATAK_FORWARDER port (257)
|
||||
GenericCoTHandler.shared.accessoryManager = accessoryManager
|
||||
do {
|
||||
try await GenericCoTHandler.shared.sendGenericCoT(cotMessage)
|
||||
try await GenericCoTHandler.shared.sendGenericCoT(cotMessage, channel: channel)
|
||||
Logger.tak.info("Sent generic CoT to mesh via ATAK_FORWARDER: \(cotMessage.type)")
|
||||
} catch {
|
||||
Logger.tak.error("Failed to send generic CoT to mesh: \(error.localizedDescription)")
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ final class TAKServerManager: ObservableObject {
|
|||
|
||||
// MARK: - Configuration (persisted via AppStorage)
|
||||
|
||||
@AppStorage("takServerChannel") var channel: Int = 0
|
||||
|
||||
@AppStorage("takServerEnabled") var enabled = false {
|
||||
didSet {
|
||||
Task {
|
||||
|
|
|
|||
|
|
@ -156,10 +156,16 @@ extension MeshPackets {
|
|||
|
||||
nonisolated public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) {
|
||||
do {
|
||||
let objects = channel.allPrivateMessages
|
||||
// Copied logic from ChannelEntity.allPrivateMessages, which is always on the MainActor
|
||||
// But this code may not be on the MainActor.
|
||||
let fetchRequest = MessageEntity.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", channel.index)
|
||||
let objects = (try? context.fetch(fetchRequest)) ?? [MessageEntity]()
|
||||
|
||||
for object in objects {
|
||||
context.delete(object)
|
||||
}
|
||||
|
||||
try context.save()
|
||||
} catch let error as NSError {
|
||||
Logger.data.error("\(error.localizedDescription, privacy: .public)")
|
||||
|
|
|
|||
|
|
@ -162,8 +162,13 @@ struct ChannelList: View {
|
|||
Button(role: .destructive) {
|
||||
Task {
|
||||
await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!)
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
channelToDeleteMessages = nil
|
||||
await MainActor.run {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
|
||||
// Reset state
|
||||
channelToDeleteMessages = nil
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Delete")
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ struct AppSettings: View {
|
|||
}
|
||||
#endif
|
||||
}
|
||||
Section(header: Text("environment")) {
|
||||
Section(header: Text("Environment")) {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $environmentEnableWeatherKit) {
|
||||
Label("Weather Conditions", systemImage: "cloud.sun")
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ struct LoRaConfig: View {
|
|||
.tag($0)
|
||||
}
|
||||
}
|
||||
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs.")
|
||||
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import OSLog
|
||||
import CoreData
|
||||
|
||||
enum CertificateImportType {
|
||||
case p12
|
||||
|
|
@ -15,6 +16,15 @@ enum CertificateImportType {
|
|||
}
|
||||
|
||||
struct TAKServerConfig: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)],
|
||||
predicate: NSPredicate(format: "role > 0"),
|
||||
animation: .default
|
||||
) private var channels: FetchedResults<ChannelEntity>
|
||||
|
||||
@StateObject private var takServer = TAKServerManager.shared
|
||||
@EnvironmentObject var accessoryManager: AccessoryManager
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
|
@ -267,6 +277,17 @@ struct TAKServerConfig: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
if !channels.isEmpty {
|
||||
Picker(selection: $takServer.channel) {
|
||||
ForEach(channels, id: \.index) { channel in
|
||||
channelLabel(channel)
|
||||
.tag(Int(channel.index))
|
||||
}
|
||||
} label: {
|
||||
Label("TAK Channel Index", systemImage: "bubble.left.and.bubble.right")
|
||||
}
|
||||
}
|
||||
|
||||
if takServer.isRunning {
|
||||
Button {
|
||||
Task {
|
||||
|
|
@ -279,7 +300,7 @@ struct TAKServerConfig: View {
|
|||
} header: {
|
||||
Text("Configuration")
|
||||
} footer: {
|
||||
Text("Secure mTLS connection on port 8089. Both server and client certificates are required.")
|
||||
Text("Secure mTLS connection on port 8089. Both server and client certificates are required. TAK Channel Index selects the channel index where TAK messages will be sent.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -407,6 +428,21 @@ struct TAKServerConfig: View {
|
|||
}
|
||||
|
||||
|
||||
// MARK: - Channel Label
|
||||
|
||||
@ViewBuilder
|
||||
private func channelLabel(_ channel: ChannelEntity) -> some View {
|
||||
if channel.name?.isEmpty ?? false {
|
||||
if channel.role == 1 {
|
||||
Text(String("PrimaryChannel").camelCaseToWords())
|
||||
} else {
|
||||
Text(String("Channel \(channel.index)").camelCaseToWords())
|
||||
}
|
||||
} else {
|
||||
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Import Handlers
|
||||
|
||||
private func handleP12Import(_ result: Result<[URL], Error>) {
|
||||
|
|
|
|||
493
MeshtasticTests/ConnectViewTests.swift
Normal file
493
MeshtasticTests/ConnectViewTests.swift
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
import Testing
|
||||
|
||||
@testable import Meshtastic
|
||||
|
||||
// MARK: - Device Tests
|
||||
|
||||
@Suite("Device")
|
||||
struct DeviceTests {
|
||||
|
||||
static let testUUID = UUID(uuidString: "12345678-1234-1234-1234-123456789ABC")!
|
||||
|
||||
@Test func creation() {
|
||||
let device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Test Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
#expect(device.id == DeviceTests.testUUID)
|
||||
#expect(device.name == "Test Radio")
|
||||
#expect(device.transportType == .ble)
|
||||
#expect(device.identifier == "BLE-001")
|
||||
#expect(device.connectionState == .disconnected)
|
||||
#expect(device.rssi == nil)
|
||||
#expect(device.num == nil)
|
||||
#expect(device.wasRestored == false)
|
||||
#expect(device.isManualConnection == false)
|
||||
}
|
||||
|
||||
@Test func creationWithAllProperties() {
|
||||
let device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Full Radio",
|
||||
transportType: .tcp,
|
||||
identifier: "192.168.1.1:4403",
|
||||
connectionState: .connected,
|
||||
rssi: -60,
|
||||
num: 123456,
|
||||
wasRestored: true,
|
||||
isManualConnection: true
|
||||
)
|
||||
#expect(device.connectionState == .connected)
|
||||
#expect(device.rssi == -60)
|
||||
#expect(device.num == 123456)
|
||||
#expect(device.wasRestored == true)
|
||||
#expect(device.isManualConnection == true)
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
(-50, BLESignalStrength.strong),
|
||||
(-64, BLESignalStrength.strong),
|
||||
(-65, BLESignalStrength.normal),
|
||||
(-80, BLESignalStrength.normal),
|
||||
(-84, BLESignalStrength.normal),
|
||||
(-85, BLESignalStrength.weak),
|
||||
(-100, BLESignalStrength.weak),
|
||||
])
|
||||
func signalStrength(rssi: Int, expected: BLESignalStrength) {
|
||||
let device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001",
|
||||
rssi: rssi
|
||||
)
|
||||
#expect(device.getSignalStrength() == expected)
|
||||
}
|
||||
|
||||
@Test func signalStrengthNilWhenNoRSSI() {
|
||||
let device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
#expect(device.getSignalStrength() == nil)
|
||||
}
|
||||
|
||||
@Test func rssiStringWithValue() {
|
||||
var device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001",
|
||||
rssi: -72
|
||||
)
|
||||
#expect(device.rssiString == "-72 dBm")
|
||||
|
||||
device.rssi = -100
|
||||
#expect(device.rssiString == "-100 dBm")
|
||||
}
|
||||
|
||||
@Test func rssiStringWithoutValue() {
|
||||
let device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
#expect(device.rssiString == "n/a")
|
||||
}
|
||||
|
||||
@Test func descriptionWithBothNames() {
|
||||
var device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
device.shortName = "TST"
|
||||
device.longName = "Test Node"
|
||||
#expect(device.description == "Test Node (TST)")
|
||||
}
|
||||
|
||||
@Test func descriptionWithShortNameOnly() {
|
||||
var device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
device.shortName = "TST"
|
||||
#expect(device.description == "TST")
|
||||
}
|
||||
|
||||
@Test func descriptionWithLongNameOnly() {
|
||||
var device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
device.longName = "Test Node"
|
||||
#expect(device.description == "Test Node")
|
||||
}
|
||||
|
||||
@Test func descriptionWithNoNames() {
|
||||
let device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
#expect(device.description == "Device(id: \(DeviceTests.testUUID))")
|
||||
}
|
||||
|
||||
@Test func hashEquality() {
|
||||
let device1 = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
let device2 = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001"
|
||||
)
|
||||
#expect(device1 == device2)
|
||||
#expect(device1.hashValue == device2.hashValue)
|
||||
}
|
||||
|
||||
@Test func codableRoundTrip() throws {
|
||||
var device = Device(
|
||||
id: DeviceTests.testUUID,
|
||||
name: "Radio",
|
||||
transportType: .ble,
|
||||
identifier: "BLE-001",
|
||||
connectionState: .connected,
|
||||
rssi: -70,
|
||||
num: 99
|
||||
)
|
||||
device.shortName = "RDO"
|
||||
device.longName = "My Radio"
|
||||
device.firmwareVersion = "2.5.0"
|
||||
|
||||
let data = try JSONEncoder().encode(device)
|
||||
let decoded = try JSONDecoder().decode(Device.self, from: data)
|
||||
|
||||
#expect(decoded.id == device.id)
|
||||
#expect(decoded.name == device.name)
|
||||
#expect(decoded.transportType == device.transportType)
|
||||
#expect(decoded.identifier == device.identifier)
|
||||
#expect(decoded.connectionState == device.connectionState)
|
||||
#expect(decoded.rssi == device.rssi)
|
||||
#expect(decoded.num == device.num)
|
||||
#expect(decoded.shortName == device.shortName)
|
||||
#expect(decoded.longName == device.longName)
|
||||
#expect(decoded.firmwareVersion == device.firmwareVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TransportType Tests
|
||||
|
||||
@Suite("TransportType")
|
||||
struct TransportTypeTests {
|
||||
|
||||
@Test func allCases() {
|
||||
let cases = TransportType.allCases
|
||||
#expect(cases.count == 3)
|
||||
#expect(cases.contains(.ble))
|
||||
#expect(cases.contains(.tcp))
|
||||
#expect(cases.contains(.serial))
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
(TransportType.ble, "BLE"),
|
||||
(TransportType.tcp, "TCP"),
|
||||
(TransportType.serial, "Serial"),
|
||||
])
|
||||
func rawValues(type: TransportType, expected: String) {
|
||||
#expect(type.rawValue == expected)
|
||||
}
|
||||
|
||||
@Test func initFromRawValue() {
|
||||
#expect(TransportType(rawValue: "BLE") == .ble)
|
||||
#expect(TransportType(rawValue: "TCP") == .tcp)
|
||||
#expect(TransportType(rawValue: "Serial") == .serial)
|
||||
#expect(TransportType(rawValue: "invalid") == nil)
|
||||
}
|
||||
|
||||
@Test func codableRoundTrip() throws {
|
||||
for type in TransportType.allCases {
|
||||
let data = try JSONEncoder().encode(type)
|
||||
let decoded = try JSONDecoder().decode(TransportType.self, from: data)
|
||||
#expect(decoded == type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ConnectionState Tests
|
||||
|
||||
@Suite("ConnectionState")
|
||||
struct ConnectionStateTests {
|
||||
|
||||
@Test func equality() {
|
||||
#expect(ConnectionState.disconnected == .disconnected)
|
||||
#expect(ConnectionState.connecting == .connecting)
|
||||
#expect(ConnectionState.connected == .connected)
|
||||
#expect(ConnectionState.disconnected != .connected)
|
||||
#expect(ConnectionState.connecting != .disconnected)
|
||||
}
|
||||
|
||||
@Test func codableRoundTrip() throws {
|
||||
let states: [ConnectionState] = [.disconnected, .connecting, .connected]
|
||||
for state in states {
|
||||
let data = try JSONEncoder().encode(state)
|
||||
let decoded = try JSONDecoder().decode(ConnectionState.self, from: data)
|
||||
#expect(decoded == state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BLESignalStrength Tests
|
||||
|
||||
@Suite("BLESignalStrength")
|
||||
struct BLESignalStrengthTests {
|
||||
|
||||
@Test func rawValues() {
|
||||
#expect(BLESignalStrength.weak.rawValue == 0)
|
||||
#expect(BLESignalStrength.normal.rawValue == 1)
|
||||
#expect(BLESignalStrength.strong.rawValue == 2)
|
||||
}
|
||||
|
||||
@Test func initFromRawValue() {
|
||||
#expect(BLESignalStrength(rawValue: 0) == .weak)
|
||||
#expect(BLESignalStrength(rawValue: 1) == .normal)
|
||||
#expect(BLESignalStrength(rawValue: 2) == .strong)
|
||||
#expect(BLESignalStrength(rawValue: 3) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TransportStatus Tests
|
||||
|
||||
@Suite("TransportStatus")
|
||||
struct TransportStatusTests {
|
||||
|
||||
@Test func equality() {
|
||||
#expect(TransportStatus.uninitialized == .uninitialized)
|
||||
#expect(TransportStatus.ready == .ready)
|
||||
#expect(TransportStatus.discovering == .discovering)
|
||||
#expect(TransportStatus.error("test") == .error("test"))
|
||||
#expect(TransportStatus.error("a") != .error("b"))
|
||||
#expect(TransportStatus.ready != .discovering)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NavigationState Tests
|
||||
|
||||
@Suite("NavigationState")
|
||||
struct NavigationStateTests {
|
||||
|
||||
@Test func defaultState() {
|
||||
let state = NavigationState()
|
||||
#expect(state.selectedTab == .connect)
|
||||
#expect(state.messages == nil)
|
||||
#expect(state.nodeListSelectedNodeNum == nil)
|
||||
#expect(state.map == nil)
|
||||
#expect(state.settings == nil)
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
NavigationState.Tab.messages,
|
||||
NavigationState.Tab.connect,
|
||||
NavigationState.Tab.nodes,
|
||||
NavigationState.Tab.map,
|
||||
NavigationState.Tab.settings,
|
||||
])
|
||||
func tabRawValues(tab: NavigationState.Tab) {
|
||||
#expect(NavigationState.Tab(rawValue: tab.rawValue) == tab)
|
||||
}
|
||||
|
||||
@Test func messagesNavigationState() {
|
||||
let channels = MessagesNavigationState.channels(channelId: 1, messageId: 100)
|
||||
let directMessages = MessagesNavigationState.directMessages(userNum: 42, messageId: 200)
|
||||
|
||||
let state1 = NavigationState(selectedTab: .messages, messages: channels)
|
||||
let state2 = NavigationState(selectedTab: .messages, messages: directMessages)
|
||||
|
||||
#expect(state1 != state2)
|
||||
#expect(state1.messages != nil)
|
||||
#expect(state2.messages != nil)
|
||||
}
|
||||
|
||||
@Test func mapNavigationState() {
|
||||
let selectedNode = MapNavigationState.selectedNode(12345)
|
||||
let waypoint = MapNavigationState.waypoint(67890)
|
||||
|
||||
#expect(selectedNode != waypoint)
|
||||
#expect(MapNavigationState.selectedNode(12345) == selectedNode)
|
||||
}
|
||||
|
||||
@Test func settingsNavigationState() {
|
||||
#expect(SettingsNavigationState(rawValue: "about") == .about)
|
||||
#expect(SettingsNavigationState(rawValue: "appSettings") == .appSettings)
|
||||
#expect(SettingsNavigationState(rawValue: "lora") == .lora)
|
||||
#expect(SettingsNavigationState(rawValue: "mqtt") == .mqtt)
|
||||
#expect(SettingsNavigationState(rawValue: "nonexistent") == nil)
|
||||
}
|
||||
|
||||
@Test func hashable() {
|
||||
let state1 = NavigationState(selectedTab: .connect)
|
||||
let state2 = NavigationState(selectedTab: .connect)
|
||||
let state3 = NavigationState(selectedTab: .messages)
|
||||
|
||||
#expect(state1 == state2)
|
||||
#expect(state1 != state3)
|
||||
#expect(state1.hashValue == state2.hashValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InvalidVersion View Tests
|
||||
|
||||
@Suite("InvalidVersion")
|
||||
struct InvalidVersionTests {
|
||||
|
||||
@Test func viewCreation() {
|
||||
let view = InvalidVersion(minimumVersion: "2.5.0", version: "2.3.0")
|
||||
#expect(view.minimumVersion == "2.5.0")
|
||||
#expect(view.version == "2.3.0")
|
||||
}
|
||||
|
||||
@Test func viewCreationWithEmptyVersions() {
|
||||
let view = InvalidVersion()
|
||||
#expect(view.minimumVersion == "")
|
||||
#expect(view.version == "")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ConnectedDevice View Tests
|
||||
|
||||
@Suite("ConnectedDevice")
|
||||
struct ConnectedDeviceTests {
|
||||
|
||||
@Test func connectedState() {
|
||||
let view = ConnectedDevice(deviceConnected: true, name: "TEST")
|
||||
#expect(view.deviceConnected == true)
|
||||
#expect(view.name == "TEST")
|
||||
#expect(view.mqttProxyConnected == false)
|
||||
#expect(view.showActivityLights == true)
|
||||
}
|
||||
|
||||
@Test func disconnectedState() {
|
||||
let view = ConnectedDevice(deviceConnected: false, name: "?")
|
||||
#expect(view.deviceConnected == false)
|
||||
#expect(view.name == "?")
|
||||
}
|
||||
|
||||
@Test func withMQTTOptions() {
|
||||
let view = ConnectedDevice(
|
||||
deviceConnected: true,
|
||||
name: "MQTT",
|
||||
mqttProxyConnected: true,
|
||||
mqttUplinkEnabled: true,
|
||||
mqttDownlinkEnabled: true,
|
||||
mqttTopic: "msh/US/2/e/#"
|
||||
)
|
||||
#expect(view.mqttProxyConnected == true)
|
||||
#expect(view.mqttUplinkEnabled == true)
|
||||
#expect(view.mqttDownlinkEnabled == true)
|
||||
#expect(view.mqttTopic == "msh/US/2/e/#")
|
||||
}
|
||||
|
||||
@Test func phoneOnlyMode() {
|
||||
let view = ConnectedDevice(
|
||||
deviceConnected: true,
|
||||
name: "PHON",
|
||||
phoneOnly: true,
|
||||
showActivityLights: false
|
||||
)
|
||||
#expect(view.phoneOnly == true)
|
||||
#expect(view.showActivityLights == false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CircleText View Tests
|
||||
|
||||
@Suite("CircleText")
|
||||
struct CircleTextTests {
|
||||
|
||||
@Test func defaultCircleSize() {
|
||||
let view = CircleText(text: "AB", color: .blue)
|
||||
#expect(view.text == "AB")
|
||||
#expect(view.circleSize == 45)
|
||||
}
|
||||
|
||||
@Test func customCircleSize() {
|
||||
let view = CircleText(text: "XY", color: .red, circleSize: 90)
|
||||
#expect(view.text == "XY")
|
||||
#expect(view.circleSize == 90)
|
||||
}
|
||||
|
||||
@Test func emojiText() {
|
||||
let view = CircleText(text: "😝", color: .orange, circleSize: 80)
|
||||
#expect(view.text == "😝")
|
||||
#expect(view.circleSize == 80)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BatteryCompact View Tests
|
||||
|
||||
@Suite("BatteryCompact")
|
||||
struct BatteryCompactTests {
|
||||
|
||||
@Test func creationWithLevel() {
|
||||
let view = BatteryCompact(batteryLevel: 75, font: .caption, iconFont: .callout, color: .accentColor)
|
||||
#expect(view.batteryLevel == 75)
|
||||
}
|
||||
|
||||
@Test func creationWithNilLevel() {
|
||||
let view = BatteryCompact(batteryLevel: nil, font: .caption, iconFont: .callout, color: .accentColor)
|
||||
#expect(view.batteryLevel == nil)
|
||||
}
|
||||
|
||||
@Test func pluggedInLevel() {
|
||||
let view = BatteryCompact(batteryLevel: 101, font: .caption, iconFont: .callout, color: .accentColor)
|
||||
#expect(view.batteryLevel! > 100)
|
||||
}
|
||||
|
||||
@Test func chargingLevel() {
|
||||
let view = BatteryCompact(batteryLevel: 100, font: .caption, iconFont: .callout, color: .accentColor)
|
||||
#expect(view.batteryLevel == 100)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SignalStrengthIndicator View Tests
|
||||
|
||||
@Suite("SignalStrengthIndicator")
|
||||
struct SignalStrengthIndicatorTests {
|
||||
|
||||
@Test func defaultDimensions() {
|
||||
let view = SignalStrengthIndicator(signalStrength: .strong)
|
||||
#expect(view.signalStrength == .strong)
|
||||
#expect(view.width == 8)
|
||||
#expect(view.height == 40)
|
||||
}
|
||||
|
||||
@Test func customDimensions() {
|
||||
let view = SignalStrengthIndicator(signalStrength: .weak, width: 5, height: 20)
|
||||
#expect(view.signalStrength == .weak)
|
||||
#expect(view.width == 5)
|
||||
#expect(view.height == 20)
|
||||
}
|
||||
|
||||
@Test(arguments: [BLESignalStrength.weak, .normal, .strong])
|
||||
func allStrengthLevels(strength: BLESignalStrength) {
|
||||
let view = SignalStrengthIndicator(signalStrength: strength)
|
||||
#expect(view.signalStrength == strength)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,148 +1,300 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
import Testing
|
||||
|
||||
@testable import Meshtastic
|
||||
|
||||
final class RouterTests: XCTestCase {
|
||||
@Suite("Router")
|
||||
struct RouterTests {
|
||||
|
||||
func testInitialState() async throws {
|
||||
// MARK: - Initialization
|
||||
|
||||
@Test func defaultInitialState() async {
|
||||
let router = await Router()
|
||||
let state = await router.navigationState
|
||||
#expect(state.selectedTab == .connect)
|
||||
#expect(state.messages == nil)
|
||||
#expect(state.nodeListSelectedNodeNum == nil)
|
||||
#expect(state.map == nil)
|
||||
#expect(state.settings == nil)
|
||||
}
|
||||
|
||||
@Test func customInitialState() async {
|
||||
let custom = NavigationState(selectedTab: .map, map: .waypoint(42))
|
||||
let router = await Router(navigationState: custom)
|
||||
let state = await router.navigationState
|
||||
#expect(state == custom)
|
||||
}
|
||||
|
||||
// MARK: - Invalid URL Handling
|
||||
|
||||
@Test func invalidSchemeIsIgnored() async throws {
|
||||
let router = await Router()
|
||||
let url = try #require(URL(string: "https:///messages"))
|
||||
await router.route(url: url)
|
||||
let tab = await router.navigationState.selectedTab
|
||||
XCTAssertEqual(tab, .connect)
|
||||
#expect(tab == .connect)
|
||||
}
|
||||
|
||||
func testRouteMessages() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///messages",
|
||||
NavigationState(selectedTab: .messages)
|
||||
)
|
||||
@Test func unknownPathIsIgnored() async throws {
|
||||
let router = await Router()
|
||||
let url = try #require(URL(string: "meshtastic:///unknown"))
|
||||
await router.route(url: url)
|
||||
let state = await router.navigationState
|
||||
#expect(state == NavigationState(selectedTab: .connect))
|
||||
}
|
||||
|
||||
func testRouteMessagesWithChannelIdAndMessageId() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///messages?channelId=0&messageId=1122334455",
|
||||
NavigationState(
|
||||
selectedTab: .messages,
|
||||
messages: .channels(
|
||||
channelId: 0,
|
||||
messageId: 1122334455
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
// MARK: - Connect
|
||||
|
||||
func testRouteMessagesWithUserNumAndMessageId() async throws {
|
||||
@Test func routeConnect() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///messages?userNum=123456789&messageId=9876543210",
|
||||
NavigationState(
|
||||
selectedTab: .messages,
|
||||
messages: .directMessages(
|
||||
userNum: 123456789,
|
||||
messageId: 9876543210
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteConnect() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///connect",
|
||||
NavigationState(selectedTab: .connect)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteNodes() async throws {
|
||||
// MARK: - Messages
|
||||
|
||||
@Test func routeMessages() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///messages",
|
||||
NavigationState(selectedTab: .messages)
|
||||
)
|
||||
}
|
||||
|
||||
@Test func routeMessagesWithChannelIdAndMessageId() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///messages?channelId=0&messageId=1122334455",
|
||||
NavigationState(
|
||||
selectedTab: .messages,
|
||||
messages: .channels(channelId: 0, messageId: 1122334455)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test func routeMessagesWithChannelIdOnly() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///messages?channelId=5",
|
||||
NavigationState(
|
||||
selectedTab: .messages,
|
||||
messages: .channels(channelId: 5, messageId: nil)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test func routeMessagesWithUserNumAndMessageId() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///messages?userNum=123456789&messageId=9876543210",
|
||||
NavigationState(
|
||||
selectedTab: .messages,
|
||||
messages: .directMessages(userNum: 123456789, messageId: 9876543210)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test func routeMessagesWithUserNumOnly() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///messages?userNum=42",
|
||||
NavigationState(
|
||||
selectedTab: .messages,
|
||||
messages: .directMessages(userNum: 42, messageId: nil)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test func routeMessagesWithOnlyMessageIdIgnoresIt() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///messages?messageId=999",
|
||||
NavigationState(selectedTab: .messages)
|
||||
)
|
||||
}
|
||||
|
||||
@Test func routeMessagesWithNonNumericParamsIgnoresThem() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///messages?channelId=abc&messageId=xyz",
|
||||
NavigationState(selectedTab: .messages)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Nodes
|
||||
|
||||
@Test func routeNodes() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///nodes",
|
||||
NavigationState(selectedTab: .nodes)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteNodesWithNodeNum() async throws {
|
||||
@Test func routeNodesWithNodeNum() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///nodes?nodenum=1234567890",
|
||||
NavigationState(
|
||||
selectedTab: .nodes,
|
||||
nodeListSelectedNodeNum: 1234567890
|
||||
)
|
||||
NavigationState(selectedTab: .nodes, nodeListSelectedNodeNum: 1234567890)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteMap() async throws {
|
||||
@Test func routeNodesWithNonNumericNodeNum() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///nodes?nodenum=abc",
|
||||
NavigationState(selectedTab: .nodes)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Map
|
||||
|
||||
@Test func routeMap() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///map",
|
||||
NavigationState(selectedTab: .map)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteMapWithWaypointId() async throws {
|
||||
@Test func routeMapWithWaypointId() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///map?waypointId=123456",
|
||||
NavigationState(
|
||||
selectedTab: .map,
|
||||
map: .waypoint(123456)
|
||||
)
|
||||
NavigationState(selectedTab: .map, map: .waypoint(123456))
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteMapWithNodeNum() async throws {
|
||||
@Test func routeMapWithNodeNum() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///map?nodenum=1234567890",
|
||||
NavigationState(
|
||||
selectedTab: .map,
|
||||
map: .selectedNode(1234567890)
|
||||
)
|
||||
NavigationState(selectedTab: .map, map: .selectedNode(1234567890))
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteSettings() async throws {
|
||||
@Test func routeMapWithBothNodeNumAndWaypointIdPrefersNode() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///map?nodenum=111&waypointId=222",
|
||||
NavigationState(selectedTab: .map, map: .selectedNode(111))
|
||||
)
|
||||
}
|
||||
|
||||
@Test func routeMapWithNonNumericParamsIgnoresThem() async throws {
|
||||
try await assertRoute(
|
||||
"meshtastic:///map?nodenum=abc&waypointId=xyz",
|
||||
NavigationState(selectedTab: .map)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
@Test func routeSettings() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///settings",
|
||||
NavigationState(
|
||||
selectedTab: .settings
|
||||
)
|
||||
NavigationState(selectedTab: .settings)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteSettingsAbout() async throws {
|
||||
@Test(arguments: [
|
||||
("about", SettingsNavigationState.about),
|
||||
("appSettings", SettingsNavigationState.appSettings),
|
||||
("routes", SettingsNavigationState.routes),
|
||||
("routeRecorder", SettingsNavigationState.routeRecorder),
|
||||
("lora", SettingsNavigationState.lora),
|
||||
("channels", SettingsNavigationState.channels),
|
||||
("shareQRCode", SettingsNavigationState.shareQRCode),
|
||||
("user", SettingsNavigationState.user),
|
||||
("bluetooth", SettingsNavigationState.bluetooth),
|
||||
("device", SettingsNavigationState.device),
|
||||
("display", SettingsNavigationState.display),
|
||||
("network", SettingsNavigationState.network),
|
||||
("position", SettingsNavigationState.position),
|
||||
("power", SettingsNavigationState.power),
|
||||
("ambientLighting", SettingsNavigationState.ambientLighting),
|
||||
("cannedMessages", SettingsNavigationState.cannedMessages),
|
||||
("detectionSensor", SettingsNavigationState.detectionSensor),
|
||||
("externalNotification", SettingsNavigationState.externalNotification),
|
||||
("mqtt", SettingsNavigationState.mqtt),
|
||||
("rangeTest", SettingsNavigationState.rangeTest),
|
||||
("paxCounter", SettingsNavigationState.paxCounter),
|
||||
("ringtone", SettingsNavigationState.ringtone),
|
||||
("serial", SettingsNavigationState.serial),
|
||||
("security", SettingsNavigationState.security),
|
||||
("storeAndForward", SettingsNavigationState.storeAndForward),
|
||||
("telemetry", SettingsNavigationState.telemetry),
|
||||
("debugLogs", SettingsNavigationState.debugLogs),
|
||||
("appFiles", SettingsNavigationState.appFiles),
|
||||
("firmwareUpdates", SettingsNavigationState.firmwareUpdates),
|
||||
("tak", SettingsNavigationState.tak),
|
||||
])
|
||||
func routeSettingsPage(path: String, expected: SettingsNavigationState) async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///settings/about",
|
||||
NavigationState(
|
||||
selectedTab: .settings,
|
||||
settings: .about
|
||||
)
|
||||
"meshtastic:///settings/\(path)",
|
||||
NavigationState(selectedTab: .settings, settings: expected)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteSettingsInvalidSetting() async throws {
|
||||
@Test func routeSettingsInvalidSetting() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///settings/invalidSetting",
|
||||
NavigationState(
|
||||
selectedTab: .settings
|
||||
)
|
||||
NavigationState(selectedTab: .settings)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - navigateToNodeDetail
|
||||
|
||||
@Test func navigateToNodeDetail() async {
|
||||
let router = await Router()
|
||||
await router.navigateToNodeDetail(nodeNum: 9876543210)
|
||||
let state = await router.navigationState
|
||||
#expect(state.selectedTab == .nodes)
|
||||
#expect(state.nodeListSelectedNodeNum == 9876543210)
|
||||
}
|
||||
|
||||
// MARK: - State Transitions
|
||||
|
||||
@Test func routingToNewTabClearsPreviousState() async throws {
|
||||
let router = await Router()
|
||||
|
||||
// First, route to messages with channel state
|
||||
let messagesURL = try #require(URL(string: "meshtastic:///messages?channelId=1&messageId=100"))
|
||||
await router.route(url: messagesURL)
|
||||
let messagesState = await router.navigationState
|
||||
#expect(messagesState.selectedTab == .messages)
|
||||
#expect(messagesState.messages != nil)
|
||||
|
||||
// Then route to map — messages state should remain but tab changes
|
||||
let mapURL = try #require(URL(string: "meshtastic:///map?waypointId=42"))
|
||||
await router.route(url: mapURL)
|
||||
let mapState = await router.navigationState
|
||||
#expect(mapState.selectedTab == .map)
|
||||
#expect(mapState.map == .waypoint(42))
|
||||
}
|
||||
|
||||
@Test func consecutiveRoutesUpdateState() async throws {
|
||||
let router = await Router()
|
||||
|
||||
let nodesURL = try #require(URL(string: "meshtastic:///nodes?nodenum=111"))
|
||||
await router.route(url: nodesURL)
|
||||
let first = await router.navigationState
|
||||
#expect(first.selectedTab == .nodes)
|
||||
#expect(first.nodeListSelectedNodeNum == 111)
|
||||
|
||||
let nodesURL2 = try #require(URL(string: "meshtastic:///nodes?nodenum=222"))
|
||||
await router.route(url: nodesURL2)
|
||||
let second = await router.navigationState
|
||||
#expect(second.selectedTab == .nodes)
|
||||
#expect(second.nodeListSelectedNodeNum == 222)
|
||||
}
|
||||
|
||||
@Test func invalidSchemeDoesNotMutateExistingState() async throws {
|
||||
let initial = NavigationState(selectedTab: .map, map: .waypoint(99))
|
||||
let router = await Router(navigationState: initial)
|
||||
let badURL = try #require(URL(string: "https:///messages"))
|
||||
await router.route(url: badURL)
|
||||
let state = await router.navigationState
|
||||
#expect(state == initial)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func assertRoute(
|
||||
router: Router,
|
||||
_ urlString: String,
|
||||
_ destination: NavigationState
|
||||
) async throws {
|
||||
let url = try XCTUnwrap(URL(string: urlString))
|
||||
let router = await Router()
|
||||
let url = try #require(URL(string: urlString))
|
||||
await router.route(url: url)
|
||||
let state = await router.navigationState
|
||||
XCTAssertEqual(state, destination)
|
||||
#expect(state == destination)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue