Meshtastic-Apple/MeshtasticTests/DeviceOnboardingTests.swift
Garth Vander Houwen 4839a978ab Onboarding tests
2026-04-18 21:25:40 -07:00

238 lines
6.9 KiB
Swift

//
// DeviceOnboardingTests.swift
// MeshtasticTests
//
// Copyright(c) Garth Vander Houwen 2026.
//
import Foundation
import CoreLocation
import UserNotifications
import Testing
@testable import Meshtastic
// MARK: - SetupGuide Enum
@Suite("DeviceOnboarding.SetupGuide")
struct SetupGuideTests {
@Test func allCasesExist() {
let cases: [DeviceOnboarding.SetupGuide] = [
.notifications, .location, .backgroundActivity,
.localNetwork, .bluetooth, .siri
]
#expect(cases.count == 6)
}
@Test func isHashable() {
var seen = Set<DeviceOnboarding.SetupGuide>()
seen.insert(.notifications)
seen.insert(.notifications) // duplicate should not grow set
seen.insert(.siri)
#expect(seen.count == 2)
}
@Test func equality() {
#expect(DeviceOnboarding.SetupGuide.bluetooth == .bluetooth)
#expect(DeviceOnboarding.SetupGuide.notifications != .siri)
#expect(DeviceOnboarding.SetupGuide.location != .backgroundActivity)
}
@Test func allCasesAreUnique() {
let cases: [DeviceOnboarding.SetupGuide] = [
.notifications, .location, .backgroundActivity,
.localNetwork, .bluetooth, .siri
]
let unique = Set(cases)
#expect(unique.count == cases.count)
}
}
// MARK: - Attributed String Formatters
@Suite("DeviceOnboarding string formatters")
struct OnboardingStringFormatterTests {
let view = DeviceOnboarding()
// Helpers
private func hasSettingsLink(_ string: AttributedString) -> Bool {
guard let range = string.range(of: "settings") else { return false }
return string[range].link != nil
}
private func settingsLinkURL(_ string: AttributedString) -> URL? {
guard let range = string.range(of: "settings") else { return nil }
return string[range].link
}
@Test func backgroundActivityStringContainsText() {
let string = view.createBackgroundActivityString()
#expect(string.description.contains("background"))
#expect(string.description.contains("settings"))
}
@Test func backgroundActivityStringHasSettingsLink() {
let string = view.createBackgroundActivityString()
#expect(hasSettingsLink(string))
}
@Test func backgroundActivitySettingsLinkIsAppSettings() {
let string = view.createBackgroundActivityString()
let url = settingsLinkURL(string)
#expect(url?.scheme == "app-settings" || url?.absoluteString.contains("settings") == true)
}
@Test func locationStringContainsText() {
let string = view.createLocationString()
#expect(string.description.contains("location"))
#expect(string.description.contains("settings"))
}
@Test func locationStringHasSettingsLink() {
let string = view.createLocationString()
#expect(hasSettingsLink(string))
}
@Test func localNetworkStringContainsText() {
let string = view.createLocalNetworkString()
#expect(string.description.contains("local network") || string.description.contains("TCP"))
#expect(string.description.contains("settings"))
}
@Test func localNetworkStringHasSettingsLink() {
let string = view.createLocalNetworkString()
#expect(hasSettingsLink(string))
}
@Test func bluetoothStringContainsText() {
let string = view.createBluetoothString()
#expect(string.description.contains("Bluetooth") || string.description.contains("BLE"))
#expect(string.description.contains("settings"))
}
@Test func bluetoothStringHasSettingsLink() {
let string = view.createBluetoothString()
#expect(hasSettingsLink(string))
}
@Test func siriStringContainsCarPlay() {
let string = view.createSiriString()
#expect(string.description.contains("CarPlay"))
}
@Test func siriStringContainsSiri() {
let string = view.createSiriString()
#expect(string.description.contains("Siri"))
}
@Test func siriStringHasSettingsLink() {
let string = view.createSiriString()
#expect(hasSettingsLink(string))
}
@Test func allStringsHaveSettingsLinks() {
let strings = [
view.createBackgroundActivityString(),
view.createLocationString(),
view.createLocalNetworkString(),
view.createBluetoothString(),
view.createSiriString()
]
for string in strings {
#expect(hasSettingsLink(string), "Expected 'settings' link in: \(string)")
}
}
}
// MARK: - Navigation Flow
@Suite("DeviceOnboarding navigation")
struct OnboardingNavigationTests {
private func nextStep(
after step: DeviceOnboarding.SetupGuide?,
notificationStatus: UNAuthorizationStatus,
criticalAlertSetting: UNNotificationSetting,
locationStatus: CLAuthorizationStatus
) -> DeviceOnboarding.SetupGuide? {
let view = DeviceOnboarding()
return view.nextStep(
after: step,
notificationStatus: notificationStatus,
criticalAlertSetting: criticalAlertSetting,
locationStatus: locationStatus
)
}
@Test func startRoutesToNotificationsWhenNotificationsUnknown() {
let step = nextStep(
after: nil,
notificationStatus: .notDetermined,
criticalAlertSetting: .notSupported,
locationStatus: .authorizedAlways
)
#expect(step == .notifications)
}
@Test func startRoutesToLocationWhenNotificationsKnownAndLocationDenied() {
let step = nextStep(
after: nil,
notificationStatus: .authorized,
criticalAlertSetting: .enabled,
locationStatus: .denied
)
#expect(step == .location)
}
@Test func startRoutesToBackgroundActivityWhenLocationAuthorized() {
let step = nextStep(
after: nil,
notificationStatus: .authorized,
criticalAlertSetting: .enabled,
locationStatus: .authorizedWhenInUse
)
#expect(step == .backgroundActivity)
}
@Test func notificationsRoutesToLocationOrBackgroundActivity() {
let denied = nextStep(
after: .notifications,
notificationStatus: .authorized,
criticalAlertSetting: .enabled,
locationStatus: .denied
)
let authorized = nextStep(
after: .notifications,
notificationStatus: .authorized,
criticalAlertSetting: .enabled,
locationStatus: .authorizedAlways
)
#expect(denied == .location)
#expect(authorized == .backgroundActivity)
}
@Test func locationRoutesToBackgroundActivityOnlyWhenAuthorized() {
let authorized = nextStep(
after: .location,
notificationStatus: .authorized,
criticalAlertSetting: .enabled,
locationStatus: .authorizedAlways
)
let denied = nextStep(
after: .location,
notificationStatus: .authorized,
criticalAlertSetting: .enabled,
locationStatus: .denied
)
#expect(authorized == .backgroundActivity)
#expect(denied == nil)
}
@Test func deterministicTailFlowMapping() {
#expect(nextStep(after: .backgroundActivity, notificationStatus: .authorized, criticalAlertSetting: .enabled, locationStatus: .authorizedAlways) == .localNetwork)
#expect(nextStep(after: .localNetwork, notificationStatus: .authorized, criticalAlertSetting: .enabled, locationStatus: .authorizedAlways) == .bluetooth)
#expect(nextStep(after: .bluetooth, notificationStatus: .authorized, criticalAlertSetting: .enabled, locationStatus: .authorizedAlways) == .siri)
#expect(nextStep(after: .siri, notificationStatus: .authorized, criticalAlertSetting: .enabled, locationStatus: .authorizedAlways) == nil)
}
}